diff --git a/src/main/java/org/apache/commons/lang3/time/StopWatch.java b/src/main/java/org/apache/commons/lang3/time/StopWatch.java index 50b4954971e..4a8823f4fba 100644 --- a/src/main/java/org/apache/commons/lang3/time/StopWatch.java +++ b/src/main/java/org/apache/commons/lang3/time/StopWatch.java @@ -19,6 +19,9 @@ import java.time.Duration; import java.time.Instant; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.Objects; import java.util.concurrent.TimeUnit; import java.util.function.Supplier; @@ -27,6 +30,7 @@ import org.apache.commons.lang3.function.FailableConsumer; import org.apache.commons.lang3.function.FailableRunnable; import org.apache.commons.lang3.function.FailableSupplier; +import org.apache.commons.lang3.tuple.ImmutablePair; /** * {@link StopWatch} provides a convenient API for timings. @@ -248,6 +252,11 @@ public static StopWatch createStarted() { */ private long stopTimeNanos; + /** + * The split list. + */ + private final List splits = new ArrayList<>(); + /** * Constructs a new instance. */ @@ -326,6 +335,16 @@ public String getMessage() { return message; } + /** + * Gets the split list. + * + * @return the list of splits. + * @since 3.20.0 + */ + public List getSplits() { + return Collections.unmodifiableList(splits); + } + /** * Gets the elapsed time in nanoseconds. * @@ -382,7 +401,7 @@ public long getSplitNanoTime() { if (splitState != SplitState.SPLIT) { throw new IllegalStateException("Stopwatch must be split to get the split time."); } - return stopTimeNanos - startTimeNanos; + return splits.get(splits.size() - 1).getRight().toNanos(); } /** @@ -557,6 +576,7 @@ private long nanosToMillis(final long nanos) { public void reset() { runningState = State.UNSTARTED; splitState = SplitState.UNSPLIT; + splits.clear(); } /** @@ -624,6 +644,28 @@ public void split() { } stopTimeNanos = System.nanoTime(); splitState = SplitState.SPLIT; + splits.add(new Split(String.valueOf(splits.size()), Duration.ofNanos(stopTimeNanos - startTimeNanos))); + } + + /** + * Splits the time with a label. + * + *

+ * 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 + * timing from the original start point. + *

+ * + * @param label A message for string presentation. + * @throws IllegalStateException if the StopWatch is not running. + * @since 3.20.0 + */ + public void split(final String label) { + if (runningState != State.RUNNING) { + throw new IllegalStateException("Stopwatch is not running."); + } + stopTimeNanos = System.nanoTime(); + splitState = SplitState.SPLIT; + splits.add(new Split(label, Duration.ofNanos(stopTimeNanos - startTimeNanos))); } /** @@ -645,6 +687,7 @@ public void start() { startTimeNanos = System.nanoTime(); startInstant = Instant.now(); runningState = State.RUNNING; + splits.clear(); } /** @@ -697,7 +740,7 @@ public void suspend() { } /** - * Gets a summary of the split time that this StopWatch recorded as a string. + * Gets a summary of the last split time that this StopWatch recorded as a string. * *

* The format used is ISO 8601-like, [message ]hours:minutes:seconds.milliseconds. @@ -744,6 +787,53 @@ public void unsplit() { throw new IllegalStateException("Stopwatch has not been split."); } splitState = SplitState.UNSPLIT; + splits.remove(splits.size() - 1); + } + + /** + * Stores a split as a label and duration. + * + * @since 3.20.0 + */ + public static final class Split extends ImmutablePair { + + /** + * Constructs a Split object with label and duration. + * + * @param label Label for this split. + * @param duration Duration for this split. + */ + public Split(String label, Duration duration) { + super(label, duration); + } + + /** + * Gets the label of this split. + * + * @return The label of this split. + */ + public String getLabel() { + return getLeft(); + } + + /** + * Gets the duration of this split. + * + * @return The duration of this split.. + */ + public Duration getDuration() { + return getRight(); + } + + /** + * Converts this instance to a string. + * + * @return this instance to a string. + */ + @Override + public String toString() { + return String.format("Split [%s, %s])", getLabel(), getDuration()); + } } } diff --git a/src/test/java/org/apache/commons/lang3/time/StopWatchTest.java b/src/test/java/org/apache/commons/lang3/time/StopWatchTest.java index e07bf77204c..d365dceac8d 100644 --- a/src/test/java/org/apache/commons/lang3/time/StopWatchTest.java +++ b/src/test/java/org/apache/commons/lang3/time/StopWatchTest.java @@ -26,6 +26,8 @@ import java.io.IOException; import java.time.Duration; import java.time.Instant; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; @@ -69,8 +71,11 @@ private StopWatch createMockStopWatch(final long nanos) { private StopWatch set(final StopWatch watch, final long nanos) { try { final long currentNanos = System.nanoTime(); + final List splits = new ArrayList<>(); + splits.add(new StopWatch.Split(String.valueOf(0), Duration.ofNanos(nanos))); FieldUtils.writeField(watch, "startTimeNanos", currentNanos - nanos, true); FieldUtils.writeField(watch, "stopTimeNanos", currentNanos, true); + FieldUtils.writeField(watch, "splits", splits, true); } catch (final IllegalAccessException e) { return null; } @@ -215,6 +220,23 @@ void testGetSplitDuration() { assertEquals(Duration.ofNanos(123456), watch.getSplitDuration()); } + @Test + void testGetSplits() { + final StopWatch stopWatch = StopWatch.create(); + assertTrue(stopWatch.getSplits().isEmpty()); + stopWatch.start(); + testGetSplits(stopWatch); + testGetSplits(StopWatch.createStarted()); + } + + private void testGetSplits(final StopWatch watch) { + assertTrue(watch.getSplits().isEmpty()); + watch.split(); + assertEquals(1, watch.getSplits().size()); + watch.unsplit(); + assertTrue(watch.getSplits().isEmpty()); + } + @Test void testGetStartInstant() { final long beforeStopWatchMillis = System.currentTimeMillis(); @@ -500,6 +522,36 @@ void testToStringWithMessage() throws InterruptedException { assertEquals(SPLIT_CLOCK_STR_LEN + MESSAGE.length() + 1, splitStr.length(), "Formatted split string not the correct length"); } + @Test + void testSplitsWithStringLabels() { + final StopWatch watch = new StopWatch(); + final String firstLabel = "one"; + final String secondLabel = "two"; + final String thirdLabel = "three"; + watch.start(); + // starting splits + watch.split(firstLabel); + watch.split(secondLabel); + watch.split(thirdLabel); + watch.stop(); + // getting splits + final List splits = watch.getSplits(); + // check size + assertEquals(3, splits.size()); + // check labels + assertEquals(firstLabel, splits.get(0).getLabel()); + assertEquals(secondLabel, splits.get(1).getLabel()); + assertEquals(thirdLabel, splits.get(2).getLabel()); + // check time in nanos + assertTrue(splits.get(0).getDuration().toNanos() > 0); + assertTrue(splits.get(1).getDuration().toNanos() > 0); + assertTrue(splits.get(2).getDuration().toNanos() > 0); + // We can only unsplit once + watch.unsplit(); + assertEquals(2, watch.getSplits().size()); + assertThrows(IllegalStateException.class, watch::unsplit); + } + private int throwIOException() throws IOException { throw new IOException("A"); }