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