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}