001/*
002 * ModeShape (http://www.modeshape.org)
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *       http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.modeshape.common.statistic;
017
018import org.modeshape.common.annotation.NotThreadSafe;
019import org.modeshape.common.math.Duration;
020import org.modeshape.common.math.DurationOperations;
021
022/**
023 * Provides a mechanism to measure time in the same way as a physical stopwatch.
024 */
025@NotThreadSafe
026public class Stopwatch implements Comparable<Stopwatch> {
027
028    private long lastStarted;
029    private final SimpleStatistics<Duration> stats;
030    private final DetailedStatistics<Duration> detailedStats;
031    private String description;
032
033    public Stopwatch() {
034        this(true);
035    }
036
037    public Stopwatch( boolean detailedStats ) {
038        this(detailedStats, null);
039    }
040
041    public Stopwatch( boolean detailedStats,
042                      String description ) {
043        this.description = description != null ? description : "";
044        this.detailedStats = detailedStats ? new DetailedStatistics<Duration>(new DurationOperations()) : null;
045        this.stats = detailedStats ? this.detailedStats : new SimpleStatistics<Duration>(new DurationOperations());
046        reset();
047    }
048
049    public String getDescription() {
050        return this.description;
051    }
052
053    /**
054     * Start the stopwatch and begin recording the statistics a new run. This method does nothing if the stopwatch is already
055     * {@link #isRunning() running}
056     * 
057     * @see #isRunning()
058     */
059    public void start() {
060        if (!this.isRunning()) {
061            this.lastStarted = System.nanoTime();
062        }
063    }
064
065    /**
066     * Stop the stopwatch and record the statistics for the latest run. This method does nothing if the stopwatch is not currently
067     * {@link #isRunning() running}
068     * 
069     * @see #isRunning()
070     */
071    public void stop() {
072        if (this.isRunning()) {
073            long duration = System.nanoTime() - this.lastStarted;
074            this.lastStarted = 0l;
075            this.stats.add(new Duration(duration));
076        }
077    }
078
079    /**
080     * Record the statistics for the latest run, but keep the stopwatch going. This method does nothing if the stopwatch is not
081     * currently {@link #isRunning() running}
082     * 
083     * @see #isRunning()
084     */
085    public void lap() {
086        if (this.isRunning()) {
087            long now = System.nanoTime();
088            long duration = now - this.lastStarted;
089            this.lastStarted = now;
090            this.stats.add(new Duration(duration));
091        }
092    }
093
094    /**
095     * Return the number of runs (complete starts and stops) this stopwatch has undergone.
096     * 
097     * @return the number of runs.
098     * @see #isRunning()
099     */
100    public int getCount() {
101        return this.stats.getCount();
102    }
103
104    /**
105     * Return whether this stopwatch is currently running.
106     * 
107     * @return true if running, or false if not
108     */
109    public boolean isRunning() {
110        return this.lastStarted != 0;
111    }
112
113    /**
114     * Get the total duration that this stopwatch has recorded.
115     * 
116     * @return the total duration, or an empty duration if this stopwatch has not been used since creation or being
117     *         {@link #reset() reset}
118     */
119    public Duration getTotalDuration() {
120        return this.stats.getTotal();
121    }
122
123    /**
124     * Get the average duration that this stopwatch has recorded.
125     * 
126     * @return the average duration, or an empty duration if this stopwatch has not been used since creation or being
127     *         {@link #reset() reset}
128     */
129    public Duration getAverageDuration() {
130        return this.stats.getMean();
131    }
132
133    /**
134     * Get the median duration that this stopwatch has recorded.
135     * 
136     * @return the median duration, or an empty duration if this stopwatch has not been used since creation or being
137     *         {@link #reset() reset}
138     */
139    public Duration getMedianDuration() {
140        return this.detailedStats != null ? this.detailedStats.getMedian() : new Duration(0l);
141    }
142
143    /**
144     * Get the minimum duration that this stopwatch has recorded.
145     * 
146     * @return the total minimum, or an empty duration if this stopwatch has not been used since creation or being
147     *         {@link #reset() reset}
148     */
149    public Duration getMinimumDuration() {
150        return this.stats.getMinimum();
151    }
152
153    /**
154     * Get the maximum duration that this stopwatch has recorded.
155     * 
156     * @return the maximum duration, or an empty duration if this stopwatch has not been used since creation or being
157     *         {@link #reset() reset}
158     */
159    public Duration getMaximumDuration() {
160        return this.stats.getMaximum();
161    }
162
163    /**
164     * Return this stopwatch's simple statistics.
165     * 
166     * @return the statistics
167     * @see #getDetailedStatistics()
168     */
169    public SimpleStatistics<Duration> getSimpleStatistics() {
170        return this.stats;
171    }
172
173    /**
174     * Return this stopwatch's detailed statistics, if they are being kept.
175     * 
176     * @return the statistics
177     * @see #getSimpleStatistics()
178     */
179    public DetailedStatistics<Duration> getDetailedStatistics() {
180        return this.detailedStats;
181    }
182
183    /**
184     * Return true if detailed statistics are being kept.
185     * 
186     * @return true if {@link #getDetailedStatistics() detailed statistics} are being kept, or false if only
187     *         {@link #getSimpleStatistics() simple statistics} are being kept.
188     */
189    public boolean isDetailedStatistics() {
190        return this.detailedStats != null;
191    }
192
193    /**
194     * Return the histogram of this stopwatch's individual runs. Two different kinds of histograms can be created. The first kind
195     * is a histogram where all of the buckets are distributed normally and all have the same width. In this case, the 'numSigmas'
196     * should be set to 0.
197     * <p>
198     * <i>Note: if only {@link #getSimpleStatistics() simple statistics} are being kept, the resulting histogram is always empty.
199     * <p>
200     * The second kind of histogram is more useful when most of the data that is clustered near one value. This histogram is
201     * focused around the values that are up to 'numSigmas' above and below the {@link #getMedianDuration() median}, and all
202     * values outside of this range are placed in the first and last bucket.
203     * </p>
204     * 
205     * @param numSigmas the number of standard deviations from the {@link #getMedianDuration() median}, or 0 if the buckets of the
206     *        histogram should be evenly distributed
207     * @return the histogram
208     */
209    public Histogram<Duration> getHistogram( int numSigmas ) {
210        return this.detailedStats != null ? this.detailedStats.getHistogram(numSigmas) : new Histogram<Duration>(
211                                                                                                                 this.stats.getMathOperations());
212    }
213
214    /**
215     * Reset this stopwatch and clear all statistics.
216     */
217    public void reset() {
218        this.lastStarted = 0l;
219        this.stats.reset();
220    }
221
222    @Override
223    public int compareTo( Stopwatch that ) {
224        return this.getTotalDuration().compareTo(that.getTotalDuration());
225    }
226
227    @Override
228    public String toString() {
229        StringBuilder sb = new StringBuilder();
230        sb.append(this.getTotalDuration());
231        if (this.stats.getCount() > 1) {
232            sb.append(" (");
233            sb.append(this.stats.getCount()).append(" samples, avg=");
234            sb.append(this.getAverageDuration());
235            sb.append("; median=");
236            sb.append(this.getMedianDuration());
237            sb.append("; min=");
238            sb.append(this.getMinimumDuration());
239            sb.append("; max=");
240            sb.append(this.getMaximumDuration());
241            sb.append(")");
242        }
243        return sb.toString();
244    }
245
246}