/* * SoapUI, Copyright (C) 2004-2016 SmartBear Software * * Licensed under the EUPL, Version 1.1 or - as soon as they will be approved by the European Commission - subsequent * versions of the EUPL (the "Licence"); * You may not use this work except in compliance with the Licence. * You may obtain a copy of the Licence at: * * http://ec.europa.eu/idabc/eupl * * Unless required by applicable law or agreed to in writing, software distributed under the Licence is * distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either * express or implied. See the Licence for the specific language governing permissions and limitations * under the Licence. */ package com.eviware.soapui.impl.wsdl.loadtest.data; import com.eviware.soapui.SoapUI; import com.eviware.soapui.impl.wsdl.loadtest.ColorPalette; import com.eviware.soapui.impl.wsdl.loadtest.WsdlLoadTest; import com.eviware.soapui.impl.wsdl.testcase.WsdlTestCase; import com.eviware.soapui.model.support.LoadTestRunListenerAdapter; import com.eviware.soapui.model.support.TestSuiteListenerAdapter; import com.eviware.soapui.model.testsuite.LoadTestRunContext; import com.eviware.soapui.model.testsuite.LoadTestRunner; import com.eviware.soapui.model.testsuite.TestCase; import com.eviware.soapui.model.testsuite.TestCaseRunContext; import com.eviware.soapui.model.testsuite.TestCaseRunner; import com.eviware.soapui.model.testsuite.TestRunner; import com.eviware.soapui.model.testsuite.TestStep; import com.eviware.soapui.model.testsuite.TestStepResult; import com.eviware.soapui.support.types.StringList; import org.apache.log4j.Logger; import javax.swing.table.AbstractTableModel; import java.awt.Color; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.EmptyStackException; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; /** * Model holding statistics.. should be refactored into interface for different * statistic models * * @author Ole.Matzura */ public final class LoadTestStatistics extends AbstractTableModel implements Runnable { public final static String NO_STATS_TESTCASE_CANCEL_REASON = "NO_STATS_TESTCASE_CANCEL_REASON"; private final static Logger log = Logger.getLogger(LoadTestStatistics.class); private final WsdlLoadTest loadTest; private long[][] data; private final static int MIN_COLUMN = 0; private final static int MAX_COLUMN = 1; private final static int AVG_COLUMN = 2; private final static int LAST_COLUMN = 3; private final static int CNT_COLUMN = 4; private final static int TPS_COLUMN = 5; private final static int BYTES_COLUMN = 6; private final static int BPS_COLUMN = 7; private final static int ERR_COLUMN = 8; private final static int SUM_COLUMN = 9; private final static int CURRENT_CNT_COLUMN = 10; private final static int RATIO_COLUMN = 11; public static final int TOTAL = -1; public static final int DEFAULT_SAMPLE_INTERVAL = 250; private InternalTestRunListener testRunListener; private InternalTestSuiteListener testSuiteListener; private InternalPropertyChangeListener propertyChangeListener; private StatisticsHistory history; private boolean changed; private long updateFrequency = DEFAULT_SAMPLE_INTERVAL; private Queue<SamplesHolder> samplesStack = new ConcurrentLinkedQueue<SamplesHolder>(); private long currentThreadCountStartTime; private long totalAverageSum; private boolean resetStatistics; private boolean running; private boolean adding; public LoadTestStatistics(WsdlLoadTest loadTest) { this.loadTest = loadTest; testRunListener = new InternalTestRunListener(); testSuiteListener = new InternalTestSuiteListener(); propertyChangeListener = new InternalPropertyChangeListener(); WsdlTestCase testCase = loadTest.getTestCase(); loadTest.addPropertyChangeListener(propertyChangeListener); loadTest.addLoadTestRunListener(testRunListener); testCase.getTestSuite().addTestSuiteListener(testSuiteListener); for (TestStep testStep : testCase.getTestStepList()) { testStep.addPropertyChangeListener(propertyChangeListener); } history = new StatisticsHistory(this); init(); } private void init() { data = new long[getRowCount()][11]; } public StatisticsHistory getHistory() { return history; } public int getRowCount() { return loadTest.getTestCase().getTestStepCount() + 1; } public WsdlLoadTest getLoadTest() { return loadTest; } public int getColumnCount() { return 12; } public String getColumnName(int columnIndex) { switch (columnIndex) { case 0: return " "; case 1: return "Test Step"; case 2: return Statistic.MININMUM.getName(); case 3: return Statistic.MAXIMUM.getName(); case 4: return Statistic.AVERAGE.getName(); case 5: return Statistic.LAST.getName(); case 6: return Statistic.COUNT.getName(); case 7: return Statistic.TPS.getName(); case 8: return Statistic.BYTES.getName(); case 9: return Statistic.BPS.getName(); case 10: return Statistic.ERRORS.getName(); case 11: return Statistic.ERRORRATIO.getName(); } return null; } public Class<?> getColumnClass(int columnIndex) { switch (columnIndex) { case 0: return Color.class; case 1: return String.class; case 4: case 7: return Float.class; default: return Long.class; } } public boolean isCellEditable(int rowIndex, int columnIndex) { return false; } public long getStatistic(int stepIndex, Statistic statistic) { if (stepIndex == TOTAL) { stepIndex = data.length - 1; } switch (statistic) { case TPS: case AVERAGE: return data[stepIndex][statistic.getIndex()] / 100; case ERRORRATIO: return data[stepIndex][Statistic.COUNT.getIndex()] == 0 ? 0 : (long) ((((float) data[stepIndex][Statistic.ERRORS.getIndex()] / (float) data[stepIndex][Statistic.COUNT .getIndex()]) + 0.5) * 100); default: return data[stepIndex][statistic.getIndex()]; } } public Object getValueAt(int rowIndex, int columnIndex) { WsdlTestCase testCase = loadTest.getTestCase(); switch (columnIndex) { case 0: return rowIndex == testCase.getTestStepCount() ? null : ColorPalette.getColor(testCase .getTestStepAt(rowIndex)); case 1: { if (rowIndex == testCase.getTestStepCount()) { return "TestCase:"; } else { return testCase.getTestStepAt(rowIndex).getLabel(); } } case 4: case 7: return new Float((float) data[rowIndex][columnIndex - 2] / 100); case 11: return data[rowIndex][Statistic.COUNT.getIndex()] == 0 ? 0 : (long) (((float) data[rowIndex][Statistic.ERRORS.getIndex()] / (float) data[rowIndex][Statistic.COUNT .getIndex()]) * 100); default: { return data == null || rowIndex >= data.length ? new Long(0) : new Long(data[rowIndex][columnIndex - 2]); } } } public void pushSamples(long[] samples, long[] sizes, long[] sampleCounts, long startTime, long timeTaken, boolean complete) { if (!running || samples.length == 0 || sizes.length == 0) { return; } samplesStack.add(new SamplesHolder(samples, sizes, sampleCounts, startTime, timeTaken, complete)); } public void run() { Thread.currentThread().setName(loadTest.getName() + " LoadTestStatistics"); while (running || !samplesStack.isEmpty()) { try { while (!samplesStack.isEmpty()) { SamplesHolder holder = samplesStack.poll(); if (holder != null) { addSamples(holder); } } Thread.sleep(200); } catch (EmptyStackException e) { } catch (Exception e) { SoapUI.logError(e); } } } private synchronized void addSamples(SamplesHolder holder) { if (adding) { throw new RuntimeException("Already adding!"); } adding = true; int totalIndex = data.length - 1; if (holder.samples.length != totalIndex || holder.sizes.length != totalIndex) { adding = false; throw new RuntimeException("Unexpected number of samples: " + holder.samples.length + ", exptected " + (totalIndex)); } // discard "old" results if (holder.startTime < currentThreadCountStartTime) { adding = false; return; } // first check that this is not a long timePassed = (holder.startTime + holder.timeTaken) - currentThreadCountStartTime; if (resetStatistics) { for (int c = 0; c < data.length; c++) { data[c][CURRENT_CNT_COLUMN] = 0; data[c][AVG_COLUMN] = 0; data[c][SUM_COLUMN] = 0; data[c][TPS_COLUMN] = 0; data[c][BYTES_COLUMN] = 0; } totalAverageSum = 0; resetStatistics = false; } long totalMin = 0; long totalMax = 0; long totalBytes = 0; long totalAvg = 0; long totalSum = 0; long totalLast = 0; long threadCount = loadTest.getThreadCount(); for (int c = 0; c < holder.samples.length; c++) { if (holder.sampleCounts[c] > 0) { // only update when appropriate if (holder.complete != loadTest.getUpdateStatisticsPerTestStep()) { long sampleAvg = holder.samples[c] / holder.sampleCounts[c]; data[c][LAST_COLUMN] = sampleAvg; data[c][CNT_COLUMN] += holder.sampleCounts[c]; data[c][CURRENT_CNT_COLUMN] += holder.sampleCounts[c]; data[c][SUM_COLUMN] += holder.samples[c]; if (sampleAvg > 0 && (sampleAvg < data[c][MIN_COLUMN] || data[c][MIN_COLUMN] == 0)) { data[c][MIN_COLUMN] = sampleAvg; } if (sampleAvg > data[c][MAX_COLUMN]) { data[c][MAX_COLUMN] = sampleAvg; } float average = (float) data[c][SUM_COLUMN] / (float) data[c][CURRENT_CNT_COLUMN]; data[c][AVG_COLUMN] = (long) (average * 100); data[c][BYTES_COLUMN] += holder.sizes[c]; if (timePassed > 0) { if (loadTest.getCalculateTPSOnTimePassed()) { data[c][TPS_COLUMN] = (data[c][CURRENT_CNT_COLUMN] * 100000) / timePassed; data[c][BPS_COLUMN] = (data[c][BYTES_COLUMN] * 1000) / timePassed; } else { data[c][TPS_COLUMN] = (long) (data[c][AVG_COLUMN] > 0 ? (100000F / average) * threadCount : 0); long avgBytes = data[c][CNT_COLUMN] == 0 ? 0 : data[c][BYTES_COLUMN] / data[c][CNT_COLUMN]; data[c][BPS_COLUMN] = (avgBytes * data[c][TPS_COLUMN]) / 100; } } } totalMin += data[c][MIN_COLUMN] * holder.sampleCounts[c]; totalMax += data[c][MAX_COLUMN] * holder.sampleCounts[c]; totalBytes += data[c][BYTES_COLUMN] * holder.sampleCounts[c]; totalAvg += data[c][AVG_COLUMN] * holder.sampleCounts[c]; totalSum += data[c][SUM_COLUMN] * holder.sampleCounts[c]; totalLast += data[c][LAST_COLUMN] * holder.sampleCounts[c]; } else { totalMin += data[c][MIN_COLUMN]; totalMax += data[c][MAX_COLUMN]; totalBytes += data[c][BYTES_COLUMN]; } } if (holder.complete) { data[totalIndex][CNT_COLUMN]++; data[totalIndex][CURRENT_CNT_COLUMN]++; totalAverageSum += totalLast * 100; data[totalIndex][AVG_COLUMN] = (long) ((float) totalAverageSum / (float) data[totalIndex][CURRENT_CNT_COLUMN]); data[totalIndex][BYTES_COLUMN] = totalBytes; if (timePassed > 0) { if (loadTest.getCalculateTPSOnTimePassed()) { data[totalIndex][TPS_COLUMN] = (data[totalIndex][CURRENT_CNT_COLUMN] * 100000) / timePassed; data[totalIndex][BPS_COLUMN] = (data[totalIndex][BYTES_COLUMN] * 1000) / timePassed; } else { data[totalIndex][TPS_COLUMN] = (long) (data[totalIndex][AVG_COLUMN] > 0 ? (10000000F / data[totalIndex][AVG_COLUMN]) * threadCount : 0); long avgBytes = data[totalIndex][CNT_COLUMN] == 0 ? 0 : data[totalIndex][BYTES_COLUMN] / data[totalIndex][CNT_COLUMN]; data[totalIndex][BPS_COLUMN] = (avgBytes * data[totalIndex][TPS_COLUMN]) / 100; } } data[totalIndex][MIN_COLUMN] = totalMin; data[totalIndex][MAX_COLUMN] = totalMax; data[totalIndex][SUM_COLUMN] = totalSum; data[totalIndex][LAST_COLUMN] = totalLast; } if (updateFrequency == 0) { fireTableDataChanged(); } else { changed = true; } adding = false; } private final class Updater implements Runnable { public void run() { Thread.currentThread().setName(loadTest.getName() + " LoadTestStatistics Updater"); // check all these for catching threading issues while (running || changed || !samplesStack.isEmpty()) { if (changed) { fireTableDataChanged(); changed = false; } if (!running && samplesStack.isEmpty()) { break; } try { Thread.sleep(updateFrequency < 1 ? 1000 : updateFrequency); } catch (InterruptedException e) { SoapUI.logError(e); } } } } private void stop() { running = false; } /** * Collect testresult samples * * @author Ole.Matzura */ private class InternalTestRunListener extends LoadTestRunListenerAdapter { public void beforeLoadTest(LoadTestRunner loadTestRunner, LoadTestRunContext context) { samplesStack.clear(); running = true; SoapUI.getThreadPool().submit(updater); SoapUI.getThreadPool().submit(LoadTestStatistics.this); currentThreadCountStartTime = System.currentTimeMillis(); totalAverageSum = 0; } @Override public void afterTestStep(LoadTestRunner loadTestRunner, LoadTestRunContext context, TestCaseRunner testRunner, TestCaseRunContext runContext, TestStepResult testStepResult) { if (loadTest.getUpdateStatisticsPerTestStep()) { TestCase testCase = testRunner.getTestCase(); if (testStepResult == null) { log.warn("Result is null in TestCase [" + testCase.getName() + "]"); return; } long[] samples = new long[testCase.getTestStepCount()]; long[] sizes = new long[samples.length]; long[] sampleCounts = new long[samples.length]; int index = testCase.getIndexOfTestStep(testStepResult.getTestStep()); sampleCounts[index]++; samples[index] += testStepResult.getTimeTaken(); sizes[index] += testStepResult.getSize(); pushSamples(samples, sizes, sampleCounts, testRunner.getStartTime(), testRunner.getTimeTaken(), false); } } public void afterTestCase(LoadTestRunner loadTestRunner, LoadTestRunContext context, TestCaseRunner testRunner, TestCaseRunContext runContext) { if (testRunner.getStatus() == TestRunner.Status.CANCELED && testRunner.getReason().equals(NO_STATS_TESTCASE_CANCEL_REASON)) { return; } List<TestStepResult> results = testRunner.getResults(); TestCase testCase = testRunner.getTestCase(); long[] samples = new long[testCase.getTestStepCount()]; long[] sizes = new long[samples.length]; long[] sampleCounts = new long[samples.length]; for (int c = 0; c < results.size(); c++) { TestStepResult testStepResult = results.get(c); if (testStepResult == null) { log.warn("Result [" + c + "] is null in TestCase [" + testCase.getName() + "]"); continue; } int index = testCase.getIndexOfTestStep(testStepResult.getTestStep()); if (index >= 0) { sampleCounts[index]++; samples[index] += testStepResult.getTimeTaken(); sizes[index] += testStepResult.getSize(); } } pushSamples(samples, sizes, sampleCounts, testRunner.getStartTime(), testRunner.getTimeTaken(), true); } @Override public void afterLoadTest(LoadTestRunner loadTestRunner, LoadTestRunContext context) { stop(); } } public int getStepCount() { return loadTest.getTestCase().getTestStepCount(); } public void reset() { init(); fireTableDataChanged(); } public void release() { reset(); loadTest.removeLoadTestRunListener(testRunListener); loadTest.getTestCase().getTestSuite().removeTestSuiteListener(testSuiteListener); for (TestStep testStep : loadTest.getTestCase().getTestStepList()) { testStep.removePropertyChangeListener(propertyChangeListener); } } private class InternalTestSuiteListener extends TestSuiteListenerAdapter { public void testStepAdded(TestStep testStep, int index) { if (testStep.getTestCase() == loadTest.getTestCase()) { init(); testStep.addPropertyChangeListener(TestStep.NAME_PROPERTY, propertyChangeListener); fireTableDataChanged(); history.reset(); } } public void testStepRemoved(TestStep testStep, int index) { if (testStep.getTestCase() == loadTest.getTestCase()) { init(); testStep.removePropertyChangeListener(propertyChangeListener); fireTableDataChanged(); history.reset(); } } } private class InternalPropertyChangeListener implements PropertyChangeListener { public void propertyChange(PropertyChangeEvent evt) { if (evt.getSource() == loadTest && evt.getPropertyName().equals(WsdlLoadTest.THREADCOUNT_PROPERTY)) { if (loadTest.getResetStatisticsOnThreadCountChange()) { resetStatistics = true; currentThreadCountStartTime = System.currentTimeMillis(); } } else if (evt.getPropertyName().equals(TestStep.NAME_PROPERTY) || evt.getPropertyName().equals(TestStep.DISABLED_PROPERTY)) { if (evt.getSource() instanceof TestStep) { fireTableCellUpdated(loadTest.getTestCase().getIndexOfTestStep((TestStep) evt.getSource()), 1); } } else if (evt.getPropertyName().equals(WsdlLoadTest.HISTORYLIMIT_PROPERTY)) { if (loadTest.getHistoryLimit() == 0) { history.reset(); } } } } public TestStep getTestStepAtRow(int selectedRow) { if (selectedRow < getRowCount() - 1) { return loadTest.getTestCase().getTestStepAt(selectedRow); } else { return null; } } public long getUpdateFrequency() { return updateFrequency; } public void setUpdateFrequency(long updateFrequency) { this.updateFrequency = updateFrequency; } public void addError(int stepIndex) { if (stepIndex != -1) { data[stepIndex][ERR_COLUMN]++; } data[data.length - 1][ERR_COLUMN]++; changed = true; } public synchronized StringList[] getSnapshot() { long[][] clone = data.clone(); StringList[] result = new StringList[getRowCount()]; for (int c = 0; c < clone.length; c++) { StringList values = new StringList(); for (int columnIndex = 2; columnIndex < getColumnCount(); columnIndex++) { switch (columnIndex) { case 4: case 7: values.add(String.valueOf((float) data[c][columnIndex - 2] / 100)); break; default: values.add(String.valueOf(data[c][columnIndex - 2])); } } result[c] = values; } return result; } private final static Map<Integer, Statistic> statisticIndexMap = new HashMap<Integer, Statistic>(); private Updater updater = new Updater(); public enum Statistic { MININMUM(MIN_COLUMN, "min", "the minimum measured teststep time"), MAXIMUM(MAX_COLUMN, "max", "the maximum measured testste time"), AVERAGE(AVG_COLUMN, "avg", "the average measured teststep time"), LAST( LAST_COLUMN, "last", "the last measured teststep time"), COUNT(CNT_COLUMN, "cnt", "the number of teststep samples measured"), TPS(TPS_COLUMN, "tps", "the number of transactions per second for this teststep"), BYTES(BYTES_COLUMN, "bytes", "the total number of bytes returned by this teststep"), BPS(BPS_COLUMN, "bps", "the number of bytes per second returned by this teststep"), ERRORS(ERR_COLUMN, "err", "the total number of assertion errors for this teststep"), SUM(SUM_COLUMN, "sum", "internal sum"), CURRENT_CNT( CURRENT_CNT_COLUMN, "ccnt", "internal cnt"), ERRORRATIO(RATIO_COLUMN, "rat", "the ratio between exections and failures"); private final String description; private final String name; private final int index; Statistic(int index, String name, String description) { this.index = index; this.name = name; this.description = description; statisticIndexMap.put(index, this); } public String getDescription() { return description; } public int getIndex() { return index; } public String getName() { return name; } public static Statistic forIndex(int column) { return statisticIndexMap.get(column); } } /** * Holds all sample values for a testcase run * * @author ole.matzura */ private static final class SamplesHolder { private final long[] samples; private final long[] sizes; private final long[] sampleCounts; private final long startTime; private final long timeTaken; private final boolean complete; public SamplesHolder(long[] samples, long[] sizes, long[] sampleCounts, long startTime, long timeTaken, boolean complete) { this.samples = samples; this.sizes = sizes; this.startTime = startTime; this.timeTaken = timeTaken; this.sampleCounts = sampleCounts; this.complete = complete; } } public synchronized void finish() { // push leftover samples while (!samplesStack.isEmpty()) { SamplesHolder holder = samplesStack.poll(); if (holder != null) { addSamples(holder); } } } }