View Javadoc

1   /*
2    *  soapUI, copyright (C) 2004-2009 eviware.com 
3    *
4    *  soapUI is free software; you can redistribute it and/or modify it under the 
5    *  terms of version 2.1 of the GNU Lesser General Public License as published by 
6    *  the Free Software Foundation.
7    *
8    *  soapUI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without 
9    *  even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 
10   *  See the GNU Lesser General Public License for more details at gnu.org.
11   */
12  
13  package com.eviware.soapui.impl.wsdl.loadtest.data;
14  
15  import java.awt.Color;
16  import java.beans.PropertyChangeEvent;
17  import java.beans.PropertyChangeListener;
18  import java.util.EmptyStackException;
19  import java.util.HashMap;
20  import java.util.List;
21  import java.util.Map;
22  import java.util.Stack;
23  
24  import javax.swing.table.AbstractTableModel;
25  
26  import org.apache.log4j.Logger;
27  
28  import com.eviware.soapui.SoapUI;
29  import com.eviware.soapui.impl.wsdl.loadtest.ColorPalette;
30  import com.eviware.soapui.impl.wsdl.loadtest.WsdlLoadTest;
31  import com.eviware.soapui.impl.wsdl.testcase.WsdlTestCase;
32  import com.eviware.soapui.model.support.LoadTestRunListenerAdapter;
33  import com.eviware.soapui.model.support.TestSuiteListenerAdapter;
34  import com.eviware.soapui.model.testsuite.LoadTestRunContext;
35  import com.eviware.soapui.model.testsuite.LoadTestRunner;
36  import com.eviware.soapui.model.testsuite.TestCase;
37  import com.eviware.soapui.model.testsuite.TestCaseRunContext;
38  import com.eviware.soapui.model.testsuite.TestCaseRunner;
39  import com.eviware.soapui.model.testsuite.TestRunner;
40  import com.eviware.soapui.model.testsuite.TestStep;
41  import com.eviware.soapui.model.testsuite.TestStepResult;
42  import com.eviware.soapui.support.types.StringList;
43  
44  /***
45   * Model holding statistics.. should be refactored into interface for different
46   * statistic models
47   * 
48   * @author Ole.Matzura
49   */
50  
51  public final class LoadTestStatistics extends AbstractTableModel implements Runnable
52  {
53  	public final static String NO_STATS_TESTCASE_CANCEL_REASON = "NO_STATS_TESTCASE_CANCEL_REASON";
54  	private final static Logger log = Logger.getLogger( LoadTestStatistics.class );
55  
56  	private final WsdlLoadTest loadTest;
57  	private long[][] data;
58  
59  	private final static int MIN_COLUMN = 0;
60  	private final static int MAX_COLUMN = 1;
61  	private final static int AVG_COLUMN = 2;
62  	private final static int LAST_COLUMN = 3;
63  	private final static int CNT_COLUMN = 4;
64  	private final static int TPS_COLUMN = 5;
65  	private final static int BYTES_COLUMN = 6;
66  	private final static int BPS_COLUMN = 7;
67  	private final static int ERR_COLUMN = 8;
68  	private final static int SUM_COLUMN = 9;
69  	private final static int CURRENT_CNT_COLUMN = 10;
70  	private final static int RATIO_COLUMN = 11;
71  
72  	public static final int TOTAL = -1;
73  
74  	public static final int DEFAULT_SAMPLE_INTERVAL = 250;
75  
76  	private InternalTestRunListener testRunListener;
77  	private InternalTestSuiteListener testSuiteListener;
78  	private InternalPropertyChangeListener propertyChangeListener;
79  
80  	private StatisticsHistory history;
81  
82  	private boolean changed;
83  	private long updateFrequency = DEFAULT_SAMPLE_INTERVAL;
84  	private Stack<SamplesHolder> samplesStack = new Stack<SamplesHolder>();
85  	private long currentThreadCountStartTime;
86  	private long totalAverageSum;
87  	private boolean resetStatistics;
88  	private boolean running;
89  	private boolean adding;
90  
91  	public LoadTestStatistics( WsdlLoadTest loadTest )
92  	{
93  		this.loadTest = loadTest;
94  
95  		testRunListener = new InternalTestRunListener();
96  		testSuiteListener = new InternalTestSuiteListener();
97  		propertyChangeListener = new InternalPropertyChangeListener();
98  
99  		WsdlTestCase testCase = loadTest.getTestCase();
100 		loadTest.addPropertyChangeListener( propertyChangeListener );
101 		loadTest.addLoadTestRunListener( testRunListener );
102 		testCase.getTestSuite().addTestSuiteListener( testSuiteListener );
103 
104 		for( TestStep testStep : testCase.getTestStepList() )
105 		{
106 			testStep.addPropertyChangeListener( propertyChangeListener );
107 		}
108 
109 		history = new StatisticsHistory( this );
110 
111 		init();
112 	}
113 
114 	private void init()
115 	{
116 		data = new long[getRowCount()][11];
117 	}
118 
119 	public StatisticsHistory getHistory()
120 	{
121 		return history;
122 	}
123 
124 	public int getRowCount()
125 	{
126 		return loadTest.getTestCase().getTestStepCount() + 1;
127 	}
128 
129 	public WsdlLoadTest getLoadTest()
130 	{
131 		return loadTest;
132 	}
133 
134 	public int getColumnCount()
135 	{
136 		return 12;
137 	}
138 
139 	public String getColumnName( int columnIndex )
140 	{
141 		switch( columnIndex )
142 		{
143 		case 0 :
144 			return " ";
145 		case 1 :
146 			return "Test Step";
147 		case 2 :
148 			return Statistic.MININMUM.getName();
149 		case 3 :
150 			return Statistic.MAXIMUM.getName();
151 		case 4 :
152 			return Statistic.AVERAGE.getName();
153 		case 5 :
154 			return Statistic.LAST.getName();
155 		case 6 :
156 			return Statistic.COUNT.getName();
157 		case 7 :
158 			return Statistic.TPS.getName();
159 		case 8 :
160 			return Statistic.BYTES.getName();
161 		case 9 :
162 			return Statistic.BPS.getName();
163 		case 10 :
164 			return Statistic.ERRORS.getName();
165 		case 11 : 
166 			return Statistic.ERRORRATIO.getName();
167 		}
168 		return null;
169 	}
170 
171 	public Class<?> getColumnClass( int columnIndex )
172 	{
173 		switch( columnIndex )
174 		{
175 		case 0 :
176 			return Color.class;
177 		case 1 :
178 			return String.class;
179 		case 4 :
180 		case 7 :
181 			return Float.class;
182 		default :
183 			return Long.class;
184 		}
185 	}
186 
187 	public boolean isCellEditable( int rowIndex, int columnIndex )
188 	{
189 		return false;
190 	}
191 
192 	public long getStatistic( int stepIndex, Statistic statistic )
193 	{
194 		if( stepIndex == TOTAL )
195 			stepIndex = data.length - 1;
196 
197 		switch( statistic )
198 		{
199 		case TPS : 
200 		case AVERAGE :
201 			return data[stepIndex][statistic.getIndex()] / 100;
202 		case ERRORRATIO :
203 			return data[stepIndex][Statistic.COUNT.getIndex()] == 0 ? 0 : 
204 				(long)(((float)data[stepIndex][Statistic.ERRORS.getIndex()] / (float)data[stepIndex][Statistic.COUNT.getIndex()])*100);
205 		default :
206 			return data[stepIndex][statistic.getIndex()];
207 		}
208 	}
209 
210 	public Object getValueAt( int rowIndex, int columnIndex )
211 	{
212 		WsdlTestCase testCase = loadTest.getTestCase();
213 
214 		switch( columnIndex )
215 		{
216 		case 0 :
217 			return rowIndex == testCase.getTestStepCount() ? null : ColorPalette.getColor( testCase
218 					.getTestStepAt( rowIndex ) );
219 		case 1 :
220 		{
221 			if( rowIndex == testCase.getTestStepCount() )
222 			{
223 				return "TestCase:";
224 			}
225 			else
226 			{
227 				return testCase.getTestStepAt( rowIndex ).getLabel();
228 			}
229 		}
230 		case 4 :
231 		case 7 :
232 			return new Float( ( float )data[rowIndex][columnIndex - 2] / 100 );
233 		case 11 :
234 			return data[rowIndex][Statistic.COUNT.getIndex()] == 0 ? 0 : 
235 				(long)(((float)data[rowIndex][Statistic.ERRORS.getIndex()] / (float)data[rowIndex][Statistic.COUNT.getIndex()])*100);
236 		default :
237 		{
238 			return data == null || rowIndex >= data.length ? new Long( 0 ) : new Long( data[rowIndex][columnIndex - 2] );
239 		}
240 		}
241 	}
242 
243 	public void pushSamples( long[] samples, long[] sizes, long[] sampleCounts, long startTime, long timeTaken,
244 			boolean complete )
245 	{
246 		if( !running || samples.length == 0 || sizes.length == 0 )
247 			return;
248 
249 		samplesStack.push( new SamplesHolder( samples, sizes, sampleCounts, startTime, timeTaken, complete ) );
250 	}
251 
252 	public void run()
253 	{
254 		while( running || !samplesStack.isEmpty() )
255 		{
256 			try
257 			{
258 				while( !samplesStack.isEmpty() )
259 				{
260 					SamplesHolder holder = samplesStack.pop();
261 					if( holder != null )
262 						addSamples( holder );
263 				}
264 
265 				Thread.sleep( 200 );
266 			}
267 			catch( EmptyStackException e )
268 			{
269 			}
270 			catch( Exception e )
271 			{
272 				SoapUI.logError( e );
273 			}
274 		}
275 	}
276 
277 	private synchronized void addSamples( SamplesHolder holder )
278 	{
279 		if( adding )
280 			throw new RuntimeException( "Already adding!" );
281 
282 		adding = true;
283 
284 		int totalIndex = data.length - 1;
285 		if( holder.samples.length != totalIndex || holder.sizes.length != totalIndex )
286 		{
287 			adding = false;
288 			throw new RuntimeException( "Unexpected number of samples: " + holder.samples.length + ", exptected "
289 					+ ( totalIndex ) );
290 		}
291 
292 		// discard "old" results
293 		if( holder.startTime < currentThreadCountStartTime )
294 		{
295 			adding = false;
296 			return;
297 		}
298 
299 		// first check that this is not a
300 		long timePassed = ( holder.startTime + holder.timeTaken ) - currentThreadCountStartTime;
301 
302 		if( resetStatistics )
303 		{
304 			for( int c = 0; c < data.length; c++ )
305 			{
306 				data[c][CURRENT_CNT_COLUMN] = 0;
307 				data[c][AVG_COLUMN] = 0;
308 				data[c][SUM_COLUMN] = 0;
309 				data[c][TPS_COLUMN] = 0;
310 				data[c][BYTES_COLUMN] = 0;
311 			}
312 
313 			totalAverageSum = 0;
314 			resetStatistics = false;
315 		}
316 
317 		long totalMin = 0;
318 		long totalMax = 0;
319 		long totalBytes = 0;
320 		long totalAvg = 0;
321 		long totalSum = 0;
322 		long totalLast = 0;
323 
324 		long threadCount = loadTest.getThreadCount();
325 
326 		for( int c = 0; c < holder.samples.length; c++ )
327 		{
328 			if( holder.sampleCounts[c] > 0 )
329 			{
330 				// only update when appropriate
331 				if( holder.complete != loadTest.getUpdateStatisticsPerTestStep() )
332 				{
333 					long sampleAvg = holder.samples[c] / holder.sampleCounts[c];
334 
335 					data[c][LAST_COLUMN] = sampleAvg;
336 					data[c][CNT_COLUMN] += holder.sampleCounts[c];
337 					data[c][CURRENT_CNT_COLUMN] += holder.sampleCounts[c];
338 					data[c][SUM_COLUMN] += holder.samples[c];
339 
340 					if( sampleAvg > 0 && ( sampleAvg < data[c][MIN_COLUMN] || data[c][MIN_COLUMN] == 0 ) )
341 						data[c][MIN_COLUMN] = sampleAvg;
342 
343 					if( sampleAvg > data[c][MAX_COLUMN] )
344 						data[c][MAX_COLUMN] = sampleAvg;
345 
346 					float average = ( float )data[c][SUM_COLUMN] / ( float )data[c][CURRENT_CNT_COLUMN];
347 
348 					data[c][AVG_COLUMN] = ( long )( average * 100 );
349 					data[c][BYTES_COLUMN] += holder.sizes[c];
350 
351 					if( timePassed > 0 )
352 					{
353 						if( loadTest.getCalculateTPSOnTimePassed() )
354 						{
355 							data[c][TPS_COLUMN] = ( data[c][CURRENT_CNT_COLUMN] * 100000 ) / timePassed;
356 							data[c][BPS_COLUMN] = ( data[c][BYTES_COLUMN] * 1000 ) / timePassed;
357 						}
358 						else
359 						{
360 							data[c][TPS_COLUMN] = ( long )( data[c][AVG_COLUMN] > 0 ? ( 100000F / average ) * threadCount : 0 );
361 
362 							long avgBytes = data[c][CNT_COLUMN] == 0 ? 0 : data[c][BYTES_COLUMN] / data[c][CNT_COLUMN];
363 							data[c][BPS_COLUMN] = ( avgBytes * data[c][TPS_COLUMN] ) / 100;
364 						}
365 					}
366 				}
367 
368 				totalMin += data[c][MIN_COLUMN] * holder.sampleCounts[c];
369 				totalMax += data[c][MAX_COLUMN] * holder.sampleCounts[c];
370 				totalBytes += data[c][BYTES_COLUMN] * holder.sampleCounts[c];
371 				totalAvg += data[c][AVG_COLUMN] * holder.sampleCounts[c];
372 				totalSum += data[c][SUM_COLUMN] * holder.sampleCounts[c];
373 				totalLast += data[c][LAST_COLUMN] * holder.sampleCounts[c];
374 			}
375 			else
376 			{
377 				totalMin += data[c][MIN_COLUMN];
378 				totalMax += data[c][MAX_COLUMN];
379 				totalBytes += data[c][BYTES_COLUMN];
380 			}
381 		}
382 
383 		if( holder.complete )
384 		{
385 			data[totalIndex][CNT_COLUMN]++ ;
386 			data[totalIndex][CURRENT_CNT_COLUMN]++ ;
387 
388 			totalAverageSum += totalLast * 100;
389 			data[totalIndex][AVG_COLUMN] = ( long )( ( float )totalAverageSum / ( float )data[totalIndex][CURRENT_CNT_COLUMN] );
390 			data[totalIndex][BYTES_COLUMN] = totalBytes;
391 
392 			if( timePassed > 0 )
393 			{
394 				if( loadTest.getCalculateTPSOnTimePassed() )
395 				{
396 					data[totalIndex][TPS_COLUMN] = ( data[totalIndex][CURRENT_CNT_COLUMN] * 100000 ) / timePassed;
397 					data[totalIndex][BPS_COLUMN] = ( data[totalIndex][BYTES_COLUMN] * 1000 ) / timePassed;
398 				}
399 				else
400 				{
401 					data[totalIndex][TPS_COLUMN] = ( long )( data[totalIndex][AVG_COLUMN] > 0 ? ( 10000000F / data[totalIndex][AVG_COLUMN] )
402 							* threadCount
403 							: 0 );
404 
405 					long avgBytes = data[totalIndex][CNT_COLUMN] == 0 ? 0 : data[totalIndex][BYTES_COLUMN]
406 							/ data[totalIndex][CNT_COLUMN];
407 
408 					data[totalIndex][BPS_COLUMN] = ( avgBytes * data[totalIndex][TPS_COLUMN] ) / 100;
409 				}
410 			}
411 
412 			data[totalIndex][MIN_COLUMN] = totalMin;
413 			data[totalIndex][MAX_COLUMN] = totalMax;
414 			data[totalIndex][SUM_COLUMN] = totalSum;
415 			data[totalIndex][LAST_COLUMN] = totalLast;
416 		}
417 
418 		if( updateFrequency == 0 )
419 			fireTableDataChanged();
420 		else
421 			changed = true;
422 
423 		adding = false;
424 	}
425 
426 	private final class Updater implements Runnable
427 	{
428 		public void run()
429 		{
430 			// check all these for catching threading issues
431 			while( running || changed || !samplesStack.isEmpty() )
432 			{
433 				if( changed )
434 				{
435 					fireTableDataChanged();
436 					changed = false;
437 				}
438 
439 				if( !running && samplesStack.isEmpty() )
440 					break;
441 
442 				try
443 				{
444 					Thread.sleep( updateFrequency < 1 ? 1000 : updateFrequency );
445 				}
446 				catch( InterruptedException e )
447 				{
448 					SoapUI.logError( e );
449 				}
450 			}
451 		}
452 	}
453 
454 	private void stop()
455 	{
456 		running = false;
457 	}
458 
459 	/***
460 	 * Collect testresult samples
461 	 * 
462 	 * @author Ole.Matzura
463 	 */
464 
465 	private class InternalTestRunListener extends LoadTestRunListenerAdapter
466 	{
467 		public void beforeLoadTest( LoadTestRunner loadTestRunner, LoadTestRunContext context )
468 		{
469 			running = true;
470 			new Thread( updater, loadTestRunner.getLoadTest().getName() + " LoadTestStatistics Updater" ).start();
471 			new Thread( LoadTestStatistics.this ).start();
472 
473 			currentThreadCountStartTime = System.currentTimeMillis();
474 			totalAverageSum = 0;
475 		}
476 
477 		@Override
478 		public void afterTestStep( LoadTestRunner loadTestRunner, LoadTestRunContext context, TestCaseRunner testRunner,
479 				TestCaseRunContext runContext, TestStepResult testStepResult )
480 		{
481 			if( loadTest.getUpdateStatisticsPerTestStep() )
482 			{
483 				TestCase testCase = testRunner.getTestCase();
484 
485 				if( testStepResult == null )
486 				{
487 					log.warn( "Result is null in TestCase [" + testCase.getName() + "]" );
488 					return;
489 				}
490 
491 				long[] samples = new long[testCase.getTestStepCount()];
492 				long[] sizes = new long[samples.length];
493 				long[] sampleCounts = new long[samples.length];
494 
495 				int index = testCase.getIndexOfTestStep( testStepResult.getTestStep() );
496 				sampleCounts[index]++ ;
497 
498 				samples[index] += testStepResult.getTimeTaken();
499 				sizes[index] += testStepResult.getSize();
500 
501 				pushSamples( samples, sizes, sampleCounts, testRunner.getStartTime(), testRunner.getTimeTaken(), false );
502 			}
503 		}
504 
505 		public void afterTestCase( LoadTestRunner loadTestRunner, LoadTestRunContext context, TestCaseRunner testRunner,
506 				TestCaseRunContext runContext )
507 		{
508 			if(testRunner.getStatus() == TestRunner.Status.CANCELED && testRunner.getReason().equals( NO_STATS_TESTCASE_CANCEL_REASON ))
509 				return;
510 			
511 			List<TestStepResult> results = testRunner.getResults();
512 			TestCase testCase = testRunner.getTestCase();
513 
514 			long[] samples = new long[testCase.getTestStepCount()];
515 			long[] sizes = new long[samples.length];
516 			long[] sampleCounts = new long[samples.length];
517 
518 			for( int c = 0; c < results.size(); c++ )
519 			{
520 				TestStepResult testStepResult = results.get( c );
521 				if( testStepResult == null )
522 				{
523 					log.warn( "Result [" + c + "] is null in TestCase [" + testCase.getName() + "]" );
524 					continue;
525 				}
526 
527 				int index = testCase.getIndexOfTestStep( testStepResult.getTestStep() );
528 				sampleCounts[index]++ ;
529 
530 				samples[index] += testStepResult.getTimeTaken();
531 				sizes[index] += testStepResult.getSize();
532 			}
533 
534 			pushSamples( samples, sizes, sampleCounts, testRunner.getStartTime(), testRunner.getTimeTaken(), true );
535 		}
536 
537 		@Override
538 		public void afterLoadTest( LoadTestRunner loadTestRunner, LoadTestRunContext context )
539 		{
540 			stop();
541 		}
542 	}
543 
544 	public int getStepCount()
545 	{
546 		return loadTest.getTestCase().getTestStepCount();
547 	}
548 
549 	public void reset()
550 	{
551 		init();
552 		fireTableDataChanged();
553 	}
554 
555 	public void release()
556 	{
557 		reset();
558 
559 		loadTest.removeLoadTestRunListener( testRunListener );
560 		loadTest.getTestCase().getTestSuite().removeTestSuiteListener( testSuiteListener );
561 
562 		for( TestStep testStep : loadTest.getTestCase().getTestStepList() )
563 		{
564 			testStep.removePropertyChangeListener( propertyChangeListener );
565 		}
566 	}
567 
568 	private class InternalTestSuiteListener extends TestSuiteListenerAdapter
569 	{
570 		public void testStepAdded( TestStep testStep, int index )
571 		{
572 			if( testStep.getTestCase() == loadTest.getTestCase() )
573 			{
574 				init();
575 				testStep.addPropertyChangeListener( TestStep.NAME_PROPERTY, propertyChangeListener );
576 				fireTableDataChanged();
577 
578 				history.reset();
579 			}
580 		}
581 
582 		public void testStepRemoved( TestStep testStep, int index )
583 		{
584 			if( testStep.getTestCase() == loadTest.getTestCase() )
585 			{
586 				init();
587 				testStep.removePropertyChangeListener( propertyChangeListener );
588 				fireTableDataChanged();
589 
590 				history.reset();
591 			}
592 		}
593 	}
594 
595 	private class InternalPropertyChangeListener implements PropertyChangeListener
596 	{
597 		public void propertyChange( PropertyChangeEvent evt )
598 		{
599 			if( evt.getSource() == loadTest && evt.getPropertyName().equals( WsdlLoadTest.THREADCOUNT_PROPERTY ) )
600 			{
601 				if( loadTest.getResetStatisticsOnThreadCountChange() )
602 				{
603 					resetStatistics = true;
604 					currentThreadCountStartTime = System.currentTimeMillis();
605 				}
606 			}
607 			else if( evt.getPropertyName().equals( TestStep.NAME_PROPERTY )
608 					|| evt.getPropertyName().equals( TestStep.DISABLED_PROPERTY ) )
609 			{
610 				if( evt.getSource() instanceof TestStep )
611 					fireTableCellUpdated( loadTest.getTestCase().getIndexOfTestStep( ( TestStep )evt.getSource() ), 1 );
612 			}
613 			else if( evt.getPropertyName().equals( WsdlLoadTest.HISTORYLIMIT_PROPERTY ) )
614 			{
615 				if( loadTest.getHistoryLimit() == 0 )
616 					history.reset();
617 			}
618 		}
619 	}
620 
621 	public TestStep getTestStepAtRow( int selectedRow )
622 	{
623 		if( selectedRow < getRowCount() - 1 )
624 			return loadTest.getTestCase().getTestStepAt( selectedRow );
625 		else
626 			return null;
627 	}
628 
629 	public long getUpdateFrequency()
630 	{
631 		return updateFrequency;
632 	}
633 
634 	public void setUpdateFrequency( long updateFrequency )
635 	{
636 		this.updateFrequency = updateFrequency;
637 	}
638 
639 	public void addError( int stepIndex )
640 	{
641 		if( stepIndex != -1 )
642 		{
643 			data[stepIndex][ERR_COLUMN]++ ;
644 		}
645 
646 		data[data.length - 1][ERR_COLUMN]++ ;
647 		changed = true;
648 	}
649 
650 	public synchronized StringList[] getSnapshot()
651 	{
652 		long[][] clone = data.clone();
653 
654 		StringList[] result = new StringList[getRowCount()];
655 
656 		for( int c = 0; c < clone.length; c++ )
657 		{
658 			StringList values = new StringList();
659 
660 			for( int columnIndex = 2; columnIndex < getColumnCount(); columnIndex++ )
661 			{
662 				switch( columnIndex )
663 				{
664 				case 4 :
665 				case 7 :
666 					values.add( String.valueOf( ( float )data[c][columnIndex - 2] / 100 ) );
667 					break;
668 				default :
669 					values.add( String.valueOf( data[c][columnIndex - 2] ) );
670 				}
671 			}
672 
673 			result[c] = values;
674 		}
675 
676 		return result;
677 	}
678 
679 	private final static Map<Integer, Statistic> statisticIndexMap = new HashMap<Integer, Statistic>();
680 
681 	private Updater updater = new Updater();
682 
683 	public enum Statistic
684 	{
685 		MININMUM( MIN_COLUMN, "min", "the minimum measured teststep time" ), 
686 		MAXIMUM( MAX_COLUMN, "max", "the maximum measured testste time" ), 
687 		AVERAGE( AVG_COLUMN, "avg", "the average measured teststep time" ), 
688 		LAST(	LAST_COLUMN, "last", "the last measured teststep time" ), 
689 		COUNT( CNT_COLUMN, "cnt", "the number of teststep samples measured" ),
690 		TPS( TPS_COLUMN, "tps",	"the number of transactions per second for this teststep" ), 
691 		BYTES( BYTES_COLUMN, "bytes", "the total number of bytes returned by this teststep" ), 
692 		BPS( BPS_COLUMN, "bps",	"the number of bytes per second returned by this teststep" ), 
693 		ERRORS( ERR_COLUMN, "err", "the total number of assertion errors for this teststep" ), 
694 		SUM( SUM_COLUMN, "sum", "internal sum"),
695 		CURRENT_CNT( CURRENT_CNT_COLUMN, "ccnt", "internal cnt"),
696 		ERRORRATIO( RATIO_COLUMN, "rat", "the ratio between exections and failures");
697 
698 		private final String description;
699 		private final String name;
700 		private final int index;
701 
702 		Statistic( int index, String name, String description )
703 		{
704 			this.index = index;
705 			this.name = name;
706 			this.description = description;
707 
708 			statisticIndexMap.put( index, this );
709 		}
710 
711 		public String getDescription()
712 		{
713 			return description;
714 		}
715 
716 		public int getIndex()
717 		{
718 			return index;
719 		}
720 
721 		public String getName()
722 		{
723 			return name;
724 		}
725 
726 		public static Statistic forIndex( int column )
727 		{
728 			return statisticIndexMap.get( column );
729 		}
730 	}
731 
732 	/***
733 	 * Holds all sample values for a testcase run
734 	 * 
735 	 * @author ole.matzura
736 	 */
737 
738 	private static final class SamplesHolder
739 	{
740 		private final long[] samples;
741 		private final long[] sizes;
742 		private final long[] sampleCounts;
743 
744 		private final long startTime;
745 		private final long timeTaken;
746 		private final boolean complete;
747 
748 		public SamplesHolder( long[] samples, long[] sizes, long[] sampleCounts, long startTime, long timeTaken,
749 				boolean complete )
750 		{
751 			this.samples = samples;
752 			this.sizes = sizes;
753 			this.startTime = startTime;
754 			this.timeTaken = timeTaken;
755 			this.sampleCounts = sampleCounts;
756 			this.complete = complete;
757 		}
758 	}
759 
760 	public synchronized void finish()
761 	{
762 		int sz = samplesStack.size();
763 		while( sz > 0 )
764 		{
765 			log.info( "Waiting for " + sz + " samples.." );
766 			try
767 			{
768 				Thread.sleep( 500 );
769 			}
770 			catch( InterruptedException e )
771 			{
772 				SoapUI.logError( e );
773 			}
774 			sz = samplesStack.size();
775 		}
776 	}
777 }