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