    001/*
    002 * Licensed to the Apache Software Foundation (ASF) under one or more
    003 * contributor license agreements.  See the NOTICE file distributed with
    004 * this work for additional information regarding copyright ownership.
    005 * The ASF licenses this file to You under the Apache License, Version 2.0
    006 * (the "License"); you may not use this file except in compliance with
    007 * the License.  You may obtain a copy of the License at
    008 *
    009 *      https://www.apache.org/licenses/LICENSE-2.0
    010 *
    011 * Unless required by applicable law or agreed to in writing, software
    012 * distributed under the License is distributed on an "AS IS" BASIS,
    013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    014 * See the License for the specific language governing permissions and
    015 * limitations under the License.
    016 */
    017
    018package org.apache.commons.lang3.time;
    019
    020import java.time.Duration;
    021import java.time.Instant;
    022import java.util.ArrayList;
    023import java.util.Collections;
    024import java.util.List;
    025import java.util.Objects;
    026import java.util.concurrent.TimeUnit;
    027import java.util.function.Supplier;
    028
    029import org.apache.commons.lang3.StringUtils;
    030import org.apache.commons.lang3.function.FailableConsumer;
    031import org.apache.commons.lang3.function.FailableRunnable;
    032import org.apache.commons.lang3.function.FailableSupplier;
    033import org.apache.commons.lang3.tuple.ImmutablePair;
    034
    035/**
    036 * {@link StopWatch} provides a convenient API for timings.
    037 *
    038 * <p>
    039 * To start the watch, call {@link #start()} or {@link StopWatch#createStarted()}. At this point you can:
    040 * </p>
    041 * <ul>
    042 * <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
    043 * point, these three options are available again.</li>
    044 * <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
    045 * the total. At this point, these three options are available again.</li>
    046 * <li>{@link #stop()} the watch to complete the timing session.</li>
    047 * </ul>
    048 *
    049 * <p>
    050 * It is intended that the output methods {@link #toString()} and {@link #getTime()} should only be called after stop, split or suspend, however a suitable
    051 * result will be returned at other points.
    052 * </p>
    053 *
    054 * <p>
    055 * 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
    056 * split.
    057 * </p>
    058 *
    059 * <ol>
    060 * <li>{@link #split()}, {@link #suspend()}, or {@link #stop()} cannot be invoked twice</li>
    061 * <li>{@link #unsplit()} may only be called if the watch has been {@link #split()}</li>
    062 * <li>{@link #resume()} may only be called if the watch has been {@link #suspend()}</li>
    063 * <li>{@link #start()} cannot be called twice without calling {@link #reset()}</li>
    064 * </ol>
    065 *
    066 * <p>
    067 * This class is not thread-safe.
    068 * </p>
    069 *
    070 * @see DurationUtils#of(FailableRunnable)
    071 * @see DurationUtils#of(FailableConsumer)
    072 * @since 2.0
    073 */
    074public class StopWatch {
    075
    076    /**
    077     * Enumeration type which indicates the split status of a StopWatch.
    078     */
    079    private enum SplitState {
    080        SPLIT, UNSPLIT
    081    }
    082
    083    /**
    084     * Enumeration type which indicates the status of a StopWatch.
    085     */
    086    private enum State {
    087
    088        RUNNING {
    089            @Override
    090            boolean isStarted() {
    091                return true;
    092            }
    093
    094            @Override
    095            boolean isStopped() {
    096                return false;
    097            }
    098
    099            @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}