001package io.cui.tools.concurrent; 002 003import static io.cui.tools.base.Preconditions.checkState; 004import static java.util.Objects.requireNonNull; 005import static java.util.concurrent.TimeUnit.DAYS; 006import static java.util.concurrent.TimeUnit.HOURS; 007import static java.util.concurrent.TimeUnit.MICROSECONDS; 008import static java.util.concurrent.TimeUnit.MILLISECONDS; 009import static java.util.concurrent.TimeUnit.MINUTES; 010import static java.util.concurrent.TimeUnit.NANOSECONDS; 011import static java.util.concurrent.TimeUnit.SECONDS; 012 013import java.io.Serializable; 014import java.time.Duration; 015import java.util.Locale; 016import java.util.concurrent.TimeUnit; 017 018/** 019 * An object that measures elapsed time in nanoseconds. It is useful to measure elapsed time using 020 * this class instead of direct calls to {@link System#nanoTime} for a few reasons: 021 * 022 * <ul> 023 * <li>An alternate time source can be substituted, for testing or performance reasons. 024 * <li>As documented by {@code nanoTime}, the value returned has no absolute meaning, and can only 025 * be interpreted as relative to another timestamp returned by {@code nanoTime} at a different 026 * time. {@code StopWatch} is a more effective abstraction because it exposes only these 027 * relative values, not the absolute ones. 028 * </ul> 029 * 030 * <p> 031 * Basic usage: 032 * 033 * <pre> 034 * 035 * StopWatch stopwatch = StopWatch.createStarted(); 036 * doSomething(); 037 * stopwatch.stop(); // optional 038 * 039 * Duration duration = stopwatch.elapsed(); 040 * 041 * log.info("time: " + stopwatch); // formatted string like "12.3 ms" 042 * </pre> 043 * 044 * <p> 045 * StopWatch methods are not idempotent; it is an error to start or stop a stopwatch that is 046 * already in the desired state. 047 * 048 * <p> 049 * When testing code that uses this class, use {@link #createUnstarted(Ticker)} or {@link 050 * #createStarted(Ticker)} to supply a fake or mock ticker. This allows you to simulate any valid 051 * behavior of the stopwatch. 052 * 053 * <p> 054 * <b>Note:</b> This class is not thread-safe. 055 * <p> 056 * 057 * @author com.google.common.base.Stopwatch 058 */ 059public final class StopWatch implements Serializable { 060 061 private static final long serialVersionUID = 4764741831457507136L; 062 063 private final Ticker ticker; 064 private boolean isRunning; 065 private long elapsedNanos; 066 private long startTick; 067 068 /** 069 * @return a created (but not started) new stopwatch using {@link System#nanoTime} as its time 070 * source. 071 * 072 */ 073 public static StopWatch createUnstarted() { 074 return new StopWatch(); 075 } 076 077 /** 078 * @param ticker specified time source, must not be null 079 * @return a created (but not started) new stopwatch, using the specified time source. 080 * 081 */ 082 public static StopWatch createUnstarted(Ticker ticker) { 083 return new StopWatch(ticker); 084 } 085 086 /** 087 * @return a created (and started) new stopwatch using {@link System#nanoTime} as its time 088 * source. 089 * 090 */ 091 public static StopWatch createStarted() { 092 return new StopWatch().start(); 093 } 094 095 /** 096 * @param ticker specified time source, must not be null 097 * @return a created (and started) new stopwatch, using the specified time source. 098 * 099 */ 100 public static StopWatch createStarted(Ticker ticker) { 101 return new StopWatch(ticker).start(); 102 } 103 104 StopWatch() { 105 ticker = new Ticker(); 106 } 107 108 StopWatch(Ticker ticker) { 109 this.ticker = requireNonNull(ticker, "ticker"); 110 } 111 112 /** 113 * @return {@code true} if {@link #start()} has been called on this stopwatch, and 114 * {@link #stop()} has not been called since the last call to {@code start()}. 115 */ 116 public boolean isRunning() { 117 return isRunning; 118 } 119 120 /** 121 * Starts the stopwatch. 122 * 123 * @return this {@code StopWatch} instance 124 * @throws IllegalStateException if the stopwatch is already running. 125 */ 126 public StopWatch start() { 127 checkState(!isRunning, "This stopwatch is already running."); 128 isRunning = true; 129 startTick = ticker.read(); 130 return this; 131 } 132 133 /** 134 * Stops the stopwatch. Future reads will return the fixed duration that had elapsed up to this 135 * point. 136 * 137 * @return this {@code StopWatch} instance 138 * @throws IllegalStateException if the stopwatch is already stopped. 139 */ 140 public StopWatch stop() { 141 var tick = ticker.read(); 142 checkState(isRunning, "This stopwatch is already stopped."); 143 isRunning = false; 144 elapsedNanos += tick - startTick; 145 return this; 146 } 147 148 /** 149 * Sets the elapsed time for this stopwatch to zero, and places it in a stopped state. 150 * 151 * @return this {@code StopWatch} instance 152 */ 153 public StopWatch reset() { 154 elapsedNanos = 0; 155 isRunning = false; 156 return this; 157 } 158 159 private long elapsedNanos() { 160 return isRunning ? ticker.read() - startTick + elapsedNanos : elapsedNanos; 161 } 162 163 /** 164 * @param desiredUnit must not be null 165 * @return the current elapsed time shown on this stopwatch, expressed in the desired time unit, 166 * with any fraction rounded down. 167 * 168 * <p> 169 * <b>Note:</b> the overhead of measurement can be more than a microsecond, so it is 170 * generally not useful to specify {@link TimeUnit#NANOSECONDS} precision here. 171 * 172 * <p> 173 * It is generally not a good idea to use an ambiguous, unitless {@code long} to 174 * represent elapsed time. Therefore, we recommend using {@link #elapsed()} instead, 175 * which returns a strongly-typed {@link Duration} instance. 176 * 177 */ 178 public long elapsed(TimeUnit desiredUnit) { 179 return desiredUnit.convert(elapsedNanos(), NANOSECONDS); 180 } 181 182 /** 183 * @return the current elapsed time shown on this stopwatch as a {@link Duration}. Unlike {@link 184 * #elapsed(TimeUnit)}, this method does not lose any precision due to rounding. 185 * 186 */ 187 public Duration elapsed() { 188 return Duration.ofNanos(elapsedNanos()); 189 } 190 191 /** Returns a string representation of the current elapsed time. */ 192 @Override 193 public String toString() { 194 var nanos = elapsedNanos(); 195 196 var unit = chooseUnit(nanos); 197 var value = (double) nanos / NANOSECONDS.convert(1, unit); 198 199 return String.format(Locale.ROOT, "%.4g", value) + " " + abbreviate(unit); 200 } 201 202 private static TimeUnit chooseUnit(long nanos) { 203 if (DAYS.convert(nanos, NANOSECONDS) > 0) { 204 return DAYS; 205 } 206 if (HOURS.convert(nanos, NANOSECONDS) > 0) { 207 return HOURS; 208 } 209 if (MINUTES.convert(nanos, NANOSECONDS) > 0) { 210 return MINUTES; 211 } 212 if (SECONDS.convert(nanos, NANOSECONDS) > 0) { 213 return SECONDS; 214 } 215 if (MILLISECONDS.convert(nanos, NANOSECONDS) > 0) { 216 return MILLISECONDS; 217 } 218 if (MICROSECONDS.convert(nanos, NANOSECONDS) > 0) { 219 return MICROSECONDS; 220 } 221 return NANOSECONDS; 222 } 223 224 private static String abbreviate(TimeUnit unit) { 225 switch (unit) { 226 case NANOSECONDS: 227 return "ns"; 228 case MICROSECONDS: 229 return "\u03bcs"; // μs 230 case MILLISECONDS: 231 return "ms"; 232 case SECONDS: 233 return "s"; 234 case MINUTES: 235 return "min"; 236 case HOURS: 237 return "h"; 238 case DAYS: 239 return "d"; 240 default: 241 throw new AssertionError(); 242 } 243 } 244}