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