001package ca.uhn.fhir.util; 002 003import com.google.common.annotations.VisibleForTesting; 004import org.apache.commons.lang3.time.DateUtils; 005 006import java.text.DecimalFormat; 007import java.text.NumberFormat; 008import java.util.Date; 009import java.util.LinkedList; 010import java.util.concurrent.TimeUnit; 011 012import static org.apache.commons.lang3.StringUtils.isNotBlank; 013 014/* 015 * #%L 016 * HAPI FHIR - Core Library 017 * %% 018 * Copyright (C) 2014 - 2019 University Health Network 019 * %% 020 * Licensed under the Apache License, Version 2.0 (the "License"); 021 * you may not use this file except in compliance with the License. 022 * You may obtain a copy of the License at 023 * 024 * http://www.apache.org/licenses/LICENSE-2.0 025 * 026 * Unless required by applicable law or agreed to in writing, software 027 * distributed under the License is distributed on an "AS IS" BASIS, 028 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 029 * See the License for the specific language governing permissions and 030 * limitations under the License. 031 * #L% 032 */ 033 034/** 035 * A multipurpose stopwatch which can be used to time tasks and produce 036 * human readable output about task duration, throughput, estimated task completion, 037 * etc. 038 * <p> 039 * <p> 040 * <b>Thread Safety Note: </b> StopWatch is not intended to be thread safe. 041 * </p> 042 * 043 * @since HAPI FHIR 3.3.0 044 */ 045public class StopWatch { 046 047 private static Long ourNowForUnitTest; 048 private long myStarted = now(); 049 private TaskTiming myCurrentTask; 050 private LinkedList<TaskTiming> myTasks; 051 /** 052 * Constructor 053 */ 054 public StopWatch() { 055 super(); 056 } 057 /** 058 * Constructor 059 * 060 * @param theStart The time to record as the start for this timer 061 */ 062 public StopWatch(Date theStart) { 063 myStarted = theStart.getTime(); 064 } 065 066 private void addNewlineIfContentExists(StringBuilder theB) { 067 if (theB.length() > 0) { 068 theB.append("\n"); 069 } 070 } 071 072 /** 073 * Finish the counter on the current task (which was started by calling 074 * {@link #startTask(String)}. This method has no effect if no task 075 * is currently started so it's ok to call it more than once. 076 */ 077 public void endCurrentTask() { 078 ensureTasksListExists(); 079 if (myCurrentTask != null) { 080 myCurrentTask.setEnd(now()); 081 } 082 myCurrentTask = null; 083 } 084 085 private void ensureTasksListExists() { 086 if (myTasks == null) { 087 myTasks = new LinkedList<>(); 088 } 089 } 090 091 /** 092 * Returns a nice human-readable display of the time taken per 093 * operation. Note that this may not actually output the number 094 * of milliseconds if the time taken per operation was very long (over 095 * 10 seconds) 096 * 097 * @see #formatMillis(long) 098 */ 099 public String formatMillisPerOperation(long theNumOperations) { 100 double millisPerOperation = (((double) getMillis()) / Math.max(1.0, theNumOperations)); 101 return formatMillis(millisPerOperation); 102 } 103 104 /** 105 * Returns a string providing the durations of all tasks collected by {@link #startTask(String)} 106 */ 107 public String formatTaskDurations() { 108 109 ensureTasksListExists(); 110 StringBuilder b = new StringBuilder(); 111 112 if (myTasks.size() > 0) { 113 long delta = myTasks.getFirst().getStart() - myStarted; 114 if (delta > 10) { 115 addNewlineIfContentExists(b); 116 b.append("Before first task"); 117 b.append(": "); 118 b.append(formatMillis(delta)); 119 } 120 } 121 122 TaskTiming last = null; 123 for (TaskTiming nextTask : myTasks) { 124 125 if (last != null) { 126 long delta = nextTask.getStart() - last.getEnd(); 127 if (delta > 10) { 128 addNewlineIfContentExists(b); 129 b.append("Between"); 130 b.append(": "); 131 b.append(formatMillis(delta)); 132 } 133 } 134 135 addNewlineIfContentExists(b); 136 b.append(nextTask.getTaskName()); 137 b.append(": "); 138 long delta = nextTask.getMillis(); 139 b.append(formatMillis(delta)); 140 141 last = nextTask; 142 } 143 144 if (myTasks.size() > 0) { 145 long delta = now() - myTasks.getLast().getEnd(); 146 if (delta > 10) { 147 addNewlineIfContentExists(b); 148 b.append("After last task"); 149 b.append(": "); 150 b.append(formatMillis(delta)); 151 } 152 } 153 154 return b.toString(); 155 } 156 157 /** 158 * Determine the current throughput per unit of time (specified in theUnit) 159 * assuming that theNumOperations operations have happened. 160 * <p> 161 * For example, if this stopwatch has 2 seconds elapsed, and this method is 162 * called for theNumOperations=30 and TimeUnit=SECONDS, 163 * this method will return 15 164 * </p> 165 * 166 * @see #getThroughput(long, TimeUnit) 167 */ 168 public String formatThroughput(long theNumOperations, TimeUnit theUnit) { 169 double throughput = getThroughput(theNumOperations, theUnit); 170 return new DecimalFormat("0.0").format(throughput); 171 } 172 173 /** 174 * Given an amount of something completed so far, and a total amount, calculates how long it will take for something to complete 175 * 176 * @param theCompleteToDate The amount so far 177 * @param theTotal The total (must be higher than theCompleteToDate 178 * @return A formatted amount of time 179 */ 180 public String getEstimatedTimeRemaining(double theCompleteToDate, double theTotal) { 181 double millis = getMillis(); 182 long millisRemaining = (long) (((theTotal / theCompleteToDate) * millis) - (millis)); 183 return formatMillis(millisRemaining); 184 } 185 186 public long getMillis(Date theNow) { 187 return theNow.getTime() - myStarted; 188 } 189 190 public long getMillis() { 191 long now = now(); 192 return now - myStarted; 193 } 194 195 public long getMillisAndRestart() { 196 long now = now(); 197 long retVal = now - myStarted; 198 myStarted = now; 199 return retVal; 200 } 201 202 /** 203 * @param theNumOperations Ok for this to be 0, it will be treated as 1 204 */ 205 public long getMillisPerOperation(long theNumOperations) { 206 return (long) (((double) getMillis()) / Math.max(1.0, theNumOperations)); 207 } 208 209 public Date getStartedDate() { 210 return new Date(myStarted); 211 } 212 213 /** 214 * Determine the current throughput per unit of time (specified in theUnit) 215 * assuming that theNumOperations operations have happened. 216 * <p> 217 * For example, if this stopwatch has 2 seconds elapsed, and this method is 218 * called for theNumOperations=30 and TimeUnit=SECONDS, 219 * this method will return 15 220 * </p> 221 * 222 * @see #formatThroughput(long, TimeUnit) 223 */ 224 public double getThroughput(long theNumOperations, TimeUnit theUnit) { 225 if (theNumOperations <= 0) { 226 return 0.0f; 227 } 228 229 long millisElapsed = Math.max(1, getMillis()); 230 long periodMillis = theUnit.toMillis(1); 231 232 double denominator = ((double) millisElapsed) / ((double) periodMillis); 233 234 return (double) theNumOperations / denominator; 235 } 236 237 public void restart() { 238 myStarted = now(); 239 } 240 241 /** 242 * Starts a counter for a sub-task 243 * <p> 244 * <b>Thread Safety Note: </b> This method is not threadsafe! Do not use subtasks in a 245 * multithreaded environment. 246 * </p> 247 * 248 * @param theTaskName Note that if theTaskName is blank or empty, no task is started 249 */ 250 public void startTask(String theTaskName) { 251 endCurrentTask(); 252 if (isNotBlank(theTaskName)) { 253 myCurrentTask = new TaskTiming() 254 .setTaskName(theTaskName) 255 .setStart(now()); 256 myTasks.add(myCurrentTask); 257 } 258 } 259 260 /** 261 * Formats value in an appropriate format. See {@link #formatMillis(long)}} 262 * for a description of the format 263 * 264 * @see #formatMillis(long) 265 */ 266 @Override 267 public String toString() { 268 return formatMillis(getMillis()); 269 } 270 271 private static class TaskTiming { 272 private long myStart; 273 private long myEnd; 274 private String myTaskName; 275 276 public long getEnd() { 277 if (myEnd == 0) { 278 return now(); 279 } 280 return myEnd; 281 } 282 283 public TaskTiming setEnd(long theEnd) { 284 myEnd = theEnd; 285 return this; 286 } 287 288 public long getMillis() { 289 return getEnd() - getStart(); 290 } 291 292 public long getStart() { 293 return myStart; 294 } 295 296 public TaskTiming setStart(long theStart) { 297 myStart = theStart; 298 return this; 299 } 300 301 public String getTaskName() { 302 return myTaskName; 303 } 304 305 public TaskTiming setTaskName(String theTaskName) { 306 myTaskName = theTaskName; 307 return this; 308 } 309 } 310 311 private static NumberFormat getDayFormat() { 312 return new DecimalFormat("0.0"); 313 } 314 315 private static NumberFormat getTenDayFormat() { 316 return new DecimalFormat("0"); 317 } 318 319 private static NumberFormat getSubMillisecondMillisFormat() { 320 return new DecimalFormat("0.000"); 321 } 322 323 /** 324 * Append a right-aligned and zero-padded numeric value to a `StringBuilder`. 325 */ 326 static private void append(StringBuilder tgt, String pfx, int dgt, long val) { 327 tgt.append(pfx); 328 if (dgt > 1) { 329 int pad = (dgt - 1); 330 for (long xa = val; xa > 9 && pad > 0; xa /= 10) { 331 pad--; 332 } 333 for (int xa = 0; xa < pad; xa++) { 334 tgt.append('0'); 335 } 336 } 337 tgt.append(val); 338 } 339 340 /** 341 * Formats a number of milliseconds for display (e.g. 342 * in a log file), tailoring the output to how big 343 * the value actually is. 344 * <p> 345 * Example outputs: 346 * </p> 347 * <ul> 348 * <li>133ms</li> 349 * <li>00:00:10.223</li> 350 * <li>1.7 days</li> 351 * <li>64 days</li> 352 * </ul> 353 */ 354 public static String formatMillis(long theMillis) { 355 return formatMillis((double) theMillis); 356 } 357 358 /** 359 * Formats a number of milliseconds for display (e.g. 360 * in a log file), tailoring the output to how big 361 * the value actually is. 362 * <p> 363 * Example outputs: 364 * </p> 365 * <ul> 366 * <li>133ms</li> 367 * <li>00:00:10.223</li> 368 * <li>1.7 days</li> 369 * <li>64 days</li> 370 * </ul> 371 */ 372 public static String formatMillis(double theMillis) { 373 StringBuilder buf = new StringBuilder(20); 374 if (theMillis > 0.0 && theMillis < 1.0) { 375 buf.append(getSubMillisecondMillisFormat().format(theMillis)); 376 buf.append("ms"); 377 } else if (theMillis < (10 * DateUtils.MILLIS_PER_SECOND)) { 378 buf.append((int) theMillis); 379 buf.append("ms"); 380 } else if (theMillis >= DateUtils.MILLIS_PER_DAY) { 381 double days = theMillis / DateUtils.MILLIS_PER_DAY; 382 if (days >= 10) { 383 buf.append(getTenDayFormat().format(days)); 384 buf.append(" days"); 385 } else if (days != 1.0f) { 386 buf.append(getDayFormat().format(days)); 387 buf.append(" days"); 388 } else { 389 buf.append(getDayFormat().format(days)); 390 buf.append(" day"); 391 } 392 } else { 393 long millisAsLong = (long) theMillis; 394 append(buf, "", 2, ((millisAsLong % DateUtils.MILLIS_PER_DAY) / DateUtils.MILLIS_PER_HOUR)); 395 append(buf, ":", 2, ((millisAsLong % DateUtils.MILLIS_PER_HOUR) / DateUtils.MILLIS_PER_MINUTE)); 396 append(buf, ":", 2, ((millisAsLong % DateUtils.MILLIS_PER_MINUTE) / DateUtils.MILLIS_PER_SECOND)); 397 if (theMillis <= DateUtils.MILLIS_PER_MINUTE) { 398 append(buf, ".", 3, (millisAsLong % DateUtils.MILLIS_PER_SECOND)); 399 } 400 } 401 return buf.toString(); 402 } 403 404 private static long now() { 405 if (ourNowForUnitTest != null) { 406 return ourNowForUnitTest; 407 } 408 return System.currentTimeMillis(); 409 } 410 411 @VisibleForTesting 412 static void setNowForUnitTestForUnitTest(Long theNowForUnitTest) { 413 ourNowForUnitTest = theNowForUnitTest; 414 } 415 416}