Stopwatch.java

// Generated by delombok at Mon Nov 18 07:27:48 UTC 2024
package de.larssh.utils.time;

import static de.larssh.utils.function.ThrowingConsumer.throwing;
import static java.util.Collections.synchronizedList;
import static java.util.Collections.unmodifiableList;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.TemporalAccessor;
import java.time.temporal.TemporalField;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.stream.Stream;
import de.larssh.utils.Nullables;
import edu.umd.cs.findbugs.annotations.Nullable;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;

/**
 * Implementation of a synchronized stopwatch starting at {@link Instant#now()}.
 * {@link #sinceStart()} can be used to retrieve the duration since the
 * stopwatch started.
 *
 * <p>
 * {@link #checkpoint(String)} adds a new checkpoint. It might be used for split
 * or lap times. {@link #sinceLast()} can be used to retrieve the duration since
 * the last checkpoint was created.
 */
public class Stopwatch {
	/**
	 * List of checkpoints
	 */
	private final List<Checkpoint> checkpoints = synchronizedList(new LinkedList<>());
	/**
	 * Instant at the stopwatches start
	 */
	private final Instant startInstant = Instant.now();
	/**
	 * Object used for locking
	 */
	private final Object lock = new Object();

	/**
	 * Adds a new checkpoint referenced by {@code name}.
	 *
	 * <p>
	 * {@code name} does not need to be unique. Multiple checkpoints with the same
	 * name might exist.
	 *
	 * @param name name to reference the checkpoint
	 * @return the new checkpoint
	 */
	public Checkpoint checkpoint(final String name) {
		synchronized (lock) {
			final Checkpoint checkpoint = new Checkpoint(this, name, Instant.now(), getLastCheckpoint());
			checkpoints.add(checkpoint);
			return checkpoint;
		}
	}

	/**
	 * List of checkpoints
	 *
	 * @return the list of checkpoints
	 */
	public List<Checkpoint> getCheckpoints() {
		return unmodifiableList(checkpoints);
	}

	/**
	 * The last created {@link Checkpoint} or empty if no checkpoint was created
	 *
	 * @return the last created checkpoint
	 */
	public Optional<Checkpoint> getLastCheckpoint() {
		return checkpoints.isEmpty() ? Optional.empty() : Optional.of(checkpoints.get(checkpoints.size() - 1));
	}

	/**
	 * Instant of the last created {@link Checkpoint} or the stopwatches starting
	 * time if no checkpoint was created
	 *
	 * @return the last created instant or the stopwatches starting time
	 */
	public Instant getLastInstant() {
		return checkpoints.isEmpty() ? getStartInstant() : checkpoints.get(checkpoints.size() - 1).getInstant();
	}

	/**
	 * Duration since the last created {@link Checkpoint} or the stopwatches
	 * starting time if no checkpoint was created
	 *
	 * @return the duration since last checkpoints instant or the stopwatches
	 *         starting time
	 */
	public Duration sinceLast() {
		return Duration.between(getLastInstant(), Instant.now());
	}

	/**
	 * Duration since the stopwatches start
	 *
	 * @return the duration since the stopwatches start
	 */
	public Duration sinceStart() {
		return Duration.between(getStartInstant(), Instant.now());
	}

	/**
	 * Waits for {@code duration} using {@code Thread#sleep(long)}. If reached, it
	 * times out at {@code timeoutSinceStart} after the {@link Stopwatch} start.
	 *
	 * @param duration          duration to wait
	 * @param timeoutSinceStart timeout duration since the {@link Stopwatch} start
	 * @return {@code true} if the timeout was not reached and {@code false} if the
	 *         timeout has been reached
	 * @throws InterruptedException if any thread has interrupted the current thread
	 */
	@SuppressWarnings("unused")
	@SuppressFBWarnings(value = "MDM_THREAD_YIELD", justification = "This is really intended to sleep for a specified duration.")
	public boolean waitFor(final Duration duration, final Duration timeoutSinceStart) throws InterruptedException {
		return waitFor(duration, timeoutSinceStart, throwing(waiting -> Thread.sleep(Nullables.orElseThrow(waiting).toMillis())));
	}

	/**
	 * Waits for {@code duration} using {@code wait}. If reached, it times out at
	 * {@code timeoutSinceStart} after the {@link Stopwatch} start.
	 *
	 * <p>
	 * Note: The {@link Duration} given to {@code wait} might be less than
	 * {@code duration} to handle {@code timeoutSinceStart} more precisely.
	 *
	 * @param duration          duration to wait
	 * @param timeoutSinceStart timeout duration since the {@link Stopwatch} start
	 * @param wait              method to use for waiting
	 * @return {@code true} if the timeout was not reached and {@code false} if the
	 *         timeout has been reached
	 */
	@SuppressFBWarnings(value = "PRMC_POSSIBLY_REDUNDANT_METHOD_CALLS", justification = "no redundant calls, because Instant.now() might return different values")
	public boolean waitFor(final Duration duration, final Duration timeoutSinceStart, final Consumer<Duration> wait) {
		if (duration.isNegative()) {
			throw new IllegalArgumentException(String.format("Parameter \"duration\" must not be negative, but is %s.", duration));
		}
		if (timeoutSinceStart.isNegative()) {
			throw new IllegalArgumentException(String.format("Parameter \"timeoutSinceStart\" must not be negative, but is %s.", timeoutSinceStart));
		}
		final Instant timeout = getStartInstant().plus(timeoutSinceStart);
		final Duration maxWaiting = Duration.between(Instant.now(), timeout);
		final Duration actualWaiting = maxWaiting.compareTo(duration) > 0 ? duration : maxWaiting;
		if (!actualWaiting.isNegative() && !actualWaiting.isZero()) {
			wait.accept(actualWaiting);
		}
		return !Instant.now().isAfter(timeout);
	}

	/**
	 * Sequential {@code Stream} with the checkpoints as its source
	 *
	 * @return a sequential {@code Stream} over the checkpoints
	 */
	public Stream<Checkpoint> stream() {
		return checkpoints.stream();
	}


	/**
	 * Implementation of a stopwatches checkpoint. It might be used for split or lap
	 * times.
	 */
	public static class Checkpoint implements Comparable<Checkpoint>, TemporalAccessor {
		/**
		 * Comparator of {@link Checkpoint}
		 */
		private static final Comparator<Checkpoint> COMPARATOR = Comparator.comparing(Checkpoint::getInstant).thenComparing(Comparator.comparing(checkpoint -> checkpoint.getPreviousCheckpoint().map(Checkpoint::getInstant).orElse(Instant.MIN)));
		/**
		 * Stopwatch
		 */
		private final Stopwatch stopwatch;
		/**
		 * Checkpoints name as reference
		 */
		private final String name;
		/**
		 * Instant
		 */
		private final Instant instant;
		/**
		 * Previous checkpoint or empty if this is the stopwatches first checkpoint
		 */
		private final Optional<Checkpoint> previousCheckpoint;

		/**
		 * {@inheritDoc}
		 */
		@Override
		public int compareTo(@Nullable final Checkpoint object) {
			return Objects.compare(this, object, COMPARATOR);
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public long getLong(@Nullable final TemporalField field) {
			return getInstant().getLong(field);
		}

		/**
		 * Instant of the previous {@link Checkpoint} or the stopwatches starting time
		 * if this is the first checkpoint
		 *
		 * @return the previous checkpoints instant or the stopwatches starting time
		 */
		public Instant getPreviousInstant() {
			return getPreviousCheckpoint().map(Checkpoint::getInstant).orElseGet(getStopwatch()::getStartInstant);
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public boolean isSupported(@Nullable final TemporalField field) {
			return getInstant().isSupported(field);
		}

		/**
		 * Duration between this checkpoint and the previous {@link Checkpoint} or the
		 * stopwatches starting time if this is the first checkpoint
		 *
		 * @return the duration between this checkpoint and previous checkpoints instant
		 *         or the stopwatches starting time
		 */
		public Duration sincePrevious() {
			return Duration.between(getPreviousInstant(), getInstant());
		}

		/**
		 * Duration between this checkpoint and the stopwatches start
		 *
		 * @return the duration between this checkpoint and the stopwatches start
		 */
		public Duration sinceStart() {
			return Duration.between(getStopwatch().getStartInstant(), getInstant());
		}

		/**
		 * Stopwatch
		 *
		 * @return the stopwatch
		 */
		@java.lang.SuppressWarnings("all")
		@edu.umd.cs.findbugs.annotations.SuppressFBWarnings(justification = "generated code")
		@lombok.Generated
		public Stopwatch getStopwatch() {
			return this.stopwatch;
		}

		/**
		 * Checkpoints name as reference
		 *
		 * @return the checkpoints name
		 */
		@java.lang.SuppressWarnings("all")
		@edu.umd.cs.findbugs.annotations.SuppressFBWarnings(justification = "generated code")
		@lombok.Generated
		public String getName() {
			return this.name;
		}

		/**
		 * Instant
		 *
		 * @return the instant
		 */
		@java.lang.SuppressWarnings("all")
		@edu.umd.cs.findbugs.annotations.SuppressFBWarnings(justification = "generated code")
		@lombok.Generated
		public Instant getInstant() {
			return this.instant;
		}

		/**
		 * Previous checkpoint or empty if this is the stopwatches first checkpoint
		 *
		 * @return the previous checkpoint
		 */
		@java.lang.SuppressWarnings("all")
		@edu.umd.cs.findbugs.annotations.SuppressFBWarnings(justification = "generated code")
		@lombok.Generated
		public Optional<Checkpoint> getPreviousCheckpoint() {
			return this.previousCheckpoint;
		}

		@edu.umd.cs.findbugs.annotations.NonNull
		@java.lang.Override
		@java.lang.SuppressWarnings("all")
		@edu.umd.cs.findbugs.annotations.SuppressFBWarnings(justification = "generated code")
		@lombok.Generated
		public java.lang.String toString() {
			return "Stopwatch.Checkpoint(instant=" + this.getInstant() + ", name=" + this.getName() + ", previousCheckpoint=" + this.getPreviousCheckpoint() + ", stopwatch=" + this.getStopwatch() + ")";
		}

		@java.lang.Override
		@java.lang.SuppressWarnings("all")
		@edu.umd.cs.findbugs.annotations.SuppressFBWarnings(justification = "generated code")
		@lombok.Generated
		public boolean equals(@edu.umd.cs.findbugs.annotations.Nullable final java.lang.Object o) {
			if (o == this) return true;
			if (!(o instanceof Stopwatch.Checkpoint)) return false;
			final Stopwatch.Checkpoint other = (Stopwatch.Checkpoint) o;
			if (!other.canEqual((java.lang.Object) this)) return false;
			final java.lang.Object this$name = this.getName();
			final java.lang.Object other$name = other.getName();
			if (this$name == null ? other$name != null : !this$name.equals(other$name)) return false;
			final java.lang.Object this$instant = this.getInstant();
			final java.lang.Object other$instant = other.getInstant();
			if (this$instant == null ? other$instant != null : !this$instant.equals(other$instant)) return false;
			final java.lang.Object this$previousCheckpoint = this.getPreviousCheckpoint();
			final java.lang.Object other$previousCheckpoint = other.getPreviousCheckpoint();
			if (this$previousCheckpoint == null ? other$previousCheckpoint != null : !this$previousCheckpoint.equals(other$previousCheckpoint)) return false;
			return true;
		}

		@java.lang.SuppressWarnings("all")
		@edu.umd.cs.findbugs.annotations.SuppressFBWarnings(justification = "generated code")
		@lombok.Generated
		protected boolean canEqual(@edu.umd.cs.findbugs.annotations.Nullable final java.lang.Object other) {
			return other instanceof Stopwatch.Checkpoint;
		}

		@java.lang.Override
		@java.lang.SuppressWarnings("all")
		@edu.umd.cs.findbugs.annotations.SuppressFBWarnings(justification = "generated code")
		@lombok.Generated
		public int hashCode() {
			final int PRIME = 59;
			int result = 1;
			final java.lang.Object $name = this.getName();
			result = result * PRIME + ($name == null ? 43 : $name.hashCode());
			final java.lang.Object $instant = this.getInstant();
			result = result * PRIME + ($instant == null ? 43 : $instant.hashCode());
			final java.lang.Object $previousCheckpoint = this.getPreviousCheckpoint();
			result = result * PRIME + ($previousCheckpoint == null ? 43 : $previousCheckpoint.hashCode());
			return result;
		}

		/**
		 * Creates a new {@code Checkpoint} instance.
		 *
		 * @param stopwatch Stopwatch
		 * @param name Checkpoints name as reference
		 * @param instant Instant
		 * @param previousCheckpoint Previous checkpoint or empty if this is the stopwatches first checkpoint
		 */
		@java.lang.SuppressWarnings("all")
		@edu.umd.cs.findbugs.annotations.SuppressFBWarnings(justification = "generated code")
		@lombok.Generated
		protected Checkpoint(final Stopwatch stopwatch, final String name, final Instant instant, final Optional<Checkpoint> previousCheckpoint) {
			this.stopwatch = stopwatch;
			this.name = name;
			this.instant = instant;
			this.previousCheckpoint = previousCheckpoint;
		}
	}

	/**
	 * Instant at the stopwatches start
	 *
	 * @return the instant at the stopwatches start time
	 */
	@java.lang.SuppressWarnings("all")
	@edu.umd.cs.findbugs.annotations.SuppressFBWarnings(justification = "generated code")
	@lombok.Generated
	public Instant getStartInstant() {
		return this.startInstant;
	}

	@edu.umd.cs.findbugs.annotations.NonNull
	@java.lang.Override
	@java.lang.SuppressWarnings("all")
	@edu.umd.cs.findbugs.annotations.SuppressFBWarnings(justification = "generated code")
	@lombok.Generated
	public java.lang.String toString() {
		return "Stopwatch(checkpoints=" + this.getCheckpoints() + ", startInstant=" + this.getStartInstant() + ")";
	}

	@java.lang.SuppressWarnings("all")
	@edu.umd.cs.findbugs.annotations.SuppressFBWarnings(justification = "generated code")
	@lombok.Generated
	public Stopwatch() {
	}
}