// Copyright (C) 2000 - 2012 Philip Aston // All rights reserved. // // This file is part of The Grinder software distribution. Refer to // the file LICENSE which is part of The Grinder distribution for // licensing details. The Grinder distribution is available on the // Internet at http://grinder.sourceforge.net/ // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS // FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE // COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES // (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) // HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, // STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED // OF THE POSSIBILITY OF SUCH DAMAGE. package net.grinder.console.model; import net.grinder.common.GrinderException; import net.grinder.common.Test; import net.grinder.console.common.ErrorHandler; import net.grinder.console.common.Resources; import net.grinder.statistics.*; import net.grinder.util.ListenerSupport; import java.util.*; /** * Collate test reports into samples and distribute to listeners. * <p> * NHN Customized version * * When notifying listeners of changes to the number of tests we send copies of the new index * arrays. This helps because most listeners are Swing dispatched and so can't guarantee the model * is in a reasonable state when they call back. * </p> * * @author Grinder Developers. * @author JunHo Yoon (modified for nGrinder) * @since 3.0 */ public class SampleModelImplementationEx implements SampleModel { private final ConsoleProperties m_properties; private final StatisticsServices m_statisticsServices; private final Timer m_timer; private final ErrorHandler m_errorHandler; private final String m_stateIgnoringString; private final String m_stateWaitingString; private final String m_stateStoppedString; private final String m_stateCapturingString; private final String m_unknownTestString; /** * The current test set. A TreeSet is used to maintain the test order. Guarded by itself. */ private final Set<Test> m_tests = new TreeSet<Test>(); private final ListenerSupport<Listener> m_listeners = new ListenerSupport<Listener>(); private final StatisticsIndexMap.LongIndex m_periodIndex; private final StatisticExpression m_tpsExpression; private final PeakStatisticExpression m_peakTPSExpression; private final SampleAccumulatorEx m_totalSampleAccumulator; private ModelTestIndex modelTestIndex; /** * A {@link SampleAccumulator} for each test. Guarded by itself. */ private final Map<Test, SampleAccumulator> m_accumulators = Collections .synchronizedMap(new HashMap<Test, SampleAccumulator>()); // Guarded by this. private InternalState m_state; /** * Creates a new <code>SampleModelImplementation</code> instance. * * @param properties The console properties. * @param statisticsServices Statistics services. * @param timer A timer. * @param resources Console resources. * @param errorHandler Error handler. * @exception GrinderException if an error occurs */ public SampleModelImplementationEx(ConsoleProperties properties, StatisticsServices statisticsServices, Timer timer, Resources resources, ErrorHandler errorHandler) throws GrinderException { m_properties = properties; m_statisticsServices = statisticsServices; m_timer = timer; m_errorHandler = errorHandler; m_stateIgnoringString = resources.getString("state.ignoring.label") + ' '; m_stateWaitingString = resources.getString("state.waiting.label"); m_stateStoppedString = resources.getString("state.stopped.label"); m_stateCapturingString = resources.getString("state.capturing.label") + ' '; m_unknownTestString = resources.getString("ignoringUnknownTest.text"); final StatisticsIndexMap indexMap = statisticsServices.getStatisticsIndexMap(); m_periodIndex = indexMap.getLongIndex("period"); final StatisticExpressionFactory statisticExpressionFactory = m_statisticsServices .getStatisticExpressionFactory(); m_tpsExpression = statisticsServices.getTPSExpression(); m_peakTPSExpression = statisticExpressionFactory .createPeak(indexMap.getDoubleIndex("peakTPS"), m_tpsExpression); m_totalSampleAccumulator = new SampleAccumulatorEx(m_peakTPSExpression, m_periodIndex, m_statisticsServices.getStatisticsSetFactory()); setInternalState(new WaitingForTriggerState()); } /** * Get the expression for TPS. * * @return The TPS expression for this model. */ public StatisticExpression getTPSExpression() { return m_tpsExpression; } /** * Get the expression for peak TPS. * * @return The peak TPS expression for this model. */ public StatisticExpression getPeakTPSExpression() { return m_peakTPSExpression; } /** * Register new tests. * * @param tests The new tests. */ public void registerTests(Collection<Test> tests) { // Need to copy collection, might be immutable. final Set<Test> newTests = new HashSet<Test>(tests); final Test[] testArray; synchronized (m_tests) { newTests.removeAll(m_tests); if (newTests.size() == 0) { // No new tests. return; } m_tests.addAll(newTests); // Create an index of m_tests sorted by test number. testArray = m_tests.toArray(new Test[m_tests.size()]); } final SampleAccumulator[] accumulatorArray = new SampleAccumulator[testArray.length]; synchronized (m_accumulators) { for (Test test : newTests) { m_accumulators.put( test, new SampleAccumulator(m_peakTPSExpression, m_periodIndex, m_statisticsServices .getStatisticsSetFactory())); } for (int i = 0; i < accumulatorArray.length; i++) { accumulatorArray[i] = m_accumulators.get(testArray[i]); } } final ModelTestIndex modelTestIndex = new ModelTestIndex(testArray, accumulatorArray); this.modelTestIndex = modelTestIndex; m_listeners.apply(new ListenerSupport.Informer<Listener>() { public void inform(Listener l) { l.newTests(newTests, modelTestIndex); } }); } /** * Get the cumulative statistics for this model. * * @return The cumulative statistics. */ public StatisticsSet getTotalCumulativeStatistics() { return m_totalSampleAccumulator.getCumulativeStatistics(); } /** * Add a new model listener. * * @param listener The listener. */ public void addModelListener(Listener listener) { m_listeners.add(listener); } /** * Add a new sample listener for the specific test. * * @param test The test to add the sample listener for. * @param listener The sample listener. */ public void addSampleListener(Test test, SampleListener listener) { final SampleAccumulator sampleAccumulator = m_accumulators.get(test); if (sampleAccumulator != null) { sampleAccumulator.addSampleListener(listener); } } /** * Add a new total sample listener. * * @param listener The sample listener. */ public void addTotalSampleListener(SampleListener listener) { m_totalSampleAccumulator.addSampleListener(listener); } /** * Reset the model. * * <p> * This doesn't affect our internal state, just the statistics and the listeners. * </p> */ public void reset() { synchronized (m_tests) { m_tests.clear(); } m_accumulators.clear(); m_totalSampleAccumulator.zero(); m_listeners.apply(new ListenerSupport.Informer<Listener>() { public void inform(Listener l) { l.resetTests(); } }); } /** * Start the model. */ public void start() { getInternalState().start(); } /** * Stop the model. */ public void stop() { getInternalState().stop(); } /** * Add a new test report. * * @param testStatisticsMap * The new test statistics. */ public void addTestReport(TestStatisticsMap testStatisticsMap) { getInternalState().newTestReport(testStatisticsMap); } /** * Get the current model state. * * @return The model state. */ public State getState() { return getInternalState().toExternalState(); } /** * Zero the accumulators. */ public void zero() { synchronized (m_accumulators) { for (SampleAccumulator sampleAccumulator : m_accumulators.values()) { sampleAccumulator.zero(); } } m_totalSampleAccumulator.zero(); } private InternalState getInternalState() { synchronized (this) { return m_state; } } private void setInternalState(InternalState newState) { synchronized (this) { m_state = newState; } m_listeners.apply(new ListenerSupport.Informer<Listener>() { public void inform(Listener l) { l.stateChanged(); } }); } private interface InternalState { State toExternalState(); void start(); void stop(); void newTestReport(TestStatisticsMap testStatisticsMap); } private abstract class AbstractInternalState implements InternalState, State { protected final boolean isActiveState() { return getInternalState() == this; } public State toExternalState() { // We don't bother cloning the state, only the description varies. return this; } public void start() { // Valid transition for all states. setInternalState(new WaitingForTriggerState()); } public void stop() { // Valid transition for all states. setInternalState(new StoppedState()); } } private final class WaitingForTriggerState extends AbstractInternalState { public WaitingForTriggerState() { zero(); } public void newTestReport(TestStatisticsMap testStatisticsMap) { if (m_properties.getIgnoreSampleCount() == 0) { setInternalState(new CapturingState()); } else { setInternalState(new TriggeredState()); } // Ensure the the first sample is recorded. getInternalState().newTestReport(testStatisticsMap); } public String getDescription() { return m_stateWaitingString; } public boolean isCapturing() { return false; } public boolean isStopped() { return false; } } private final class StoppedState extends AbstractInternalState { public void newTestReport(TestStatisticsMap testStatisticsMap) { // nothing to do } public String getDescription() { return m_stateStoppedString; } public boolean isStopped() { return true; } public boolean isCapturing() { return false; } } private abstract class AbstractSamplingState extends AbstractInternalState { // Guarded by this. private long mlastTime = 0; private volatile long msampleCount = 1; public void newTestReport(TestStatisticsMap testStatisticsMap) { (testStatisticsMap.new ForEach() { public void next(Test test, StatisticsSet statistics) { final SampleAccumulator sampleAccumulator = m_accumulators.get(test); synchronized (m_accumulators) { if (sampleAccumulator == null) { m_errorHandler.handleInformationMessage(m_unknownTestString + " " + test); } else { sampleAccumulator.addIntervalStatistics(statistics); if (shouldAccumulateSamples()) { sampleAccumulator.addCumulativeStaticstics(statistics); } if (!statistics.isComposite()) { m_totalSampleAccumulator.addIntervalStatistics(statistics); if (shouldAccumulateSamples()) { m_totalSampleAccumulator.addCumulativeStatistics(statistics); } } } } } // CHECKSTYLE:OFF }).iterate(); } protected void schedule() { synchronized (this) { if (mlastTime == 0) { mlastTime = System.currentTimeMillis(); } } m_timer.schedule(new TimerTask() { public void run() { sample(); } }, m_properties.getSampleInterval()); } public final void sample() { if (!isActiveState()) { return; } try { final long period; synchronized (this) { period = System.currentTimeMillis() - mlastTime; } final long sampleInterval = m_properties.getSampleInterval(); SampleAccumulatorEx totalSampleAccumulatorSnapshot; synchronized (m_accumulators) { for (SampleAccumulator sampleAccumulator : m_accumulators.values()) { sampleAccumulator.fireSample(sampleInterval, period); } totalSampleAccumulatorSnapshot = new SampleAccumulatorEx(m_totalSampleAccumulator); m_totalSampleAccumulator.refreshIntervalStatistics(sampleInterval, period); } totalSampleAccumulatorSnapshot.fireSample(sampleInterval, period); ++msampleCount; // I'm ignoring a minor race here: the model could have been // stopped // after the task was started. // We call setInternalState() even if the InternalState hasn't // changed since we've altered the sample count. if (getInternalState() instanceof StoppedState) { return; } setInternalState(nextState()); m_listeners.apply(new ListenerSupport.Informer<Listener>() { public void inform(Listener l) { l.newSample(); } }); } finally { synchronized (this) { if (isActiveState()) { schedule(); } } } } public final long getSampleCount() { return msampleCount; } protected abstract boolean shouldAccumulateSamples(); protected abstract InternalState nextState(); } private final class TriggeredState extends AbstractSamplingState { public TriggeredState() { schedule(); } protected boolean shouldAccumulateSamples() { return false; } protected InternalState nextState() { if (getSampleCount() > m_properties.getIgnoreSampleCount()) { return new CapturingState(); } return this; } public String getDescription() { return m_stateIgnoringString + getSampleCount(); } public boolean isCapturing() { return false; } public boolean isStopped() { return false; } } private final class CapturingState extends AbstractSamplingState { public CapturingState() { zero(); schedule(); } protected boolean shouldAccumulateSamples() { return true; } protected InternalState nextState() { final int collectSampleCount = m_properties.getCollectSampleCount(); if (collectSampleCount != 0 && getSampleCount() > collectSampleCount) { return new StoppedState(); } return this; } public String getDescription() { return m_stateCapturingString + getSampleCount(); } public boolean isCapturing() { return true; } public boolean isStopped() { return false; } } @SuppressWarnings("UnusedDeclaration") public ModelTestIndex getModelTestIndex() { return modelTestIndex; } public StatisticsIndexMap.LongIndex getPeriodIndex() { return m_periodIndex; } public int getSampleInterval() { return m_properties.getSampleInterval(); } }