/*
* soapUI, copyright (C) 2004-2011 eviware.com
*
* soapUI is free software; you can redistribute it and/or modify it under the
* terms of version 2.1 of the GNU Lesser General Public License as published by
* the Free Software Foundation.
*
* soapUI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
* even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Lesser General Public License for more details at gnu.org.
*/
package com.eviware.soapui.impl.wsdl.loadtest.data;
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;
import javax.swing.table.AbstractTableModel;
import org.apache.log4j.Logger;
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;
/**
* 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 );
}
}
}