001/*
002 * Copyright 2023 the original author or authors.
003 * <p>
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 * <p>
008 * https://www.apache.org/licenses/LICENSE-2.0
009 * <p>
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 de.cuioss.tools.concurrent;
017
018import static de.cuioss.tools.base.Preconditions.checkState;
019import static java.util.Objects.requireNonNull;
020import static java.util.concurrent.TimeUnit.DAYS;
021import static java.util.concurrent.TimeUnit.HOURS;
022import static java.util.concurrent.TimeUnit.MICROSECONDS;
023import static java.util.concurrent.TimeUnit.MILLISECONDS;
024import static java.util.concurrent.TimeUnit.MINUTES;
025import static java.util.concurrent.TimeUnit.NANOSECONDS;
026import static java.util.concurrent.TimeUnit.SECONDS;
027
028import java.io.Serializable;
029import java.time.Duration;
030import java.util.Locale;
031import java.util.concurrent.TimeUnit;
032
033/**
034 * An object that measures elapsed time in nanoseconds. It is useful to measure
035 * elapsed time using this class instead of direct calls to
036 * {@link System#nanoTime} for a few reasons:
037 *
038 * <ul>
039 * <li>An alternate time source can be substituted, for testing or performance
040 * reasons.
041 * <li>As documented by {@code nanoTime}, the value returned has no absolute
042 * meaning, and can only be interpreted as relative to another timestamp
043 * returned by {@code nanoTime} at a different time. {@code StopWatch} is a more
044 * effective abstraction because it exposes only these relative values, not the
045 * absolute ones.
046 * </ul>
047 *
048 * <p>
049 * Basic usage:
050 *
051 * <pre>
052 *
053 * StopWatch stopwatch = StopWatch.createStarted();
054 * doSomething();
055 * stopwatch.stop(); // optional
056 *
057 * Duration duration = stopwatch.elapsed();
058 *
059 * log.info("time: " + stopwatch); // formatted string like "12.3 ms"
060 * </pre>
061 *
062 * <p>
063 * StopWatch methods are not idempotent; it is an error to start or stop a
064 * stopwatch that is already in the desired state.
065 *
066 * <p>
067 * When testing code that uses this class, use {@link #createUnstarted(Ticker)}
068 * or {@link #createStarted(Ticker)} to supply a fake or mock ticker. This
069 * allows you to simulate any valid behavior of the stopwatch.
070 *
071 * <p>
072 * <b>Note:</b> This class is not thread-safe.
073 *
074 * @author com.google.common.base.Stopwatch
075 */
076public final class StopWatch implements Serializable {
077
078    private static final long serialVersionUID = 4764741831457507136L;
079
080    private final Ticker ticker;
081    private boolean isRunning;
082    private long elapsedNanos;
083    private long startTick;
084
085    /**
086     * @return a created (but not started) new stopwatch using
087     *         {@link System#nanoTime} as its time source.
088     *
089     */
090    public static StopWatch createUnstarted() {
091        return new StopWatch();
092    }
093
094    /**
095     * @param ticker specified time source, must not be null
096     * @return a created (but not started) new stopwatch, using the specified time
097     *         source.
098     *
099     */
100    public static StopWatch createUnstarted(Ticker ticker) {
101        return new StopWatch(ticker);
102    }
103
104    /**
105     * @return a created (and started) new stopwatch using {@link System#nanoTime}
106     *         as its time source.
107     *
108     */
109    public static StopWatch createStarted() {
110        return new StopWatch().start();
111    }
112
113    /**
114     * @param ticker specified time source, must not be null
115     * @return a created (and started) new stopwatch, using the specified time
116     *         source.
117     *
118     */
119    public static StopWatch createStarted(Ticker ticker) {
120        return new StopWatch(ticker).start();
121    }
122
123    StopWatch() {
124        ticker = new Ticker();
125    }
126
127    StopWatch(Ticker ticker) {
128        this.ticker = requireNonNull(ticker, "ticker");
129    }
130
131    /**
132     * @return {@code true} if {@link #start()} has been called on this stopwatch,
133     *         and {@link #stop()} has not been called since the last call to
134     *         {@code start()}.
135     */
136    public boolean isRunning() {
137        return isRunning;
138    }
139
140    /**
141     * Starts the stopwatch.
142     *
143     * @return this {@code StopWatch} instance
144     * @throws IllegalStateException if the stopwatch is already running.
145     */
146    public StopWatch start() {
147        checkState(!isRunning, "This stopwatch is already running.");
148        isRunning = true;
149        startTick = ticker.read();
150        return this;
151    }
152
153    /**
154     * Stops the stopwatch. Future reads will return the fixed duration that had
155     * elapsed up to this point.
156     *
157     * @return this {@code StopWatch} instance
158     * @throws IllegalStateException if the stopwatch is already stopped.
159     */
160    public StopWatch stop() {
161        var tick = ticker.read();
162        checkState(isRunning, "This stopwatch is already stopped.");
163        isRunning = false;
164        elapsedNanos += tick - startTick;
165        return this;
166    }
167
168    /**
169     * Sets the elapsed time for this stopwatch to zero, and places it in a stopped
170     * state.
171     *
172     * @return this {@code StopWatch} instance
173     */
174    public StopWatch reset() {
175        elapsedNanos = 0;
176        isRunning = false;
177        return this;
178    }
179
180    private long elapsedNanos() {
181        return isRunning ? ticker.read() - startTick + elapsedNanos : elapsedNanos;
182    }
183
184    /**
185     * @param desiredUnit must not be null
186     * @return the current elapsed time shown on this stopwatch, expressed in the
187     *         desired time unit, with any fraction rounded down.
188     *
189     *         <p>
190     *         <b>Note:</b> the overhead of measurement can be more than a
191     *         microsecond, so it is generally not useful to specify
192     *         {@link TimeUnit#NANOSECONDS} precision here.
193     *
194     *         <p>
195     *         It is generally not a good idea to use an ambiguous, unitless
196     *         {@code long} to represent elapsed time. Therefore, we recommend using
197     *         {@link #elapsed()} instead, which returns a strongly-typed
198     *         {@link Duration} instance.
199     *
200     */
201    public long elapsed(TimeUnit desiredUnit) {
202        return desiredUnit.convert(elapsedNanos(), NANOSECONDS);
203    }
204
205    /**
206     * @return the current elapsed time shown on this stopwatch as a
207     *         {@link Duration}. Unlike {@link #elapsed(TimeUnit)}, this method does
208     *         not lose any precision due to rounding.
209     *
210     */
211    public Duration elapsed() {
212        return Duration.ofNanos(elapsedNanos());
213    }
214
215    /** Returns a string representation of the current elapsed time. */
216    @Override
217    public String toString() {
218        var nanos = elapsedNanos();
219
220        var unit = chooseUnit(nanos);
221        var value = (double) nanos / NANOSECONDS.convert(1, unit);
222
223        return String.format(Locale.ROOT, "%.4g", value) + " " + abbreviate(unit);
224    }
225
226    private static TimeUnit chooseUnit(long nanos) {
227        if (DAYS.convert(nanos, NANOSECONDS) > 0) {
228            return DAYS;
229        }
230        if (HOURS.convert(nanos, NANOSECONDS) > 0) {
231            return HOURS;
232        }
233        if (MINUTES.convert(nanos, NANOSECONDS) > 0) {
234            return MINUTES;
235        }
236        if (SECONDS.convert(nanos, NANOSECONDS) > 0) {
237            return SECONDS;
238        }
239        if (MILLISECONDS.convert(nanos, NANOSECONDS) > 0) {
240            return MILLISECONDS;
241        }
242        if (MICROSECONDS.convert(nanos, NANOSECONDS) > 0) {
243            return MICROSECONDS;
244        }
245        return NANOSECONDS;
246    }
247
248    private static String abbreviate(TimeUnit unit) {
249        return switch (unit) {
250        case NANOSECONDS -> "ns";
251        case MICROSECONDS -> "\u03bcs"; // μs
252        case MILLISECONDS -> "ms";
253        case SECONDS -> "s";
254        case MINUTES -> "min";
255        case HOURS -> "h";
256        case DAYS -> "d";
257        default -> throw new AssertionError();
258        };
259    }
260}