1 package com.randomnoun.common.timer;
2
3 /* (c) 2013 randomnoun. All Rights Reserved. This work is licensed under a
4 * BSD Simplified License. (http://www.randomnoun.com/bsd-simplified.html)
5 */
6
7 import java.io.*;
8 import java.text.*;
9 import java.util.*;
10
11 import com.randomnoun.common.Text;
12
13 /**
14 * An object that performs time benchmarking.
15 *
16 * <p>This object contains a reference to all benchmark instances
17 * that are currently executing in the VM, referenced by <code>benchId</code>.
18 * It is encouraged that each individual request has a unique benchId, although
19 * this is not required unless you need to use the {@link #getBenchmark} method.
20 *
21 * (if multiple instances are created using the same <code>benchId</code>,
22 * only the most recent instance will be retrievable via getBenchmark()).
23 *
24 * <p>Multiple checkpoints can be set during the course of a benchmark, which
25 * will be written to disk after completion.
26 *
27 * <p>Output is generated only after the benchmark has completed. All output is
28 * buffered, and references to any open benchmark files are kept within this
29 * object in order to minimise the amount of overhead that benchmarking will
30 * impose.
31 *
32 * <p>This class does not expire benchmarks, and therefore may cause memory leaks
33 * if benchmarks are not closed correctly.
34 *
35 * <p>Output is of the form:
36 * <pre style="code">
37 * benchId1,begin,timestamp,pointID1,timestamp1,pointID1,timestamp2,[...],end,timestampn,duration
38 * benchId2,begin,timestamp,pointID1,timestamp1,pointID1,timestamp2,[...],end,timestampn,duration
39 * :
40 * :
41 * </pre>
42 *
43 * <p>where <i>benchId</i> is the benchId indentifying this benchmark, and
44 * <i>pointIDn</i> are the individual checkpoint identifiers. Each line finishes
45 * with the text ",duration," followed by the duration of the entire benchmark, in
46 * milliseconds. Timestamps are displayed in the format defined by the
47 * {@link #setDateFormat(String)} method.
48 *
49 * <p>There is no current way of following a benchmark through the EJB boundary layer;
50 * to do so would currently involve overriding the User object, which I'm not terribly keen to do.
51 * It is possible to set up a new Benchmark on the EJB side, and track performance
52 * independently over there. It should then be a simple matter to heuristically match up
53 * individual HTTP requests/struts actions and the EJB methods that they invoke
54 * (under normal load conditions). It's important, however, that two Benchmark engines in the
55 * same VM do not write to the same file, as they will corrupt each other's output.
56 *
57 * @author knoxg
58 *
59 */
60 public class Benchmark {
61
62 /** A Comparator which performs comparisons between two Benchmarks.
63 */
64 public static class BenchmarkComparator implements Comparator<Benchmark> {
65
66 /** Create a new BenchmarkComparator object */
67 public BenchmarkComparator() {
68 }
69
70 /** Compare two structured list elements
71 *
72 * @see java.util.Comparator#compare(java.lang.Object, java.lang.Object)
73 */
74 public int compare(Benchmark a, Benchmark b) throws IllegalArgumentException {
75 Benchmark benchmarkA = (Benchmark) a;
76 Benchmark benchmarkB = (Benchmark) b;
77
78 return benchmarkA.startTime < benchmarkB.startTime ? -1 :
79 (benchmarkA.startTime > benchmarkB.startTime ? 1 : 0);
80 }
81 }
82
83 /** A class containing the checkpoints for a single benchmark */
84 public class CheckpointList {
85 /** List of Checkpoints, containing the checkpoints for a single
86 * benchmark */
87 List<Checkpoint> list = new ArrayList<Checkpoint>();
88
89 /** Return an iterator for the checkpoint list */
90 public Iterator<Checkpoint> iterator() {
91 return list.iterator();
92 }
93
94 /** Retrieve an individual checkpoint */
95 public Checkpoint get(int n) {
96 return (Checkpoint) list.get(n);
97 }
98
99 /** 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 }