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 }