View Javadoc

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