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 * Stores a split as a label and duration.
78 *
79 * @since 3.20.0
80 */
81 public static final class Split extends ImmutablePair<String, Duration> {
82
83 /**
84 * Constructs a Split object with label and duration.
85 *
86 * @param label Label for this split.
87 * @param duration Duration for this split.
88 */
89 public Split(String label, Duration duration) {
90 super(label, duration);
91 }
92
93 /**
94 * Gets the duration of this split.
95 *
96 * @return The duration of this split.
97 */
98 public Duration getDuration() {
99 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 }