001package org.javasimon.utils; 002 003import java.text.DecimalFormat; 004import java.text.DecimalFormatSymbols; 005import java.text.SimpleDateFormat; 006import java.util.Date; 007import java.util.Locale; 008import java.util.concurrent.Callable; 009import java.util.regex.Pattern; 010 011import org.javasimon.Counter; 012import org.javasimon.Manager; 013import org.javasimon.Simon; 014import org.javasimon.SimonException; 015import org.javasimon.SimonFilter; 016import org.javasimon.SimonManager; 017import org.javasimon.Split; 018import org.javasimon.Stopwatch; 019 020/** 021 * SimonUtils provides static utility methods. 022 * <p/> 023 * <h3>Human readable outputs</h3> 024 * Both {@link org.javasimon.Stopwatch} and {@link org.javasimon.Counter} provide human readable 025 * {@code toString} outputs. All nanosecond values are converted into few valid digits with 026 * proper unit (ns, us, ms, s) - this is done via method {@link #presentNanoTime(long)}. 027 * Max/min counter values are checked for undefined state (max/min long value is converted 028 * to string "undef") - via method {@link #presentMinMaxCount(long)}. 029 * <p/> 030 * <h3>Aggregation utilities</h3> 031 * It is possible to sum up (aggregate) values for a subtree for a particular Simon type using 032 * {@link #calculateCounterAggregate(org.javasimon.Simon)} or {@link #calculateStopwatchAggregate(org.javasimon.Simon)}. 033 * Methods come also in versions allowing to filter by {@link org.javasimon.SimonFilter}. 034 * <p/> 035 * <h3>Simon tree operations</h3> 036 * For various debug purposes there is a method that creates string displaying the whole Simon sub-tree. 037 * Here is example code that initializes two random Simons and prints the whole Simon hierarchy 038 * (note that the method can be used to obtain any sub-tree of the hierarchy): 039 * <pre> 040 * Split split = SimonManager.getStopwatch("com.my.other.stopwatch").start(); 041 * SimonManager.getCounter("com.my.counter").setState(SimonState.DISABLED, false); 042 * split.stop(); 043 * System.out.println(SimonUtils.simonTreeString(SimonManager.getRootSimon()));</pre> 044 * And the output is: 045 * <pre> 046 * (+): Unknown Simon: [ ENABLED] 047 * com(+): Unknown Simon: [com INHERIT] 048 * my(+): Unknown Simon: [com.my INHERIT] 049 * other(+): Unknown Simon: [com.my.other INHERIT] 050 * stopwatch(+): Simon Stopwatch: total 24.2 ms, counter 1, max 24.2 ms, min 24.2 ms, mean 24.2 ms [com.my.other.stopwatch INHERIT] 051 * counter(-): Simon Counter: counter=0, max=undef, min=undef [com.my.counter DISABLED]</pre> 052 * Notice +/- signs in parenthesis that displays effective Simon state (enabled/disabled), further 053 * details are printed via each Simon's {@code toString} method. 054 * 055 * <h3>Other utilities</h3> 056 * It is possible to obtain "local name" of the Simon (behind the last dot) via {@link #localName(String)} 057 * or check if the name is valid Simon name via {@link #checkName(String)}. 058 * 059 * @author <a href="mailto:virgo47@gmail.com">Richard "Virgo" Richter</a> 060 * @author Radovan Sninsky 061 * @since 1.0 062 */ 063@SuppressWarnings({"UnusedDeclaration"}) 064public final class SimonUtils { 065 066 /** Regex character class content for {@link #NAME_PATTERN}. */ 067 public static final String NAME_PATTERN_CHAR_CLASS_CONTENT = "-_\\[\\]A-Za-z0-9.,@$%)(<>"; 068 069 /** Regex pattern for Simon names. */ 070 public static final Pattern NAME_PATTERN = Pattern.compile("[" + NAME_PATTERN_CHAR_CLASS_CONTENT + "]+"); 071 072 /** 073 * Allowed Simon name characters. 074 * 075 * @since 2.3 076 */ 077 public static final String ALLOWED_CHARS = "-_[]ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789.,@$%()<>"; 078 079 /** 080 * Name of the attribute where manager is searched for in an appropriate context - used for Spring/JavaEE/console integration. 081 * While the name can be used in any context supporting named attributes, it is primarily aimed for ServletContext. Manager can 082 * be shared in ServletContext by {@code SimonWebConfigurationBean} (Spring module) and then picked up by {@code SimonServletFilter} 083 * (JavaEE module) and {@code SimonConsoleFilter} (Embeddable console). If no manager is found in the attribute of the context, 084 * it is expected that components will use default {@link org.javasimon.SimonManager} instead. 085 * 086 * @since 3.2 087 */ 088 public static final String MANAGER_SERVLET_CTX_ATTRIBUTE = "manager-servlet-ctx-attribute"; 089 090 private static final SimpleDateFormat TIMESTAMP_FORMAT = new SimpleDateFormat("yyMMdd-HHmmss.SSS"); 091 092 private static final int UNIT_PREFIX_FACTOR = 1000; 093 private static final DecimalFormatSymbols DECIMAL_FORMAT_SYMBOLS = new DecimalFormatSymbols(Locale.US); 094 private static final int TEN = 10; 095 private static final DecimalFormat UNDER_TEN_FORMAT = new DecimalFormat("0.00", DECIMAL_FORMAT_SYMBOLS); 096 private static final int HUNDRED = 100; 097 private static final DecimalFormat UNDER_HUNDRED_FORMAT = new DecimalFormat("00.0", DECIMAL_FORMAT_SYMBOLS); 098 private static final DecimalFormat DEFAULT_FORMAT = new DecimalFormat("000", DECIMAL_FORMAT_SYMBOLS); 099 100 private static final String UNDEF_STRING = "undef"; 101 private static final int CLIENT_CODE_STACK_INDEX; 102 103 static { 104 // Finds out the index of "this code" in the returned stack trace - funny but it differs in JDK 1.5 and 1.6 105 int i = 1; 106 for (StackTraceElement ste : Thread.currentThread().getStackTrace()) { 107 i++; 108 if (ste.getClassName().equals(SimonUtils.class.getName())) { 109 break; 110 } 111 } 112 CLIENT_CODE_STACK_INDEX = i; 113 } 114 115 private SimonUtils() { 116 throw new AssertionError(); 117 } 118 119 /** 120 * Returns nano-time in human readable form with unit. Number is always from 10 to 9999 121 * except for seconds that are the biggest unit used. In case of undefined value ({@link Long#MAX_VALUE} or 122 * {@link Long#MIN_VALUE}) string {@link #UNDEF_STRING} is returned, so it can be used for min/max values as well. 123 * 124 * @param nanos time in nanoseconds 125 * @return human readable time string 126 */ 127 public static String presentNanoTime(long nanos) { 128 if (nanos == Long.MAX_VALUE || nanos == Long.MIN_VALUE) { 129 return UNDEF_STRING; 130 } 131 return presentNanoTimePrivate((double) nanos); 132 } 133 134 /** 135 * Returns nano-time in human readable form with unit. Number is always from 10 to 9999 136 * except for seconds that are the biggest unit used. 137 * 138 * @param nanos time in nanoseconds 139 * @return human readable time string 140 */ 141 public static String presentNanoTime(double nanos) { 142 if (nanos == Double.MAX_VALUE) { 143 return UNDEF_STRING; 144 } 145 return presentNanoTimePrivate(nanos); 146 } 147 148 private static String presentNanoTimePrivate(double time) { 149 if (Math.abs(time) < 1d) { 150 return "0"; 151 } 152 153 if (time < UNIT_PREFIX_FACTOR) { 154 return ((long) time) + " ns"; 155 } 156 157 time /= UNIT_PREFIX_FACTOR; 158 if (time < UNIT_PREFIX_FACTOR) { 159 return formatTime(time, " us"); 160 } 161 162 time /= UNIT_PREFIX_FACTOR; 163 if (time < UNIT_PREFIX_FACTOR) { 164 return formatTime(time, " ms"); 165 } 166 167 time /= UNIT_PREFIX_FACTOR; 168 return formatTime(time, " s"); 169 } 170 171 private static synchronized String formatTime(double time, String unit) { 172 if (time < TEN) { 173 return UNDER_TEN_FORMAT.format(time) + unit; 174 } 175 if (time < HUNDRED) { 176 return UNDER_HUNDRED_FORMAT.format(time) + unit; 177 } 178 return DEFAULT_FORMAT.format(time) + unit; 179 } 180 181 /** 182 * Returns timestamp in human readable (yet condensed) form "yyMMdd-HHmmss.SSS". 183 * 184 * @param timestamp timestamp in millis 185 * @return timestamp as a human readable string 186 */ 187 public static String presentTimestamp(long timestamp) { 188 if (timestamp == 0) { 189 return UNDEF_STRING; 190 } 191 synchronized (TIMESTAMP_FORMAT) { 192 return TIMESTAMP_FORMAT.format(new Date(timestamp)); 193 } 194 } 195 196 /** 197 * Returns min/max counter values in human readable form - if the value is max or min long value 198 * it is considered unused and string "undef" is returned. 199 * 200 * @param minmax counter extreme value 201 * @return counter value or "undef" if counter contains {@code Long.MIN_VALUE} or {@code Long.MAX_VALUE} 202 */ 203 public static String presentMinMaxCount(long minmax) { 204 if (minmax == Long.MAX_VALUE || minmax == Long.MIN_VALUE) { 205 return UNDEF_STRING; 206 } 207 return String.valueOf(minmax); 208 } 209 210 /** 211 * Returns multi-line string containing Simon tree starting with the specified Simon. 212 * Root Simon can be used to obtain tree with all Simons. Returns {@code null} for 213 * input value of null or for NullSimon or any Simon with name equal to null (anonymous 214 * Simons) - this is also the case when the Manager is disabled and tree for its root 215 * Simon is requested. 216 * 217 * @param simon root Simon of the output tree 218 * @return string containing the tree or null if the Simon is null Simon 219 */ 220 public static String simonTreeString(Simon simon) { 221 if (simon == null || simon.getName() == null) { 222 return null; 223 } 224 StringBuilder sb = new StringBuilder(); 225 printSimonTree(0, simon, sb); 226 return sb.toString(); 227 } 228 229 private static void printSimonTree(int level, Simon simon, StringBuilder sb) { 230 printSimon(level, simon, sb); 231 for (Simon child : simon.getChildren()) { 232 printSimonTree(level + 1, child, sb); 233 } 234 } 235 236 private static void printSimon(int level, Simon simon, StringBuilder sb) { 237 for (int i = 0; i < level; i++) { 238 sb.append(" "); 239 } 240 sb.append(localName(simon.getName())) 241 .append('(') 242 .append(simon.isEnabled() ? '+' : '-') 243 .append("): ") 244 .append(simon.toString()) 245 .append('\n'); 246 } 247 248 /** 249 * Returns last part of Simon name - local name. 250 * 251 * @param name full Simon name 252 * @return string containing local name 253 */ 254 public static String localName(String name) { 255 int ix = name.lastIndexOf(Manager.HIERARCHY_DELIMITER); 256 if (ix == -1) { 257 return name; 258 } 259 return name.substring(ix + 1); 260 } 261 262 /** 263 * Checks if the input string is correct Simon name. Simon name is checked against 264 * public {@link #NAME_PATTERN}. 265 * 266 * @param name checked Simon name 267 * @return true if the string is proper Simon name 268 */ 269 public static boolean checkName(String name) { 270 return NAME_PATTERN.matcher(name).matches(); 271 } 272 273 /** 274 * Checks if the input string is correct Simon name and throws {@link SimonException} otherwise. 275 * 276 * @param name checked Simon name 277 * @throws SimonException if the string is not proper Simon name 278 */ 279 public static void validateSimonName(String name) { 280 if (!checkName(name)) { 281 throw new SimonException("Simon name must match following pattern: '" + SimonUtils.NAME_PATTERN.pattern() + "', used name: " + name); 282 } 283 } 284 285 /** 286 * Autogenerates name for the Simon using the fully-qualified class name. 287 * <i>This method has inherent performance penalty (getting the stack-trace) so it is recommended to store 288 * pre-generated name. (Whole get-start-stop cycle is roughly 30-times slower with this method included.)</i> 289 * 290 * @param suffix name suffix for eventual Simon discrimination 291 * @return autogenerated name for Simon 292 * @since 3.1 293 */ 294 public static String generateNameForClass(String suffix) { 295 return generatePrivate(suffix, false); 296 } 297 298 /** 299 * Autogenerates name for the Simon using the fully qualified class name and the method name. 300 * <i>This method has inherent performance penalty (getting the stack-trace) so it is recommended to store 301 * pre-generated name. (Whole get-start-stop cycle is roughly 30-times slower with this method included.)</i> 302 * 303 * @param suffix name suffix for eventual Simon discrimination 304 * @return autogenerated name for Simon 305 * @since 3.1 306 */ 307 public static String generateNameForClassAndMethod(String suffix) { 308 return generatePrivate(suffix, true); 309 } 310 311 // method is extracted, so the stack trace index is always right 312 private static String generatePrivate(String suffix, boolean includeMethodName) { 313 StackTraceElement stackElement = Thread.currentThread().getStackTrace()[CLIENT_CODE_STACK_INDEX]; 314 StringBuilder nameBuilder = new StringBuilder(stackElement.getClassName()); 315 if (includeMethodName) { 316 nameBuilder.append('.').append(stackElement.getMethodName()); 317 } 318 if (suffix != null) { 319 nameBuilder.append(suffix); 320 } 321 return nameBuilder.toString(); 322 } 323 324 /** 325 * Autogenerates name for the Simon using the class name and the method name. 326 * 327 * @return autogenerated name for Simon 328 * @see #generateNameForClassAndMethod(String) 329 */ 330 public static String generateName() { 331 return generatePrivate(null, true); 332 } 333 334 /** 335 * Calls a block of code with stopwatch around and returns result. 336 * 337 * @param name name of the Stopwatch 338 * @param callable callable block of code 339 * @param <T> return type 340 * @return whatever block of code returns 341 * @throws Exception whatever block of code throws 342 * @since 3.0 343 */ 344 public static <T> T doWithStopwatch(String name, Callable<T> callable) throws Exception { 345 try (Split split = SimonManager.getStopwatch(name).start()) { 346 return callable.call(); 347 } 348 } 349 350 /** 351 * Calls a block of code with stopwatch around, can not return any result or throw an exception 352 * (use {@link #doWithStopwatch(String, java.util.concurrent.Callable)} instead). 353 * 354 * @param name name of the Stopwatch 355 * @param runnable wrapped block of code 356 * @since 3.0 357 */ 358 public static void doWithStopwatch(String name, Runnable runnable) { 359 try (Split split = SimonManager.getStopwatch(name).start()) { 360 runnable.run(); 361 } 362 } 363 364 private static final String SHRINKED_STRING = "..."; 365 366 /** 367 * Shrinks the middle of the input string if it is too long, so it does not exceed limitTo. 368 * 369 * @since 3.2 370 */ 371 public static String compact(String input, int limitTo) { 372 if (input == null || input.length() <= limitTo) { 373 return input; 374 } 375 376 int headLength = limitTo / 2; 377 int tailLength = limitTo - SHRINKED_STRING.length() - headLength; 378 if (tailLength < 0) { 379 tailLength = 1; 380 } 381 382 return input.substring(0, headLength) + SHRINKED_STRING + input.substring(input.length() - tailLength); 383 } 384 385 /** 386 * Aggregate statistics from all stopwatches in hierarchy of simons. 387 * 388 * @param simon root of the hierarchy of simons for which statistics will be aggregated 389 * @return aggregated statistics 390 * @since 3.5 391 */ 392 public static StopwatchAggregate calculateStopwatchAggregate(Simon simon) { 393 return calculateStopwatchAggregate(simon, null); 394 } 395 396 /** 397 * Aggregate statistics from all stopwatches in hierarchy that pass specified filter. Filter is applied 398 * to all simons in the hierarchy of all types. If a simon is rejected by filter its children are not considered. 399 * Simons are aggregated in the top-bottom fashion, i.e. parent simons are aggregated before children. 400 * 401 * @param simon root of the hierarchy of simons for which statistics will be aggregated 402 * @param filter filter to select subsets of simons to aggregate 403 * @return aggregates statistics 404 * @since 3.5 405 */ 406 public static StopwatchAggregate calculateStopwatchAggregate(Simon simon, SimonFilter filter) { 407 StopwatchAggregate stopwatchAggregate = new StopwatchAggregate(); 408 aggregateStopwatches(stopwatchAggregate, simon, 409 filter != null ? filter : SimonFilter.ACCEPT_ALL_FILTER); 410 411 return stopwatchAggregate; 412 } 413 414 private static void aggregateStopwatches(StopwatchAggregate aggregate, Simon simon, SimonFilter filter) { 415 if (filter.accept(simon)) { 416 if (simon instanceof Stopwatch) { 417 Stopwatch stopwatch = (Stopwatch) simon; 418 aggregate.addSample(stopwatch.sample()); 419 } 420 421 for (Simon child : simon.getChildren()) { 422 aggregateStopwatches(aggregate, child, filter); 423 } 424 } 425 } 426 427 /** 428 * Aggregate statistics from all counters in hierarchy of simons. 429 * 430 * @param simon root of the hierarchy of simons for which statistics will be aggregated 431 * @return aggregated statistics 432 * @since 3.5 433 */ 434 public static CounterAggregate calculateCounterAggregate(Simon simon) { 435 return calculateCounterAggregate(simon, null); 436 } 437 438 /** 439 * Aggregate statistics from all counters in hierarchy that pass specified filter. Filter is applied 440 * to all simons in the hierarchy of all types. If a simon is rejected by filter its children are not considered. 441 * Simons are aggregated in the top-bottom fashion, i.e. parent simons are aggregated before children. 442 * 443 * @param simon root of the hierarchy of simons for which statistics will be aggregated 444 * @param filter filter to select subsets of simons to aggregate 445 * @return aggregates statistics 446 * @since 3.5 447 */ 448 public static CounterAggregate calculateCounterAggregate(Simon simon, SimonFilter filter) { 449 CounterAggregate aggregate = new CounterAggregate(); 450 aggregateCounters(aggregate, simon, 451 filter != null ? filter : SimonFilter.ACCEPT_ALL_FILTER); 452 453 return aggregate; 454 } 455 456 private static void aggregateCounters(CounterAggregate aggregate, Simon simon, SimonFilter filter) { 457 if (filter.accept(simon)) { 458 if (simon instanceof Counter) { 459 Counter counter = (Counter) simon; 460 aggregate.addSample(counter.sample()); 461 } 462 463 for (Simon child : simon.getChildren()) { 464 aggregateCounters(aggregate, child, filter); 465 } 466 } 467 } 468}