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