View Javadoc
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 }