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    *      http://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.Objects;
23  import java.util.concurrent.TimeUnit;
24  
25  import org.apache.commons.lang3.StringUtils;
26  import org.apache.commons.lang3.function.FailableConsumer;
27  import org.apache.commons.lang3.function.FailableRunnable;
28  
29  /**
30   * {@link StopWatch} provides a convenient API for timings.
31   *
32   * <p>
33   * To start the watch, call {@link #start()} or {@link StopWatch#createStarted()}. At this point you can:
34   * </p>
35   * <ul>
36   * <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
37   * point, these three options are available again.</li>
38   * <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
39   * the total. At this point, these three options are available again.</li>
40   * <li>{@link #stop()} the watch to complete the timing session.</li>
41   * </ul>
42   *
43   * <p>
44   * It is intended that the output methods {@link #toString()} and {@link #getTime()} should only be called after stop, split or suspend, however a suitable
45   * result will be returned at other points.
46   * </p>
47   *
48   * <p>
49   * 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
50   * split.
51   * </p>
52   *
53   * <ol>
54   * <li>{@link #split()}, {@link #suspend()}, or {@link #stop()} cannot be invoked twice</li>
55   * <li>{@link #unsplit()} may only be called if the watch has been {@link #split()}</li>
56   * <li>{@link #resume()} may only be called if the watch has been {@link #suspend()}</li>
57   * <li>{@link #start()} cannot be called twice without calling {@link #reset()}</li>
58   * </ol>
59   *
60   * <p>
61   * This class is not thread-safe
62   * </p>
63   *
64   * @see DurationUtils#of(FailableRunnable)
65   * @see DurationUtils#of(FailableConsumer)
66   *
67   * @since 2.0
68   */
69  public class StopWatch {
70  
71      /**
72       * Enumeration type which indicates the split status of a StopWatch.
73       */
74      private enum SplitState {
75          SPLIT, UNSPLIT
76      }
77  
78      /**
79       * Enumeration type which indicates the status of a StopWatch.
80       */
81      private enum State {
82  
83          RUNNING {
84              @Override
85              boolean isStarted() {
86                  return true;
87              }
88  
89              @Override
90              boolean isStopped() {
91                  return false;
92              }
93  
94              @Override
95              boolean isSuspended() {
96                  return false;
97              }
98          },
99  
100         STOPPED {
101             @Override
102             boolean isStarted() {
103                 return false;
104             }
105 
106             @Override
107             boolean isStopped() {
108                 return true;
109             }
110 
111             @Override
112             boolean isSuspended() {
113                 return false;
114             }
115         },
116 
117         SUSPENDED {
118             @Override
119             boolean isStarted() {
120                 return true;
121             }
122 
123             @Override
124             boolean isStopped() {
125                 return false;
126             }
127 
128             @Override
129             boolean isSuspended() {
130                 return true;
131             }
132         },
133 
134         UNSTARTED {
135             @Override
136             boolean isStarted() {
137                 return false;
138             }
139 
140             @Override
141             boolean isStopped() {
142                 return true;
143             }
144 
145             @Override
146             boolean isSuspended() {
147                 return false;
148             }
149         };
150 
151         /**
152          * Tests whether the StopWatch is started. A suspended StopWatch is also started.
153          *
154          * @return boolean If the StopWatch is started.
155          */
156         abstract boolean isStarted();
157 
158         /**
159          * Tests whether the StopWatch is stopped. A StopWatch which is not yet started and explicitly stopped is considered stopped.
160          *
161          * @return boolean If the StopWatch is stopped.
162          */
163         abstract boolean isStopped();
164 
165         /**
166          * Tests whether the StopWatch is suspended.
167          *
168          * @return boolean If the StopWatch is suspended.
169          */
170         abstract boolean isSuspended();
171     }
172 
173     private static final long NANO_2_MILLIS = 1000000L;
174 
175     /**
176      * Creates a StopWatch.
177      *
178      * @return StopWatch a StopWatch.
179      *
180      * @since 3.10
181      */
182     public static StopWatch create() {
183         return new StopWatch();
184     }
185 
186     /**
187      * Creates and starts a StopWatch.
188      *
189      * @return StopWatch a started StopWatch.
190      *
191      * @since 3.5
192      */
193     public static StopWatch createStarted() {
194         final StopWatch sw = new StopWatch();
195         sw.start();
196         return sw;
197     }
198 
199     /**
200      * A message for string presentation.
201      *
202      * @since 3.10
203      */
204     private final String message;
205 
206     /**
207      * The current running state of the StopWatch.
208      */
209     private State runningState = State.UNSTARTED;
210 
211     /**
212      * Whether the StopWatch has a split time recorded.
213      */
214     private SplitState splitState = SplitState.UNSPLIT;
215 
216     /**
217      * The start time in nanoseconds.
218      *
219      * This field can be removed once we move off of Java 8.
220      */
221     private long startTimeNanos;
222 
223     /**
224      * The start Instant.
225      * <p>
226      * nanoTime is only for elapsed time so we need to also store the currentTimeMillis to maintain the old getStartTime API.
227      * </p>
228      * <p>
229      * On Java 8, Instant has millisecond precision, only later versions use nanoseconds.
230      * </p>
231      */
232     private Instant startInstant;
233 
234     /**
235      * The end Instant.
236      * <p>
237      * nanoTime is only for elapsed time so we need to also store the currentTimeMillis to maintain the old getStartTime API.
238      * </p>
239      * <p>
240      * On Java 8, Instant has millisecond precision, only later versions use nanoseconds.
241      * </p>
242      */
243     private Instant stopInstant;
244 
245     /**
246      * The stop time in nanoseconds.
247      *
248      * This field can be removed once we move off of Java 8.
249      */
250     private long stopTimeNanos;
251 
252     /**
253      * Constructs a new instance.
254      */
255     public StopWatch() {
256         this(null);
257     }
258 
259     /**
260      * Constructs a new instance.
261      *
262      * @param message A message for string presentation.
263      * @since 3.10
264      */
265     public StopWatch(final String message) {
266         this.message = message;
267     }
268 
269     /**
270      * Formats the split time with {@link DurationFormatUtils#formatDurationHMS}.
271      *
272      * @return the split time formatted by {@link DurationFormatUtils#formatDurationHMS}.
273      * @since 3.10
274      */
275     public String formatSplitTime() {
276         return DurationFormatUtils.formatDurationHMS(getSplitDuration().toMillis());
277     }
278 
279     /**
280      * Formats the time formatted with {@link DurationFormatUtils#formatDurationHMS}.
281      *
282      * @return the time formatted by {@link DurationFormatUtils#formatDurationHMS}.
283      * @since 3.10
284      */
285     public String formatTime() {
286         return DurationFormatUtils.formatDurationHMS(getTime());
287     }
288 
289     /**
290      * Gets the Duration on the StopWatch.
291      *
292      * <p>
293      * This is either the Duration between the start and the moment this method is called, or the Duration between start and stop.
294      * </p>
295      *
296      * @return the Duration.
297      * @since 3.16.0
298      */
299     public Duration getDuration() {
300         return Duration.ofNanos(getNanoTime());
301     }
302 
303     /**
304      * Gets the message for string presentation.
305      *
306      * @return the message for string presentation.
307      * @since 3.10
308      */
309     public String getMessage() {
310         return message;
311     }
312 
313     /**
314      * Gets the <em>elapsed</em> time in nanoseconds.
315      *
316      * <p>
317      * This is either the time between the start and the moment this method is called, or the amount of time between start and stop.
318      * </p>
319      *
320      * @return the <em>elapsed</em> time in nanoseconds.
321      * @see System#nanoTime()
322      * @since 3.0
323      */
324     public long getNanoTime() {
325         if (runningState == State.STOPPED || runningState == State.SUSPENDED) {
326             return stopTimeNanos - startTimeNanos;
327         }
328         if (runningState == State.UNSTARTED) {
329             return 0;
330         }
331         if (runningState == State.RUNNING) {
332             return System.nanoTime() - startTimeNanos;
333         }
334         throw new IllegalStateException("Illegal running state has occurred.");
335     }
336 
337     /**
338      * Gets the split Duration on the StopWatch.
339      *
340      * <p>
341      * This is the Duration between start and latest split.
342      * </p>
343      *
344      * @return the split Duration
345      *
346      * @throws IllegalStateException if the StopWatch has not yet been split.
347      * @since 3.16.0
348      */
349     public Duration getSplitDuration() {
350         return Duration.ofNanos(getSplitNanoTime());
351     }
352 
353     /**
354      * Gets the split time in nanoseconds.
355      *
356      * <p>
357      * This is the time between start and latest split.
358      * </p>
359      *
360      * @return the split time in nanoseconds
361      *
362      * @throws IllegalStateException if the StopWatch has not yet been split.
363      * @since 3.0
364      */
365     public long getSplitNanoTime() {
366         if (splitState != SplitState.SPLIT) {
367             throw new IllegalStateException("Stopwatch must be split to get the split time.");
368         }
369         return stopTimeNanos - startTimeNanos;
370     }
371 
372     /**
373      * Gets the split time on the StopWatch.
374      *
375      * <p>
376      * This is the time between start and latest split.
377      * </p>
378      *
379      * @return the split time in milliseconds
380      *
381      * @throws IllegalStateException if the StopWatch has not yet been split.
382      * @since 2.1
383      * @deprecated Use {@link #getSplitDuration()}.
384      */
385     @Deprecated
386     public long getSplitTime() {
387         return nanosToMillis(getSplitNanoTime());
388     }
389 
390     /**
391      * Gets the Instant this StopWatch was started, between the current time and midnight, January 1, 1970 UTC.
392      *
393      * @return the Instant this StopWatch was started, between the current time and midnight, January 1, 1970 UTC.
394      * @throws IllegalStateException if this StopWatch has not been started
395      * @since 3.16.0
396      */
397     public Instant getStartInstant() {
398         return Instant.ofEpochMilli(getStartTime());
399     }
400 
401     /**
402      * Gets the time this StopWatch was started in milliseconds, between the current time and midnight, January 1, 1970 UTC.
403      *
404      * @return the time this StopWatch was started in milliseconds, between the current time and midnight, January 1, 1970 UTC.
405      * @throws IllegalStateException if this StopWatch has not been started
406      * @since 2.4
407      * @deprecated Use {@link #getStartInstant()}.
408      */
409     @Deprecated
410     public long getStartTime() {
411         if (runningState == State.UNSTARTED) {
412             throw new IllegalStateException("Stopwatch has not been started");
413         }
414         // stopTimeNanos stores System.nanoTime() for elapsed time
415         return startInstant.toEpochMilli();
416     }
417 
418     /**
419      * Gets the Instant this StopWatch was stopped, between the current time and midnight, January 1, 1970 UTC.
420      *
421      * @return the Instant this StopWatch was stopped in milliseconds, between the current time and midnight, January 1, 1970 UTC.
422      * @throws IllegalStateException if this StopWatch has not been started
423      * @since 3.16.0
424      */
425     public Instant getStopInstant() {
426         return Instant.ofEpochMilli(getStopTime());
427     }
428 
429     /**
430      * Gets the time this StopWatch was stopped in milliseconds, between the current time and midnight, January 1, 1970 UTC.
431      *
432      * @return the time this StopWatch was stopped in milliseconds, between the current time and midnight, January 1, 1970 UTC.
433      * @throws IllegalStateException if this StopWatch has not been started
434      * @since 3.12.0
435      * @deprecated Use {@link #getStopInstant()}.
436      */
437     @Deprecated
438     public long getStopTime() {
439         if (runningState == State.UNSTARTED) {
440             throw new IllegalStateException("Stopwatch has not been started");
441         }
442         // stopTimeNanos stores System.nanoTime() for elapsed time
443         return stopInstant.toEpochMilli();
444     }
445 
446     /**
447      * Gets the time on the StopWatch.
448      *
449      * <p>
450      * This is either the time between the start and the moment this method is called, or the amount of time between start and stop.
451      * </p>
452      *
453      * @return the time in milliseconds
454      * @deprecated Use {@link #getDuration()}.
455      */
456     @Deprecated
457     public long getTime() {
458         return nanosToMillis(getNanoTime());
459     }
460 
461     /**
462      * Gets the time in the specified TimeUnit.
463      *
464      * <p>
465      * 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
466      * expressed in the desired TimeUnit with any remainder rounded down. For example, if the specified unit is {@code TimeUnit.HOURS} and the StopWatch time is
467      * 59 minutes, then the result returned will be {@code 0}.
468      * </p>
469      *
470      * @param timeUnit the unit of time, not null
471      * @return the time in the specified TimeUnit, rounded down
472      * @since 3.5
473      */
474     public long getTime(final TimeUnit timeUnit) {
475         return timeUnit.convert(getNanoTime(), TimeUnit.NANOSECONDS);
476     }
477 
478     /**
479      * Tests whether the StopWatch is started. A suspended StopWatch is also started watch.
480      *
481      * @return boolean If the StopWatch is started.
482      * @since 3.2
483      */
484     public boolean isStarted() {
485         return runningState.isStarted();
486     }
487 
488     /**
489      * Tests whether StopWatch is stopped. The StopWatch which's not yet started and explicitly stopped StopWatch is considered as stopped.
490      *
491      * @return boolean If the StopWatch is stopped.
492      * @since 3.2
493      */
494     public boolean isStopped() {
495         return runningState.isStopped();
496     }
497 
498     /**
499      * Tests whether the StopWatch is suspended.
500      *
501      * @return boolean If the StopWatch is suspended.
502      * @since 3.2
503      */
504     public boolean isSuspended() {
505         return runningState.isSuspended();
506     }
507 
508     /**
509      * Converts nanoseconds to milliseconds.
510      *
511      * @param nanos nanoseconds to convert.
512      * @return milliseconds conversion result.
513      */
514     private long nanosToMillis(final long nanos) {
515         return nanos / NANO_2_MILLIS;
516     }
517 
518     /**
519      * Resets the StopWatch. Stops it if need be.
520      *
521      * <p>
522      * This method clears the internal values to allow the object to be reused.
523      * </p>
524      */
525     public void reset() {
526         runningState = State.UNSTARTED;
527         splitState = SplitState.UNSPLIT;
528     }
529 
530     /**
531      * Resumes the StopWatch after a suspend.
532      *
533      * <p>
534      * 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.
535      * </p>
536      *
537      * @throws IllegalStateException if the StopWatch has not been suspended.
538      */
539     public void resume() {
540         if (runningState != State.SUSPENDED) {
541             throw new IllegalStateException("Stopwatch must be suspended to resume. ");
542         }
543         startTimeNanos += System.nanoTime() - stopTimeNanos;
544         runningState = State.RUNNING;
545     }
546 
547     /**
548      * Splits the time.
549      *
550      * <p>
551      * 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
552      * timing from the original start point.
553      * </p>
554      *
555      * @throws IllegalStateException if the StopWatch is not running.
556      */
557     public void split() {
558         if (runningState != State.RUNNING) {
559             throw new IllegalStateException("Stopwatch is not running. ");
560         }
561         stopTimeNanos = System.nanoTime();
562         splitState = SplitState.SPLIT;
563     }
564 
565     /**
566      * Starts the StopWatch.
567      *
568      * <p>
569      * This method starts a new timing session, clearing any previous values.
570      * </p>
571      *
572      * @throws IllegalStateException if the StopWatch is already running.
573      */
574     public void start() {
575         if (runningState == State.STOPPED) {
576             throw new IllegalStateException("Stopwatch must be reset before being restarted. ");
577         }
578         if (runningState != State.UNSTARTED) {
579             throw new IllegalStateException("Stopwatch already started. ");
580         }
581         startTimeNanos = System.nanoTime();
582         startInstant = Instant.now();
583         runningState = State.RUNNING;
584     }
585 
586     /**
587      * Stops the StopWatch.
588      *
589      * <p>
590      * This method ends a new timing session, allowing the time to be retrieved.
591      * </p>
592      *
593      * @throws IllegalStateException if the StopWatch is not running.
594      */
595     public void stop() {
596         if (runningState != State.RUNNING && runningState != State.SUSPENDED) {
597             throw new IllegalStateException("Stopwatch is not running. ");
598         }
599         if (runningState == State.RUNNING) {
600             stopTimeNanos = System.nanoTime();
601             stopInstant = Instant.now();
602         }
603         runningState = State.STOPPED;
604     }
605 
606     /**
607      * Suspends the StopWatch for later resumption.
608      *
609      * <p>
610      * 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.
611      * </p>
612      *
613      * @throws IllegalStateException if the StopWatch is not currently running.
614      */
615     public void suspend() {
616         if (runningState != State.RUNNING) {
617             throw new IllegalStateException("Stopwatch must be running to suspend. ");
618         }
619         stopTimeNanos = System.nanoTime();
620         stopInstant = Instant.now();
621         runningState = State.SUSPENDED;
622     }
623 
624     /**
625      * Gets a summary of the split time that the StopWatch recorded as a string.
626      *
627      * <p>
628      * The format used is ISO 8601-like, [<i>message</i> ]<i>hours</i>:<i>minutes</i>:<i>seconds</i>.<i>milliseconds</i>.
629      * </p>
630      *
631      * @return the split time as a String
632      * @since 2.1
633      * @since 3.10 Returns the prefix {@code "message "} if the message is set.
634      */
635     public String toSplitString() {
636         final String msgStr = Objects.toString(message, StringUtils.EMPTY);
637         final String formattedTime = formatSplitTime();
638         return msgStr.isEmpty() ? formattedTime : msgStr + StringUtils.SPACE + formattedTime;
639     }
640 
641     /**
642      * Gets a summary of the time that the StopWatch recorded as a string.
643      *
644      * <p>
645      * The format used is ISO 8601-like, [<i>message</i> ]<i>hours</i>:<i>minutes</i>:<i>seconds</i>.<i>milliseconds</i>.
646      * </p>
647      *
648      * @return the time as a String
649      * @since 3.10 Returns the prefix {@code "message "} if the message is set.
650      */
651     @Override
652     public String toString() {
653         final String msgStr = Objects.toString(message, StringUtils.EMPTY);
654         final String formattedTime = formatTime();
655         return msgStr.isEmpty() ? formattedTime : msgStr + StringUtils.SPACE + formattedTime;
656     }
657 
658     /**
659      * Removes a split.
660      *
661      * <p>
662      * This method clears the stop time. The start time is unaffected, enabling timing from the original start point to continue.
663      * </p>
664      *
665      * @throws IllegalStateException if the StopWatch has not been split.
666      */
667     public void unsplit() {
668         if (splitState != SplitState.SPLIT) {
669             throw new IllegalStateException("Stopwatch has not been split. ");
670         }
671         splitState = SplitState.UNSPLIT;
672     }
673 
674 }