001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      https://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017
018package org.apache.commons.lang3.time;
019
020import java.time.Duration;
021import java.time.Instant;
022import java.util.ArrayList;
023import java.util.Collections;
024import java.util.List;
025import java.util.Objects;
026import java.util.concurrent.TimeUnit;
027import java.util.function.Supplier;
028
029import org.apache.commons.lang3.StringUtils;
030import org.apache.commons.lang3.function.FailableConsumer;
031import org.apache.commons.lang3.function.FailableRunnable;
032import org.apache.commons.lang3.function.FailableSupplier;
033import org.apache.commons.lang3.tuple.ImmutablePair;
034
035/**
036 * {@link StopWatch} provides a convenient API for timings.
037 *
038 * <p>
039 * To start the watch, call {@link #start()} or {@link StopWatch#createStarted()}. At this point you can:
040 * </p>
041 * <ul>
042 * <li>{@link #split()} the watch to get the time whilst the watch continues in the background. {@link #unsplit()} will remove the effect of the split. At this
043 * point, these three options are available again.</li>
044 * <li>{@link #suspend()} the watch to pause it. {@link #resume()} allows the watch to continue. Any time between the suspend and resume will not be counted in
045 * the total. At this point, these three options are available again.</li>
046 * <li>{@link #stop()} the watch to complete the timing session.</li>
047 * </ul>
048 *
049 * <p>
050 * It is intended that the output methods {@link #toString()} and {@link #getTime()} should only be called after stop, split or suspend, however a suitable
051 * result will be returned at other points.
052 * </p>
053 *
054 * <p>
055 * NOTE: As from v2.1, the methods protect against inappropriate calls. Thus you cannot now call stop before start, resume before suspend or unsplit before
056 * split.
057 * </p>
058 *
059 * <ol>
060 * <li>{@link #split()}, {@link #suspend()}, or {@link #stop()} cannot be invoked twice</li>
061 * <li>{@link #unsplit()} may only be called if the watch has been {@link #split()}</li>
062 * <li>{@link #resume()} may only be called if the watch has been {@link #suspend()}</li>
063 * <li>{@link #start()} cannot be called twice without calling {@link #reset()}</li>
064 * </ol>
065 *
066 * <p>
067 * This class is not thread-safe.
068 * </p>
069 *
070 * @see DurationUtils#of(FailableRunnable)
071 * @see DurationUtils#of(FailableConsumer)
072 * @since 2.0
073 */
074public class StopWatch {
075
076    /**
077     * Stores a split as a label and duration.
078     *
079     * @since 3.20.0
080     */
081    public static final class Split extends ImmutablePair<String, Duration> {
082
083        /**
084         * Constructs a Split object with label and duration.
085         *
086         * @param label Label for this split.
087         * @param duration Duration for this split.
088         */
089        public Split(String label, Duration duration) {
090            super(label, duration);
091        }
092
093        /**
094         * Gets the duration of this split.
095         *
096         * @return The duration of this split.
097         */
098        public Duration getDuration() {
099            return getRight();
100        }
101
102        /**
103         * Gets the label of this split.
104         *
105         * @return The label of this split.
106         */
107        public String getLabel() {
108            return getLeft();
109        }
110
111        /**
112         * Converts this instance to a string.
113         *
114         * @return this instance to a string.
115         */
116        @Override
117        public String toString() {
118            return String.format("Split [%s, %s])", getLabel(), getDuration());
119        }
120    }
121
122    /**
123     * Enumeration type which indicates the split status of a StopWatch.
124     */
125    private enum SplitState {
126        SPLIT, UNSPLIT
127    }
128
129    /**
130     * Enumeration type which indicates the status of a StopWatch.
131     */
132    private enum State {
133
134        RUNNING {
135            @Override
136            boolean isStarted() {
137                return true;
138            }
139
140            @Override
141            boolean isStopped() {
142                return false;
143            }
144
145            @Override
146            boolean isSuspended() {
147                return false;
148            }
149        },
150
151        STOPPED {
152            @Override
153            boolean isStarted() {
154                return false;
155            }
156
157            @Override
158            boolean isStopped() {
159                return true;
160            }
161
162            @Override
163            boolean isSuspended() {
164                return false;
165            }
166        },
167
168        SUSPENDED {
169            @Override
170            boolean isStarted() {
171                return true;
172            }
173
174            @Override
175            boolean isStopped() {
176                return false;
177            }
178
179            @Override
180            boolean isSuspended() {
181                return true;
182            }
183        },
184
185        UNSTARTED {
186            @Override
187            boolean isStarted() {
188                return false;
189            }
190
191            @Override
192            boolean isStopped() {
193                return true;
194            }
195
196            @Override
197            boolean isSuspended() {
198                return false;
199            }
200        };
201
202        /**
203         * Tests whether this StopWatch is started. A suspended StopWatch is also started.
204         *
205         * @return boolean If this StopWatch is started.
206         */
207        abstract boolean isStarted();
208
209        /**
210         * Tests whether this StopWatch is stopped. A StopWatch which is not yet started and explicitly stopped is considered stopped.
211         *
212         * @return boolean If this StopWatch is stopped.
213         */
214        abstract boolean isStopped();
215
216        /**
217         * Tests whether this StopWatch is suspended.
218         *
219         * @return boolean If this StopWatch is suspended.
220         */
221        abstract boolean isSuspended();
222    }
223
224    private static final long NANO_2_MILLIS = 1_000_000L;
225
226    /**
227     * Creates a StopWatch.
228     *
229     * @return StopWatch a StopWatch.
230     * @since 3.10
231     */
232    public static StopWatch create() {
233        return new StopWatch();
234    }
235
236    /**
237     * Creates and starts a StopWatch.
238     *
239     * @return StopWatch a started StopWatch.
240     * @since 3.5
241     */
242    public static StopWatch createStarted() {
243        final StopWatch sw = new StopWatch();
244        sw.start();
245        return sw;
246    }
247
248    /**
249     * A message for string presentation.
250     *
251     * @since 3.10
252     */
253    private final String message;
254
255    /**
256     * The current running state of this StopWatch.
257     */
258    private State runningState = State.UNSTARTED;
259
260    /**
261     * Whether this StopWatch has a split time recorded.
262     */
263    private SplitState splitState = SplitState.UNSPLIT;
264
265    /**
266     * The start time in nanoseconds.
267     *
268     * This field can be removed once we move off of Java 8.
269     */
270    private long startTimeNanos;
271
272    /**
273     * The start Instant.
274     * <p>
275     * nanoTime is only for elapsed time so we need to also store the currentTimeMillis to maintain the old getStartTime API.
276     * </p>
277     * <p>
278     * On Java 8, Instant has millisecond precision, later versions use nanoseconds.
279     * </p>
280     */
281    private Instant startInstant;
282
283    /**
284     * The end Instant.
285     * <p>
286     * nanoTime is only for elapsed time so we need to also store the currentTimeMillis to maintain the old getStartTime API.
287     * </p>
288     * <p>
289     * On Java 8, Instant has millisecond precision, later versions use nanoseconds.
290     * </p>
291     */
292    private Instant stopInstant;
293
294    /**
295     * The stop time in nanoseconds.
296     *
297     * This field can be removed once we move off of Java 8.
298     */
299    private long stopTimeNanos;
300
301    /**
302     * The split list.
303     */
304    private final List<Split> splits = new ArrayList<>();
305
306    /**
307     * Constructs a new instance.
308     */
309    public StopWatch() {
310        this(null);
311    }
312
313    /**
314     * Constructs a new instance.
315     *
316     * @param message A message for string presentation.
317     * @since 3.10
318     */
319    public StopWatch(final String message) {
320        this.message = message;
321    }
322
323    /**
324     * Formats the split time with {@link DurationFormatUtils#formatDurationHMS}.
325     *
326     * @return the split time formatted by {@link DurationFormatUtils#formatDurationHMS}.
327     * @since 3.10
328     */
329    public String formatSplitTime() {
330        return DurationFormatUtils.formatDurationHMS(getSplitDuration().toMillis());
331    }
332
333    /**
334     * Formats the time formatted with {@link DurationFormatUtils#formatDurationHMS}.
335     *
336     * @return the time formatted by {@link DurationFormatUtils#formatDurationHMS}.
337     * @since 3.10
338     */
339    public String formatTime() {
340        return DurationFormatUtils.formatDurationHMS(getTime());
341    }
342
343    /**
344     * Delegates to {@link Supplier#get()} while recording the duration of the call.
345     *
346     * @param <T>      the type of results supplied by this supplier.
347     * @param supplier The supplier to {@link Supplier#get()}.
348     * @return a result from the given Supplier.
349     * @since 3.18.0
350     */
351    public <T> T get(final Supplier<T> supplier) {
352        startResume();
353        try {
354            return supplier.get();
355        } finally {
356            suspend();
357        }
358    }
359
360    /**
361     * Gets the Duration on this StopWatch.
362     *
363     * <p>
364     * This is either the Duration between the start and the moment this method is called, or the Duration between start and stop.
365     * </p>
366     *
367     * @return the Duration.
368     * @since 3.16.0
369     */
370    public Duration getDuration() {
371        return Duration.ofNanos(getNanoTime());
372    }
373
374    /**
375     * Gets the message for string presentation.
376     *
377     * @return the message for string presentation.
378     * @since 3.10
379     */
380    public String getMessage() {
381        return message;
382    }
383
384    /**
385     * Gets the <em>elapsed</em> time in nanoseconds.
386     *
387     * <p>
388     * This is either the time between the start and the moment this method is called, or the amount of time between start and stop.
389     * </p>
390     *
391     * @return the <em>elapsed</em> time in nanoseconds.
392     * @see System#nanoTime()
393     * @since 3.0
394     */
395    public long getNanoTime() {
396        switch (runningState) {
397        case STOPPED:
398        case SUSPENDED:
399            return stopTimeNanos - startTimeNanos;
400        case UNSTARTED:
401            return 0;
402        case RUNNING:
403            return System.nanoTime() - startTimeNanos;
404        default:
405            break;
406        }
407        throw new IllegalStateException("Illegal running state has occurred.");
408    }
409
410    /**
411     * Gets the split Duration on this StopWatch.
412     *
413     * <p>
414     * This is the Duration between start and latest split.
415     * </p>
416     *
417     * @return the split Duration.
418     * @throws IllegalStateException if this StopWatch has not yet been split.
419     * @since 3.16.0
420     */
421    public Duration getSplitDuration() {
422        return Duration.ofNanos(getSplitNanoTime());
423    }
424
425    /**
426     * Gets the split time in nanoseconds.
427     *
428     * <p>
429     * This is the time between start and latest split.
430     * </p>
431     *
432     * @return the split time in nanoseconds.
433     * @throws IllegalStateException if this StopWatch has not yet been split.
434     * @since 3.0
435     */
436    public long getSplitNanoTime() {
437        if (splitState != SplitState.SPLIT) {
438            throw new IllegalStateException("Stopwatch must be split to get the split time.");
439        }
440        return splits.get(splits.size() - 1).getRight().toNanos();
441    }
442
443    /**
444     * Gets the split list.
445     *
446     * @return the list of splits.
447     * @since 3.20.0
448     */
449    public List<Split> getSplits() {
450        return Collections.unmodifiableList(splits);
451    }
452
453    /**
454     * Gets the split time on this StopWatch.
455     *
456     * <p>
457     * This is the time between start and latest split.
458     * </p>
459     *
460     * @return the split time in milliseconds.
461     * @throws IllegalStateException if this StopWatch has not yet been split.
462     * @since 2.1
463     * @deprecated Use {@link #getSplitDuration()}.
464     */
465    @Deprecated
466    public long getSplitTime() {
467        return nanosToMillis(getSplitNanoTime());
468    }
469
470    /**
471     * Gets the Instant this StopWatch was started, between the current time and midnight, January 1, 1970 UTC.
472     *
473     * @return the Instant this StopWatch was started, between the current time and midnight, January 1, 1970 UTC.
474     * @throws IllegalStateException if this StopWatch has not been started.
475     * @since 3.16.0
476     */
477    public Instant getStartInstant() {
478        if (runningState == State.UNSTARTED) {
479            throw new IllegalStateException("Stopwatch has not been started");
480        }
481        return startInstant;
482    }
483
484    /**
485     * Gets the time this StopWatch was started in milliseconds, between the current time and midnight, January 1, 1970 UTC.
486     *
487     * @return the time this StopWatch was started in milliseconds, between the current time and midnight, January 1, 1970 UTC.
488     * @throws IllegalStateException if this StopWatch has not been started.
489     * @since 2.4
490     * @deprecated Use {@link #getStartInstant()}.
491     */
492    @Deprecated
493    public long getStartTime() {
494        return getStartInstant().toEpochMilli();
495    }
496
497    /**
498     * Gets the Instant this StopWatch was stopped, between the current time and midnight, January 1, 1970 UTC.
499     *
500     * @return the Instant this StopWatch was stopped in milliseconds, between the current time and midnight, January 1, 1970 UTC.
501     * @throws IllegalStateException if this StopWatch has not been started.
502     * @since 3.16.0
503     */
504    public Instant getStopInstant() {
505        if (runningState == State.UNSTARTED) {
506            throw new IllegalStateException("Stopwatch has not been started");
507        }
508        return stopInstant;
509    }
510
511    /**
512     * Gets the time this StopWatch was stopped in milliseconds, between the current time and midnight, January 1, 1970 UTC.
513     *
514     * @return the time this StopWatch was stopped in milliseconds, between the current time and midnight, January 1, 1970 UTC.
515     * @throws IllegalStateException if this StopWatch has not been started.
516     * @since 3.12.0
517     * @deprecated Use {@link #getStopInstant()}.
518     */
519    @Deprecated
520    public long getStopTime() {
521        // stopTimeNanos stores System.nanoTime() for elapsed time
522        final Instant stop = getStopInstant();
523        return stop != null ? stop.toEpochMilli() : 0;
524    }
525
526    /**
527     * Delegates to {@link FailableSupplier#get()} while recording the duration of the call.
528     *
529     * @param <T>      the type of results supplied by this supplier.
530     * @param <E>      The kind of thrown exception or error.
531     * @param supplier The supplier to {@link Supplier#get()}.
532     * @return a result from the given Supplier.
533     * @throws Throwable if the supplier fails.
534     * @since 3.18.0
535     */
536    public <T, E extends Throwable> T getT(final FailableSupplier<T, E> supplier) throws Throwable {
537        startResume();
538        try {
539            return supplier.get();
540        } finally {
541            suspend();
542        }
543    }
544
545    /**
546     * Gets the time on this StopWatch.
547     *
548     * <p>
549     * This is either the time between the start and the moment this method is called, or the amount of time between start and stop.
550     * </p>
551     *
552     * @return the time in milliseconds.
553     * @see #getDuration()
554     */
555    public long getTime() {
556        return nanosToMillis(getNanoTime());
557    }
558
559    /**
560     * Gets the time in the specified TimeUnit.
561     *
562     * <p>
563     * This is either the time between the start and the moment this method is called, or the amount of time between start and stop. The resulting time will be
564     * expressed in the desired TimeUnit with any remainder rounded down. For example, if the specified unit is {@code TimeUnit.HOURS} and this StopWatch time
565     * is 59 minutes, then the result returned will be {@code 0}.
566     * </p>
567     *
568     * @param timeUnit the unit of time, not null.
569     * @return the time in the specified TimeUnit, rounded down.
570     * @since 3.5
571     */
572    public long getTime(final TimeUnit timeUnit) {
573        return timeUnit.convert(getNanoTime(), TimeUnit.NANOSECONDS);
574    }
575
576    /**
577     * Tests whether this StopWatch is started. A suspended StopWatch is also started watch.
578     *
579     * @return boolean If this StopWatch is started.
580     * @since 3.2
581     */
582    public boolean isStarted() {
583        return runningState.isStarted();
584    }
585
586    /**
587     * Tests whether StopWatch is stopped. this StopWatch which's not yet started and explicitly stopped StopWatch is considered as stopped.
588     *
589     * @return boolean If this StopWatch is stopped.
590     * @since 3.2
591     */
592    public boolean isStopped() {
593        return runningState.isStopped();
594    }
595
596    /**
597     * Tests whether this StopWatch is suspended.
598     *
599     * @return boolean If this StopWatch is suspended.
600     * @since 3.2
601     */
602    public boolean isSuspended() {
603        return runningState.isSuspended();
604    }
605
606    /**
607     * Converts nanoseconds to milliseconds.
608     *
609     * @param nanos nanoseconds to convert.
610     * @return milliseconds conversion result.
611     */
612    private long nanosToMillis(final long nanos) {
613        return nanos / NANO_2_MILLIS;
614    }
615
616    /**
617     * Resets this StopWatch. Stops it if need be.
618     *
619     * <p>
620     * This method clears the internal values to allow the object to be reused.
621     * </p>
622     */
623    public void reset() {
624        runningState = State.UNSTARTED;
625        splitState = SplitState.UNSPLIT;
626        splits.clear();
627    }
628
629    /**
630     * Resumes this StopWatch after a suspend.
631     *
632     * <p>
633     * This method resumes the watch after it was suspended. The watch will not include time between the suspend and resume calls in the total time.
634     * </p>
635     *
636     * @throws IllegalStateException if this StopWatch has not been suspended.
637     */
638    public void resume() {
639        if (runningState != State.SUSPENDED) {
640            throw new IllegalStateException("Stopwatch must be suspended to resume.");
641        }
642        startTimeNanos += System.nanoTime() - stopTimeNanos;
643        runningState = State.RUNNING;
644    }
645
646    /**
647     * Delegates to {@link Runnable#run()} while recording the duration of the call.
648     *
649     * @param runnable The runnable to {@link Runnable#run()}.
650     * @since 3.18.0
651     */
652    public void run(final Runnable runnable) {
653        startResume();
654        try {
655            runnable.run();
656        } finally {
657            suspend();
658        }
659    }
660
661    /**
662     * Delegates to {@link FailableRunnable#run()} while recording the duration of the call.
663     *
664     * @param <E>      The kind of {@link Throwable}.
665     * @param runnable The runnable to {@link FailableRunnable#run()}.
666     * @throws Throwable Thrown by {@link FailableRunnable#run()}.
667     * @since 3.18.0
668     */
669    public <E extends Throwable> void runT(final FailableRunnable<E> runnable) throws Throwable {
670        startResume();
671        try {
672            runnable.run();
673        } finally {
674            suspend();
675        }
676    }
677
678    /**
679     * Splits the time.
680     *
681     * <p>
682     * This method sets the stop time of the watch to allow a time to be extracted. The start time is unaffected, enabling {@link #unsplit()} to continue the
683     * timing from the original start point.
684     * </p>
685     *
686     * @throws IllegalStateException if this StopWatch is not running.
687     */
688    public void split() {
689        if (runningState != State.RUNNING) {
690            throw new IllegalStateException("Stopwatch is not running.");
691        }
692        stopSet();
693        splitState = SplitState.SPLIT;
694        splits.add(new Split(String.valueOf(splits.size()), Duration.ofNanos(stopTimeNanos - startTimeNanos)));
695    }
696
697    /**
698     * Splits the time with a label.
699     *
700     * <p>
701     * This method sets the stop time of the watch to allow a time to be extracted. The start time is unaffected, enabling {@link #unsplit()} to continue the
702     * timing from the original start point.
703     * </p>
704     *
705     * @param label A message for string presentation.
706     * @throws IllegalStateException if the StopWatch is not running.
707     * @since 3.20.0
708     */
709    public void split(final String label) {
710        if (runningState != State.RUNNING) {
711            throw new IllegalStateException("Stopwatch is not running.");
712        }
713        stopSet();
714        splitState = SplitState.SPLIT;
715        splits.add(new Split(label, Duration.ofNanos(stopTimeNanos - startTimeNanos)));
716    }
717
718    /**
719     * Starts this StopWatch.
720     *
721     * <p>
722     * This method starts a new timing session, clearing any previous values.
723     * </p>
724     *
725     * @throws IllegalStateException if this StopWatch is already running.
726     */
727    public void start() {
728        if (runningState == State.STOPPED) {
729            throw new IllegalStateException("Stopwatch must be reset before being restarted.");
730        }
731        if (runningState != State.UNSTARTED) {
732            throw new IllegalStateException("Stopwatch already started.");
733        }
734        startTimeNanos = System.nanoTime();
735        startInstant = Instant.now();
736        runningState = State.RUNNING;
737        splits.clear();
738    }
739
740    /**
741     * Starts or resumes this StopWatch.
742     */
743    private void startResume() {
744        if (isStopped()) {
745            start();
746        } else if (isSuspended()) {
747            resume();
748        }
749    }
750
751    /**
752     * Stops this StopWatch.
753     *
754     * <p>
755     * This method ends a new timing session, allowing the time to be retrieved.
756     * </p>
757     *
758     * @throws IllegalStateException if this StopWatch is not running.
759     */
760    public void stop() {
761        if (runningState != State.RUNNING && runningState != State.SUSPENDED) {
762            throw new IllegalStateException("Stopwatch is not running.");
763        }
764        if (runningState == State.RUNNING) {
765            stopSet();
766        }
767        runningState = State.STOPPED;
768    }
769
770    private void stopSet() {
771        stopTimeNanos = System.nanoTime();
772        stopInstant = Instant.now();
773    }
774
775    /**
776     * Suspends this StopWatch for later resumption.
777     *
778     * <p>
779     * This method suspends the watch until it is resumed. The watch will not include time between the suspend and resume calls in the total time.
780     * </p>
781     *
782     * @throws IllegalStateException if this StopWatch is not currently running.
783     */
784    public void suspend() {
785        if (runningState != State.RUNNING) {
786            throw new IllegalStateException("Stopwatch must be running to suspend.");
787        }
788        stopSet();
789        runningState = State.SUSPENDED;
790    }
791
792    /**
793     * Gets a summary of the last split time that this StopWatch recorded as a string.
794     *
795     * <p>
796     * The format used is ISO 8601-like, [<em>message</em> ]<em>hours</em>:<em>minutes</em>:<em>seconds</em>.<em>milliseconds</em>.
797     * </p>
798     *
799     * @return the split time as a String.
800     * @since 2.1
801     * @since 3.10 Returns the prefix {@code "message "} if the message is set.
802     */
803    public String toSplitString() {
804        final String msgStr = Objects.toString(message, StringUtils.EMPTY);
805        final String formattedTime = formatSplitTime();
806        return msgStr.isEmpty() ? formattedTime : msgStr + StringUtils.SPACE + formattedTime;
807    }
808
809    /**
810     * Gets a summary of the time that this StopWatch recorded as a string.
811     *
812     * <p>
813     * The format used is ISO 8601-like, [<em>message</em> ]<em>hours</em>:<em>minutes</em>:<em>seconds</em>.<em>milliseconds</em>.
814     * </p>
815     *
816     * @return the time as a String.
817     * @since 3.10 Returns the prefix {@code "message "} if the message is set.
818     */
819    @Override
820    public String toString() {
821        final String msgStr = Objects.toString(message, StringUtils.EMPTY);
822        final String formattedTime = formatTime();
823        return msgStr.isEmpty() ? formattedTime : msgStr + StringUtils.SPACE + formattedTime;
824    }
825
826    /**
827     * Removes the split.
828     *
829     * <p>
830     * This method clears the stop time. The start time is unaffected, enabling timing from the original start point to continue.
831     * </p>
832     *
833     * @throws IllegalStateException if this StopWatch has not been split.
834     */
835    public void unsplit() {
836        if (splitState != SplitState.SPLIT) {
837            throw new IllegalStateException("Stopwatch has not been split.");
838        }
839        splitState = SplitState.UNSPLIT;
840        splits.remove(splits.size() - 1);
841    }
842}