View Javadoc

1   // Copyright (C) 2001 - 2012 Philip Aston
2   // Copyright (C) 2012 Marc Holden
3   // All rights reserved.
4   //
5   // This file is part of The Grinder software distribution. Refer to
6   // the file LICENSE which is part of The Grinder distribution for
7   // licensing details. The Grinder distribution is available on the
8   // Internet at http://grinder.sourceforge.net/
9   //
10  // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
11  // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
12  // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
13  // FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
14  // COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
15  // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
16  // (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
17  // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
18  // HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
19  // STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
20  // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
21  // OF THE POSSIBILITY OF SUCH DAMAGE.
22  
23  package net.grinder.console.model;
24  
25  import java.util.Collection;
26  import java.util.Collections;
27  import java.util.HashMap;
28  import java.util.HashSet;
29  import java.util.Map;
30  import java.util.Set;
31  import java.util.Timer;
32  import java.util.TimerTask;
33  import java.util.TreeSet;
34  
35  import net.grinder.common.GrinderException;
36  import net.grinder.common.Test;
37  import net.grinder.console.common.ErrorHandler;
38  import net.grinder.console.common.Resources;
39  import net.grinder.statistics.PeakStatisticExpression;
40  import net.grinder.statistics.StatisticExpression;
41  import net.grinder.statistics.StatisticExpressionFactory;
42  import net.grinder.statistics.StatisticsIndexMap;
43  import net.grinder.statistics.StatisticsServices;
44  import net.grinder.statistics.StatisticsSet;
45  import net.grinder.statistics.TestStatisticsMap;
46  import net.grinder.util.ListenerSupport;
47  
48  
49  /**
50   * Collate test reports into samples and distribute to listeners.
51   *
52   * <p>
53   * When notifying listeners of changes to the number of tests we send copies of
54   * the new index arrays. This helps because most listeners are Swing dispatched
55   * and so can't guarantee the model is in a reasonable state when they call
56   * back.
57   * </p>
58   *
59   * @author Philip Aston
60   */
61  public final class SampleModelImplementation implements SampleModel {
62  
63    private final ConsoleProperties m_properties;
64    private final StatisticsServices m_statisticsServices;
65    private final Timer m_timer;
66    private final ErrorHandler m_errorHandler;
67  
68    private final String m_stateIgnoringString;
69    private final String m_stateWaitingString;
70    private final String m_stateStoppedString;
71    private final String m_stateCapturingString;
72    private final String m_unknownTestString;
73  
74    /**
75     * The current test set. A TreeSet is used to maintain the test
76     * order. Guarded by itself.
77     */
78    private final Set<Test> m_tests = new TreeSet<Test>();
79  
80    private final ListenerSupport<Listener> m_listeners =
81      new ListenerSupport<Listener>();
82  
83    private final StatisticsIndexMap.LongIndex m_periodIndex;
84    private final StatisticExpression m_tpsExpression;
85    private final PeakStatisticExpression m_peakTPSExpression;
86  
87    private final SampleAccumulator m_totalSampleAccumulator;
88  
89    /**
90     * A {@link SampleAccumulator} for each test. Guarded by itself.
91     */
92    private final Map<Test, SampleAccumulator> m_accumulators =
93      Collections.synchronizedMap(new HashMap<Test, SampleAccumulator>());
94  
95    // Guarded by this.
96    private InternalState m_state;
97  
98    /**
99     * Creates a new <code>SampleModelImplementation</code> instance.
100    *
101    * @param properties The console properties.
102    * @param statisticsServices Statistics services.
103    * @param timer A timer.
104    * @param resources Console resources.
105    * @param errorHandler Error handler
106    *
107    * @exception GrinderException if an error occurs
108    */
109   public SampleModelImplementation(final ConsoleProperties properties,
110                                    final StatisticsServices statisticsServices,
111                                    final Timer timer,
112                                    final Resources resources,
113                                    final ErrorHandler errorHandler)
114     throws GrinderException {
115 
116     m_properties = properties;
117     m_statisticsServices = statisticsServices;
118     m_timer = timer;
119     m_errorHandler = errorHandler;
120 
121     m_stateIgnoringString = resources.getString("state.ignoring.label") + ' ';
122     m_stateWaitingString = resources.getString("state.waiting.label");
123     m_stateStoppedString = resources.getString("state.stopped.label");
124     m_stateCapturingString = resources.getString("state.capturing.label") + ' ';
125     m_unknownTestString = resources.getString("ignoringUnknownTest.text");
126 
127     final StatisticsIndexMap indexMap =
128       statisticsServices.getStatisticsIndexMap();
129 
130     m_periodIndex = indexMap.getLongIndex("period");
131 
132     final StatisticExpressionFactory statisticExpressionFactory =
133       m_statisticsServices.getStatisticExpressionFactory();
134 
135     m_tpsExpression = statisticsServices.getTPSExpression();
136 
137     m_peakTPSExpression =
138       statisticExpressionFactory.createPeak(
139         indexMap.getDoubleIndex("peakTPS"), m_tpsExpression);
140 
141     m_totalSampleAccumulator =
142       new SampleAccumulator(m_peakTPSExpression, m_periodIndex,
143                             m_statisticsServices.getStatisticsSetFactory());
144 
145     setInternalState(new WaitingForTriggerState());
146   }
147 
148   /**
149    * {@inheritDoc}
150    */
151   @Override
152   public StatisticExpression getTPSExpression() {
153     return m_tpsExpression;
154   }
155 
156   /**
157    * {@inheritDoc}
158    */
159   @Override
160   public StatisticExpression getPeakTPSExpression() {
161     return m_peakTPSExpression;
162   }
163 
164   /**
165    * {@inheritDoc}
166    */
167   @Override
168   public void registerTests(final Collection<Test> tests) {
169     // Need to copy collection, might be immutable.
170     final Set<Test> newTests = new HashSet<Test>(tests);
171 
172     final Test[] testArray;
173 
174     synchronized (m_tests) {
175       newTests.removeAll(m_tests);
176 
177       if (newTests.size() == 0) {
178         // No new tests.
179         return;
180       }
181 
182       m_tests.addAll(newTests);
183 
184       // Create an index of m_tests sorted by test number.
185       testArray = m_tests.toArray(new Test[m_tests.size()]);
186     }
187 
188     final SampleAccumulator[] accumulatorArray =
189       new SampleAccumulator[testArray.length];
190 
191     synchronized (m_accumulators) {
192       for (final Test test : newTests) {
193         m_accumulators.put(test,
194                            new SampleAccumulator(
195                              m_peakTPSExpression,
196                              m_periodIndex,
197                              m_statisticsServices.getStatisticsSetFactory()));
198       }
199 
200       for (int i = 0; i < accumulatorArray.length; i++) {
201         accumulatorArray[i] = m_accumulators.get(testArray[i]);
202       }
203     }
204 
205     final ModelTestIndex modelTestIndex =
206       new ModelTestIndex(testArray, accumulatorArray);
207 
208     m_listeners.apply(
209       new ListenerSupport.Informer<Listener>() {
210         @Override
211         public void inform(final Listener l) {
212           l.newTests(newTests, modelTestIndex); }
213       });
214   }
215 
216   /**
217    * {@inheritDoc}
218    */
219   @Override
220   public StatisticsSet getTotalCumulativeStatistics() {
221     return m_totalSampleAccumulator.getCumulativeStatistics();
222   }
223 
224   /**
225    * {@inheritDoc}
226    */
227   @Override
228   public StatisticsSet getTotalLatestStatistics() {
229     return m_totalSampleAccumulator.getLastSampleStatistics();
230   }
231 
232   /**
233    * {@inheritDoc}
234    */
235   @Override
236   public void addModelListener(final Listener listener) {
237     m_listeners.add(listener);
238   }
239 
240   /**
241    * {@inheritDoc}
242    */
243   @Override
244   public void addSampleListener(final Test test,
245                                 final SampleListener listener) {
246     final SampleAccumulator sampleAccumulator = m_accumulators.get(test);
247 
248     if (sampleAccumulator != null) {
249       sampleAccumulator.addSampleListener(listener);
250     }
251   }
252 
253   /**
254    * {@inheritDoc}
255    */
256   @Override
257   public void addTotalSampleListener(final SampleListener listener) {
258     m_totalSampleAccumulator.addSampleListener(listener);
259   }
260 
261   /**
262    * {@inheritDoc}
263    */
264   @Override
265   public void reset() {
266 
267     synchronized (m_tests) {
268       m_tests.clear();
269     }
270 
271     m_accumulators.clear();
272     m_totalSampleAccumulator.zero();
273 
274     m_listeners.apply(
275       new ListenerSupport.Informer<Listener>() {
276         @Override
277         public void inform(final Listener l) { l.resetTests(); }
278       });
279   }
280 
281   /**
282    * {@inheritDoc}
283    */
284   @Override
285   public void zeroStatistics() {
286     zero();
287   }
288 
289   /**
290    * {@inheritDoc}
291    */
292   @Override
293   public void start() {
294     getInternalState().start();
295   }
296 
297   /**
298    * {@inheritDoc}
299    */
300   @Override
301   public void stop() {
302     getInternalState().stop();
303   }
304 
305   /**
306    * {@inheritDoc}
307    */
308   @Override
309   public void addTestReport(final TestStatisticsMap testStatisticsMap) {
310     getInternalState().newTestReport(testStatisticsMap);
311   }
312 
313   /**
314    * {@inheritDoc}
315    */
316   @Override
317   public State getState() {
318     return getInternalState().toExternalState();
319   }
320 
321   private void zero() {
322     synchronized (m_accumulators) {
323       for (final SampleAccumulator sampleAccumulator :
324         m_accumulators.values()) {
325         sampleAccumulator.zero();
326       }
327     }
328 
329     m_totalSampleAccumulator.zero();
330   }
331 
332   private InternalState getInternalState() {
333     synchronized (this) {
334       return m_state;
335     }
336   }
337 
338   private void setInternalState(final InternalState newState) {
339     synchronized (this) {
340       m_state = newState;
341     }
342 
343     m_listeners.apply(
344       new ListenerSupport.Informer<Listener>() {
345         @Override
346         public void inform(final Listener l) { l.stateChanged(); }
347       });
348   }
349 
350   private interface InternalState {
351     State toExternalState();
352 
353     void start();
354 
355     void stop();
356 
357     void newTestReport(TestStatisticsMap testStatisticsMap);
358   }
359 
360   private abstract class AbstractInternalState
361     implements InternalState, State {
362 
363     protected final boolean isActiveState() {
364       return getInternalState() == this;
365     }
366 
367     @Override
368     public final State toExternalState() {
369       // We don't bother cloning the state, only the description varies.
370       return this;
371     }
372 
373     @Override
374     public final void start() {
375       // Valid transition for all states.
376       setInternalState(new WaitingForTriggerState());
377     }
378 
379     @Override
380     public final void stop() {
381       // Valid transition for all states.
382       setInternalState(new StoppedState());
383     }
384 
385     @Override
386     public long getSampleCount() {
387       return -1;
388     }
389   }
390 
391   private final class WaitingForTriggerState extends AbstractInternalState {
392     public WaitingForTriggerState() {
393       zero();
394     }
395 
396     @Override
397     public void newTestReport(final TestStatisticsMap testStatisticsMap) {
398       if (m_properties.getIgnoreSampleCount() == 0) {
399         setInternalState(new CapturingState());
400       }
401       else {
402         setInternalState(new TriggeredState());
403       }
404 
405       // Ensure the the first sample is recorded.
406       getInternalState().newTestReport(testStatisticsMap);
407     }
408 
409     @Override
410     public String getDescription() {
411       return m_stateWaitingString;
412     }
413 
414     @Override
415     public Value getValue() {
416       return Value.WaitingForFirstReport;
417     }
418   }
419 
420   private final class StoppedState extends AbstractInternalState {
421     @Override
422     public void newTestReport(final TestStatisticsMap testStatisticsMap) {
423     }
424 
425     @Override
426     public String getDescription() {
427       return m_stateStoppedString;
428     }
429 
430     @Override
431     public Value getValue() {
432       return Value.Stopped;
433     }
434   }
435 
436   private abstract class SamplingState extends AbstractInternalState {
437     // Guarded by this.
438     private long m_lastTime = 0;
439 
440     private volatile long m_sampleCount = 1;
441 
442     @Override
443     public final void newTestReport(final TestStatisticsMap testStatisticsMap) {
444       testStatisticsMap.new ForEach() {
445         @Override
446         public void next(final Test test, final StatisticsSet statistics) {
447           final SampleAccumulator sampleAccumulator = m_accumulators.get(test);
448 
449           if (sampleAccumulator == null) {
450             m_errorHandler.handleInformationMessage(
451               m_unknownTestString + " " + test);
452           }
453           else {
454             sampleAccumulator.addIntervalStatistics(statistics);
455 
456             if (shouldAccumulateSamples()) {
457               sampleAccumulator.addCumulativeStaticstics(statistics);
458             }
459 
460             if (!statistics.isComposite()) {
461               m_totalSampleAccumulator.addIntervalStatistics(statistics);
462 
463               if (shouldAccumulateSamples()) {
464                 m_totalSampleAccumulator.addCumulativeStaticstics(statistics);
465               }
466             }
467           }
468         }
469       }
470       .iterate();
471     }
472 
473     protected final void schedule() {
474       synchronized (this) {
475         if (m_lastTime == 0) {
476           m_lastTime = System.currentTimeMillis();
477         }
478       }
479 
480       m_timer.schedule(
481         new TimerTask() {
482           @Override
483           public void run() { sample(); }
484         },
485         m_properties.getSampleInterval());
486     }
487 
488     public final void sample() {
489       if (!isActiveState()) {
490         return;
491       }
492 
493       try {
494         final long period;
495 
496         synchronized (this) {
497           period = System.currentTimeMillis() - m_lastTime;
498         }
499 
500         final long sampleInterval = m_properties.getSampleInterval();
501 
502         synchronized (m_accumulators) {
503           for (final SampleAccumulator sampleAccumulator :
504             m_accumulators.values()) {
505             sampleAccumulator.fireSample(sampleInterval, period);
506           }
507         }
508 
509         m_totalSampleAccumulator.fireSample(sampleInterval, period);
510 
511         ++m_sampleCount;
512 
513         // I'm ignoring a minor race here: the model could have been stopped
514         // after the task was started.
515         // We call setInternalState() even if the InternalState hasn't
516         // changed since we've altered the sample count.
517         setInternalState(nextState());
518 
519         m_listeners.apply(
520           new ListenerSupport.Informer<Listener>() {
521             @Override
522             public void inform(final Listener l) { l.newSample(); }
523           });
524       }
525       finally {
526         synchronized (this) {
527           if (isActiveState()) {
528             schedule();
529           }
530         }
531       }
532     }
533 
534     @Override
535     public final long getSampleCount() {
536       return m_sampleCount;
537     }
538 
539     protected abstract boolean shouldAccumulateSamples();
540 
541     protected abstract InternalState nextState();
542   }
543 
544   private final class TriggeredState extends SamplingState {
545     public TriggeredState() {
546       schedule();
547     }
548 
549     @Override
550     protected boolean shouldAccumulateSamples() {
551       return false;
552     }
553 
554     @Override
555     protected InternalState nextState() {
556       if (getSampleCount() > m_properties.getIgnoreSampleCount()) {
557         return new CapturingState();
558       }
559 
560       return this;
561     }
562 
563     @Override
564     public String getDescription() {
565       return m_stateIgnoringString + getSampleCount();
566     }
567 
568     @Override
569     public Value getValue() {
570       return Value.IgnoringInitialSamples;
571     }
572   }
573 
574   private final class CapturingState extends SamplingState {
575     public CapturingState() {
576       zero();
577       schedule();
578     }
579 
580     @Override
581     protected boolean shouldAccumulateSamples() {
582       return true;
583     }
584 
585     @Override
586     protected InternalState nextState() {
587       final int collectSampleCount = m_properties.getCollectSampleCount();
588 
589       if (collectSampleCount != 0 && getSampleCount() > collectSampleCount) {
590         return new StoppedState();
591       }
592 
593       return this;
594     }
595 
596     @Override
597     public String getDescription() {
598       return m_stateCapturingString + getSampleCount();
599     }
600 
601     @Override
602     public Value getValue() {
603       return Value.Recording;
604     }
605   }
606 }