001package com.randomnoun.common.timer; 002 003/* (c) 2013 randomnoun. All Rights Reserved. This work is licensed under a 004 * BSD Simplified License. (http://www.randomnoun.com/bsd-simplified.html) 005 */ 006 007import java.io.*; 008import java.text.*; 009import java.util.*; 010 011import com.randomnoun.common.Text; 012 013/** 014 * An object that performs time benchmarking. 015 * 016 * <p>This object contains a reference to all benchmark instances 017 * that are currently executing in the VM, referenced by <code>benchId</code>. 018 * It is encouraged that each individual request has a unique benchId, although 019 * this is not required unless you need to use the {@link #getBenchmark} method. 020 * 021 * (if multiple instances are created using the same <code>benchId</code>, 022 * only the most recent instance will be retrievable via getBenchmark()). 023 * 024 * <p>Multiple checkpoints can be set during the course of a benchmark, which 025 * will be written to disk after completion. 026 * 027 * <p>Output is generated only after the benchmark has completed. All output is 028 * buffered, and references to any open benchmark files are kept within this 029 * object in order to minimise the amount of overhead that benchmarking will 030 * impose. 031 * 032 * <p>This class does not expire benchmarks, and therefore may cause memory leaks 033 * if benchmarks are not closed correctly. 034 * 035 * <p>Output is of the form: 036 * <pre style="code"> 037 * benchId1,begin,timestamp,pointID1,timestamp1,pointID1,timestamp2,[...],end,timestampn,duration 038 * benchId2,begin,timestamp,pointID1,timestamp1,pointID1,timestamp2,[...],end,timestampn,duration 039 * : 040 * : 041 * </pre> 042 * 043 * <p>where <i>benchId</i> is the benchId indentifying this benchmark, and 044 * <i>pointIDn</i> are the individual checkpoint identifiers. Each line finishes 045 * with the text ",duration," followed by the duration of the entire benchmark, in 046 * milliseconds. Timestamps are displayed in the format defined by the 047 * {@link #setDateFormat(String)} method. 048 * 049 * <p>There is no current way of following a benchmark through the EJB boundary layer; 050 * to do so would currently involve overriding the User object, which I'm not terribly keen to do. 051 * It is possible to set up a new Benchmark on the EJB side, and track performance 052 * independently over there. It should then be a simple matter to heuristically match up 053 * individual HTTP requests/struts actions and the EJB methods that they invoke 054 * (under normal load conditions). It's important, however, that two Benchmark engines in the 055 * same VM do not write to the same file, as they will corrupt each other's output. 056 * 057 * @author knoxg 058 * 059 */ 060public class Benchmark { 061 062 /** A Comparator which performs comparisons between two Benchmarks. 063 */ 064 public static class BenchmarkComparator implements Comparator<Benchmark> { 065 066 /** Create a new BenchmarkComparator object */ 067 public BenchmarkComparator() { 068 } 069 070 /** Compare two structured list elements 071 * 072 * @see java.util.Comparator#compare(java.lang.Object, java.lang.Object) 073 */ 074 public int compare(Benchmark a, Benchmark b) throws IllegalArgumentException { 075 Benchmark benchmarkA = (Benchmark) a; 076 Benchmark benchmarkB = (Benchmark) b; 077 078 return benchmarkA.startTime < benchmarkB.startTime ? -1 : 079 (benchmarkA.startTime > benchmarkB.startTime ? 1 : 0); 080 } 081 } 082 083 /** A class containing the checkpoints for a single benchmark */ 084 public class CheckpointList { 085 /** List of Checkpoints, containing the checkpoints for a single 086 * benchmark */ 087 List<Checkpoint> list = new ArrayList<Checkpoint>(); 088 089 /** Return an iterator for the checkpoint list */ 090 public Iterator<Checkpoint> iterator() { 091 return list.iterator(); 092 } 093 094 /** Retrieve an individual checkpoint */ 095 public Checkpoint get(int n) { 096 return (Checkpoint) list.get(n); 097 } 098 099 /** Adds a checkpoint to this set. The checkpoint is stamped using 100 * the current system time. */ 101 public void addCheckPoint(String id) { 102 list.add(new Checkpoint(id)); 103 } 104 105 /** Adds an annotation to this set. */ 106 public void addAnnotation(String annotation) { 107 list.add(new Checkpoint(annotation, -1)); 108 } 109 110 111 /** Returns a string version of this set. Timestamps are returned 112 * using the format specified by the Benchmark class */ 113 public String toString() { 114 int i; 115 int size = list.size(); 116 StringBuffer sb = new StringBuffer(); 117 Checkpoint checkpoint; 118 SimpleDateFormat dateFormat; 119 120 dateFormat = new SimpleDateFormat(Benchmark.dateFormatText); 121 122 for (i = 0; i < size; i++) { 123 checkpoint = (Checkpoint) list.get(i); 124 if (checkpoint.getTimestamp()==-1) { 125 sb.append(','); 126 sb.append(Text.escapeCsv("\u0087" + checkpoint.id)); // ASCII 135, Hex 87 127 } else { 128 sb.append(','); 129 sb.append(Text.escapeCsv(dateFormat.format(checkpoint.getDate()))); 130 sb.append(','); 131 sb.append(Text.escapeCsv(checkpoint.id)); 132 } 133 } 134 135 if (list.size() > 0) { 136 sb.append(",t,"); 137 checkpoint = (Checkpoint) list.get(list.size() - 1); 138 sb.append(HiResTimer.getElapsedMillis(((Checkpoint) list.get(0)).timestamp, checkpoint.timestamp)); 139 } 140 141 return sb.toString(); 142 } 143 } 144 145 /** A single benchmark point. May be either a begin, end, or checkpoint. 146 * 147 * A checkpoint with a timestamp of -1 is an annotation. 148 */ 149 public class Checkpoint { 150 /** The string ID for this benchmark point */ 151 String id; 152 153 /** The time at which this point was created to the benchmark */ 154 long timestamp; 155 156 public Checkpoint(String id) { 157 this.id = id; 158 this.timestamp = HiResTimer.getTimestamp(); 159 } 160 161 public Checkpoint(String id, long timestamp) { 162 this.id = id; 163 this.timestamp = timestamp; 164 } 165 166 public String getId() { 167 return id; 168 } 169 170 public long getTimestamp() { 171 return timestamp; 172 } 173 174 public Date getDate() { 175 // return date; 176 return new Date(startTime + HiResTimer.getElapsedMillis(startHiresTimestamp, timestamp)); 177 } 178 } 179 180 /** Hashtable mapping filenames to java.io.Writer objects */ 181 private static Hashtable<String, Writer> writers = null; 182 183 /** Hashtable mapping benchmark IDs to Benchmark objects */ 184 private static Hashtable<String, Benchmark> benchmarks = null; 185 186 /** date format for timestamps. The default date format is 187 * the SimpleDateFormat <tt>"yyyy-dd-MM HH:mm:ss.SSS"</tt>. Note that 188 * better-than-millisecond precision is unavailable using the 189 * standard Java-supplied timer functions. */ 190 private static String dateFormatText = null; 191 192 /** the set of checkpoints for this benchmark instance */ 193 protected CheckpointList checkpointList = null; 194 195 /** the string ID for this benchmark */ 196 private String benchId = null; 197 198 /** Start time, in milliseconds */ 199 protected long startTime; 200 201 /** HiRes Timer token for start of benchmark */ 202 protected long startHiresTimestamp; 203 204 /** True if benchmark is active, false if the benchmark has completed. */ 205 protected boolean active; 206 207 /** the file where the results of this benchmark will be written */ 208 private String filename = null; 209 210 /** Returns a PrintWriter that can be written to in order to 211 * commit a benchmark to disk. */ 212 private static PrintWriter open(String filename) 213 throws IOException { 214 PrintWriter out; 215 216 // use existing Writer if it is already open 217 if (writers.get(filename) != null) { 218 return (PrintWriter) writers.get(filename); 219 } 220 221 // 'true' flag indicates files will be appended to 222 out = new PrintWriter(new BufferedWriter(new FileWriter(filename, true))); 223 writers.put(filename, out); 224 225 return out; 226 } 227 228 /** Instantiates a new benchmark. Automatically creates a 'begin' checkpoint 229 * for this new instance. */ 230 public Benchmark(String benchId, String filename) { 231 startTime = System.currentTimeMillis(); 232 startHiresTimestamp = HiResTimer.getTimestamp(); 233 234 checkpointList = new CheckpointList(); 235 benchmarks.put(benchId, this); 236 checkpointList.addCheckPoint("begin"); 237 this.benchId = benchId; 238 this.filename = filename; 239 this.active = true; 240 } 241 242 /** Alters the ID for a benchmark that is already running. */ 243 public void setBenchId(String benchId) { 244 benchmarks.remove(this.benchId); 245 this.benchId = benchId; 246 benchmarks.put(benchId, this); 247 } 248 249 /** Performs checkpoint. */ 250 public void checkpoint(String pointID) { 251 checkpointList.addCheckPoint(pointID); 252 } 253 254 /** Creates a benchmark annotation */ 255 public void annotate(String annotation) { 256 checkpointList.addAnnotation(annotation); 257 } 258 259 260 /** Returns a string representation of this benchmark */ 261 public String toString() { 262 return benchId + checkpointList.toString(); 263 } 264 265 /** Completes benchmark and writes to disk */ 266 public void end() 267 throws IOException { 268 PrintWriter printWriter; 269 270 checkpointList.addCheckPoint("end"); 271 if (filename != null) { 272 printWriter = (PrintWriter) writers.get(filename); 273 if (printWriter == null) { 274 printWriter = open(filename); 275 } 276 printWriter.println(this.toString()); 277 printWriter.flush(); 278 } 279 active = false; 280 benchmarks.remove(benchId); 281 } 282 283 /** Returns whether this benchmark is currently active */ 284 public boolean isActive() { 285 return active; 286 } 287 288 289 /** Cancel this benchmark */ 290 public void cancel() { 291 benchmarks.remove(benchId); 292 } 293 294 /** Returns all checkpoints stored in this benchmark */ 295 public CheckpointList getCheckpointList() { 296 return checkpointList; 297 } 298 299 /** Returns the benchmark ID */ 300 public String getId() { 301 return benchId; 302 } 303 304 /** Returns the benchmark with the supplied ID */ 305 public static Benchmark getBenchmark(String benchId) { 306 return (Benchmark) benchmarks.get(benchId); 307 } 308 309 /** Sets the date format for all output of this class. The date format 310 * is represented as a string, rather than a DateFormat object, since 311 * most SimpleDateFormats are not thread-safe. */ 312 public static void setDateFormat(String dateFormatText) { 313 Benchmark.dateFormatText = dateFormatText; 314 } 315 316 /** Flushes all writers within this benchmark object (commits any 317 * unwritten data to disk). Files are still kept open. */ 318 public static void flushWriters() 319 throws IOException { 320 Enumeration<Writer> e; 321 322 for (e = writers.elements(); e.hasMoreElements();) { 323 ((Writer) e.nextElement()).flush(); 324 } 325 } 326 327 /** Closes all writers within this benchmark object. Files will be 328 * automatically reopened for any running benchmarks. */ 329 public static void closeWriters() 330 throws IOException { 331 Enumeration<Writer> e; 332 333 for (e = writers.elements(); e.hasMoreElements();) { 334 ((Writer) e.nextElement()).close(); 335 } 336 337 // remove references to old writers 338 writers = new Hashtable<String, Writer>(); 339 } 340 341 public String toJson() { 342 String result = "["; 343 Iterator<Checkpoint> i = getCheckpointList().iterator(); 344 // Benchmark.Checkpoint cpStart = i.hasNext() ? (Benchmark.Checkpoint) i.next() : null; 345 while (i.hasNext()) { 346 Benchmark.Checkpoint cpNext = (Benchmark.Checkpoint) i.next(); 347 result += "{\"id\":\"" + Text.escapeJavascript(cpNext.getId()) + "\",\"timestamp\":" + cpNext.getTimestamp() + "}"; 348 if (i.hasNext()) { result += ","; } 349 } 350 result += "]"; 351 return result; 352 } 353 354 355 static { 356 writers = new Hashtable<String, Writer>(); 357 benchmarks = new Hashtable<String, Benchmark>(); 358 359 // default date format 360 dateFormatText = "yyyy-MM-dd HH:mm:ss.SSS"; 361 } 362}