ExpressoTestCenter/qa/mail/Desempenho: JSLitmus.js

File JSLitmus.js, 18.6 KB (added by amuller, 15 years ago)
Line 
1// JSLitmus.js
2//
3// History:
4//   2008-10-27: Initial release
5//   2008-11-09: Account for iteration loop overhead
6//   2008-11-13: Added OS detection
7//
8// Copyright (c) 2008, Robert Kieffer
9// All Rights Reserved
10//
11// Permission is hereby granted, free of charge, to any person obtaining a copy
12// of this software and associated documentation files (the
13// Software), to deal in the Software without restriction, including
14// without limitation the rights to use, copy, modify, merge, publish,
15// distribute, sublicense, and/or sell copies of the Software, and to permit
16// persons to whom the Software is furnished to do so, subject to the
17// following conditions:
18//
19// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND,
20// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
21// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
22// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
23// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
24// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
25// USE OR OTHER DEALINGS IN THE SOFTWARE.
26
27(function() {
28  // Private methods and state
29
30  // Get platform info but don't go crazy trying to everything that's out
31  // there.  This is just for the major platforms and OSes.
32  var platform = 'unknown platform', ua = navigator.userAgent;
33
34  // Detect OS
35  var oses = ['Windows','iPhone OS','(Intel |PPC )?Mac OS X','Linux'].join('|');
36  var pOS = new RegExp('((' + oses + ') [^ \);]*)').test(ua) ? RegExp.$1 : null;
37  if (!pOS) pOS = new RegExp('((' + oses + ')[^ \);]*)').test(ua) ? RegExp.$1 : null;
38
39  // Detect browser
40  var pName = /(Chrome|MSIE|Safari|Opera|Firefox)/.test(ua) ? RegExp.$1 : null;
41
42  // Detect version
43  var vre = new RegExp('(Version|' + pName + ')[ \/]([^ ;]*)');
44  var pVersion = (pName && vre.test(ua)) ? RegExp.$2 : null;
45  var platform = (pOS && pName && pVersion) ? pName + ' '  + pVersion + ' on ' + pOS : 'unknown platform';
46
47  /* Enhanced version of escape() */
48  var urlEscape = function(s) {
49    return escape(s).replace(/\+/g, '%2b');
50  };
51
52  /* Find an element by id */
53  var $ = function(id) {return document.getElementById(id)};
54
55  /* Show a status message */
56  var status = function(msg) {
57    var el = $('jsl_status');
58    if (el) el.innerHTML = msg || '';
59  }
60
61  /* Convert a number to an abbreviated string like, "15K" or "10M" */
62  var toLabel = function(n) {
63    if (n == Infinity) {
64      return 'Infinity';
65    } else if (n > 1e9) {
66      n = Math.round(n/1e8);
67      return n/10 + 'B';
68    } else if (n > 1e6) {
69      n = Math.round(n/1e5);
70      return n/10 + 'M';
71    } else if (n > 1e3) {
72      n = Math.round(n/1e2);
73      return n/10 + 'K';
74    }
75    return n;
76  };
77
78  /* Copy properties from src to dst */
79  var objectExtend = function(dst, src) {
80    for (var k in src) dst[k] = src[k]; return dst;
81  };
82
83  /* Like Array.join(), but for the key-value pairs in an object */
84  var objectJoin = function(o, delimit1, delimit2) {
85    var pairs = [];
86    for (var k in o) pairs.push(k + delimit1 + o[k]);
87    return pairs.join(delimit2);
88  };
89
90  // IE workaround - monkey patch Array.indexOf() if it's not defined
91  if (!Array.prototype.indexOf) {
92    Array.prototype.indexOf = function(o) {
93      for (var i = 0; i < this.length; i++) if (this[i] === o) return i;
94      return -1;
95    }
96  }
97
98  /**
99   * (private) Test manages a single test (created with
100   * JSLitmus.test())
101   */
102  var Test = function (name, f) {
103    if (!f || !/function[^\(]*\(([^,\)]*)/.test(f.toString())) {
104      throw new Error('"' + name + '" test: Test is not a valid Function object');
105    }
106    this.loopArg = RegExp.$1;
107    this.name = name;
108    this.f = f;
109  }
110 
111  //
112  // Test - static members
113  //
114  objectExtend(Test, {
115    // Calibration tests for establishing iteration loop overhead
116    CALIBRATIONS: [
117      new Test('empty (looping)', function(count) {while (count--);}),
118      new Test('empty (non-looping)', function() {})
119    ],
120
121    /*
122     * Run calibration tests.  Returns true if calibrations are not yet
123     * complete (in which calling code should run the tests yet).
124     * onCalibrated - Callback to invoke when calibrations have finished
125     */
126    calibrate: function(onCalibrated) {
127      for (var i = 0; i < Test.CALIBRATIONS.length; i++) {
128        var cal = Test.CALIBRATIONS[i];
129        if (cal.running) return true;
130        if (!cal.count) {
131          cal.isCalibration = true;
132          cal.onStop = onCalibrated;
133          //cal.MIN_TIME = .1; // Do calibrations quickly
134          cal.run(2e4);
135          return true;
136        }
137      }
138      return false;
139    }
140  });
141
142  // Test instance members
143  objectExtend(Test.prototype, {
144    INIT_COUNT: 10,     // Initial number of iterations
145    MAX_COUNT: 1e9,     // Max iterations allowed (i.e. used to detect bad looping functions)
146    MIN_TIME: .5,       // Minimum time a test should take to get valid results (secs).
147
148    /** Called when the test state changes */
149    onChange: function() {},
150
151    /** Called when the test is finished */
152    onStop: function() {},
153
154    /**
155     * Reset test state
156     */
157    reset: function() {
158      delete this.count;
159      delete this.time;
160      delete this.running;
161      delete this.error;
162    },
163
164    /**
165     * Public(ish) method for running the test. We call the actual run method,
166     * _run(), in a timeout to make sure the browser has a chance to finish
167     * rendering any UI changes we've made, like updating the status message.
168     */
169    run: function(count) {
170      count = count || this.INIT_COUNT
171      status('Testing ' + this.name + ' x ' + count);
172      this.running = true;
173      var me = this;
174      setTimeout(function() {me._run(count);}, 200);
175    },
176
177    /**
178     * Run the test
179     */
180    _run: function(count) {
181      var me = this;
182
183      // Make sure calibration tests have run
184      if (!me.isCalibration && Test.calibrate(function() {me.run(count);})) return;
185      this.error = null;
186
187      try {
188        var start, f = this.f, now, i = count;
189
190        // Start the timer
191        start = new Date();
192
193        // Now for the money shot.  If this is a looping function ...
194        if (this.loopArg) {
195          // ... let it do the iteration itself
196          f(count);
197        } else {
198          // ... otherwise do the iteration for it
199          while (i--) f();
200        }
201
202        // Get time test took (in secs)
203        this.time = Math.max(1,new Date() - start)/1000;
204
205        // Store iteration count and per-operation time taken
206        this.count = count;
207        this.period = this.time/count;
208
209        // Do we need to do another run?
210        this.running = this.time <= this.MIN_TIME;
211
212        // ... if so, compute how many times we should iterate
213        if (this.running) {
214          // Bump the count to the nearest power of 2
215          var x = this.MIN_TIME/this.time;
216          var pow = Math.pow(2, Math.max(1, Math.ceil(Math.log(x)/Math.log(2))));
217          count *= pow;
218          if (count > this.MAX_COUNT) {
219            throw new Error('Max count exceeded.  If this test uses a looping function, make sure the iteration loop is working properly.')
220          }
221        }
222      } catch (e) {
223        // Exceptions are caught and displayed in the test UI
224        this.reset();
225        this.error = e;
226      }
227
228      // Figure out what to do next
229      if (this.running) {
230        me.run(count);
231      } else {
232        status('');
233        me.onStop(me);
234      }
235
236      // Finish up
237      this.onChange(this);
238    },
239
240    /**
241     * Get the number of operations per second for this test.
242     * normalize - if true, iteration loop overhead taken into account
243     */
244    getHz: function(normalize) {
245      var p = this.period;
246
247      // Adjust period based on the calibration test time
248      if (normalize && !this.isCalibration) {
249        var cal = Test.CALIBRATIONS[this.loopArg ? 0 : 1];
250
251        // If the period is within 20% of the calibration time, then zero the
252        // it out
253        p = p < cal.period*1.2 ? 0 : p - cal.period;
254      }
255
256      return Math.round(1/p);
257    },
258
259    toString: function() {
260      return this.name + ' - '  + this.time/this.count + ' secs';
261    }
262  });
263
264  // CSS we need for the UI
265  var STYLESHEET = '<style> \
266    #jslitmus {font-family:sans-serif; font-size: 12px;} \
267    #jslitmus a {text-decoration: none;} \
268    #jslitmus a:hover {text-decoration: underline;} \
269    #jsl_status { \
270      margin-top: 10px; \
271      font-size: 10px; \
272      color: #888; \
273    } \
274    A IMG  {border:none} \
275    #test_results { \
276      margin-top: 10px; \
277      font-size: 12px; \
278      font-family: sans-serif; \
279      border-collapse: collapse; \
280      border-spacing: 0px; \
281    } \
282    #test_results th, #test_results td { \
283      border: solid 1px #ccc; \
284      vertical-align: top; \
285      padding: 3px; \
286    } \
287    #test_results th { \
288      vertical-align: bottom; \
289      background-color: #ccc; \
290      padding: 1px; \
291      font-size: 10px; \
292    } \
293    #test_results #test_platform { \
294      color: #444; \
295      text-align:center; \
296    } \
297    #test_results .test_row { \
298      color: #006; \
299      cursor: pointer; \
300    } \
301    #test_results .test_nonlooping { \
302      border-left-style: dotted; \
303      border-left-width: 2px; \
304    } \
305    #test_results .test_looping { \
306      border-left-style: solid; \
307      border-left-width: 2px; \
308    } \
309    #test_results .test_name {white-space: nowrap;} \
310    #test_results .test_pending { \
311    } \
312    #test_results .test_running { \
313      font-style: italic; \
314    } \
315    #test_results .test_done {} \
316    #test_results .test_done { \
317      text-align: right; \
318      font-family: monospace; \
319    } \
320    #test_results .test_error {color: #600;} \
321    #test_results .test_error .error_head {font-weight:bold;} \
322    #test_results .test_error .error_body {font-size:85%;} \
323    #test_results .test_row:hover td { \
324      background-color: #ffc; \
325      text-decoration: underline; \
326    } \
327    #chart { \
328      margin-top: 10px; \
329    } \
330    #chart img { \
331      border: solid 1px #ccc; \
332      margin-bottom: 5px; \
333    } \
334    #chart #tiny_url { \
335      float: left; \
336      font-size: 10px; \
337    } \
338    #jslitmus_credit { \
339      font-size: 10px; \
340      color: #888; \
341      display:block; \
342      text-align:center; \
343      float: right; \
344    } \
345    </style>';
346
347  // HTML markup for the UI
348  var MARKUP = '<div id="jslitmus"> \
349      <button onclick="JSLitmus.runAll()">Run Tests</button> \
350      <button id="stop_button" disabled="disabled" onclick="JSLitmus.stop()">Stop Tests</button> \
351      <br \> \
352      <br \> \
353      <input type="checkbox" style="vertical-align: middle" id="test_normalize" checked="checked" onchange="JSLitmus.renderAll()""> Normalize results \
354      <table id="test_results"> \
355        <colgroup> \
356          <col /> \
357          <col width="100" /> \
358        </colgroup> \
359        <tr><th id="test_platform" colspan="2">' + platform + '</th></tr> \
360        <tr><th>Test</th><th>Ops/sec</th></tr> \
361        <tr id="test_row_template" class="test_row" style="display:none"> \
362          <td class="test_name"></td> \
363          <td class="test_result">Ready</td> \
364        </tr> \
365      </table> \
366      <div id="jsl_status"></div> \
367      <div id="chart" style="display:none"> \
368      <a id="chart_link" target="_blank"><img id="chart_image"></a> \
369      <a id="jslitmus_credit" title="Check out the JSLitmus home page" href="http://broofa.com/Tools/JSLitmus" target="_blank">powered by JSLitmus</a> \
370      <a id="tiny_url" href="" title="Get a compact url for this chart" target="_blank">chart\'s tinyurl</a> \
371      </div> \
372    </div>';
373
374  /**
375   * JSLitmus API
376   */
377  window.JSLitmus = {
378    _tests: [],
379    _queue: [],
380
381    params: {},
382
383    /**
384     * Initialize
385     */
386    _init: function() {
387      // Parse query params into JSLitmus.params[] hash
388      var match = (location + '').match(/([^?#]*)(#.*)?$/);
389      if (match) {
390        var pairs = match[1].split('&');
391        for (var i = 0; i < pairs.length; i++) {
392          var pair = pairs[i].split('=');
393          if (pair.length > 1) {
394            var key = pair.shift();
395            var value = pair.length > 1 ? pair.join('=') : pair[0];
396            this.params[key] = value;
397          }
398        }
399      }
400
401      // Write out the stylesheet.  We have to do this here because IE
402      // doesn't honor sheets written after the document has loaded.
403      document.write(STYLESHEET);
404
405      // Setup the rest of the UI once the document is loaded
406      if (window.addEventListener) {
407        window.addEventListener('load', this._setup, false);
408      } else if (document.addEventListener) {
409        document.addEventListener('load', this._setup, false);
410      } else if (window.attachEvent) {
411        window.attachEvent('onload', this._setup);
412      }
413
414
415      return this;
416    },
417
418    /**
419     * Set up the UI
420     */
421    _setup: function() {
422      var el = $('jslitmus_container');
423      if (!el) document.body.appendChild(el = document.createElement('div'));
424
425      el.innerHTML = MARKUP;
426
427      // Render the UI for all our tests
428      for (var i=0; i < JSLitmus._tests.length; i++)
429        JSLitmus.renderTest(JSLitmus._tests[i]);
430    },
431
432    /**
433     * (Re)render all the test results
434     */
435    renderAll: function() {
436      for (var i = 0; i < JSLitmus._tests.length; i++)
437        JSLitmus.renderTest(JSLitmus._tests[i]);
438      JSLitmus.renderChart();
439    },
440
441    /**
442     * (Re)render the chart graphics
443     */
444    renderChart: function() {
445      var url = JSLitmus.chartUrl();
446      $('chart_link').href = url;
447      $('chart_image').src = url;
448      $('tiny_url').href = 'http://tinyurl.com/create.php?url='+encodeURIComponent(url)
449      $('chart').style.display = '';
450    },
451
452    /**
453     * (Re)redner the results for a specific test
454     */
455    renderTest: function(test) {
456      // Make a new row if needed
457      if (!test._row) {
458        var trow = $('test_row_template');
459        if (!trow) return;
460
461        test._row = trow.cloneNode(true);
462        test._row.style.display = '';
463        test._row.id = '';
464        test._row.onclick = function() {JSLitmus._queueTest(test);};
465        test._row.title = 'Run ' + test.name + ' test';
466        trow.parentNode.appendChild(test._row);
467        test._row.cells[0].innerHTML = test.name;
468      }
469
470      var cell = test._row.cells[1];
471      var cns = [test.loopArg ? 'test_looping' : 'test_nonlooping'];
472
473      if (test.error) {
474        cns.push('test_error');
475        cell.innerHTML =
476        '<div class="error_head">' + test.error + '</div>' +
477        '<ul class="error_body"><li>' +
478          objectJoin(test.error, ': ', '</li><li>') +
479          '</li></ul>';
480      } else {
481        if (test.running) {
482          cns.push('test_running');
483          cell.innerHTML = 'running';
484        } else if (JSLitmus._queue.indexOf(test) >= 0) {
485          cns.push('test_pending');
486          cell.innerHTML = 'pending';
487        } else if (test.count) {
488          cns.push('test_done');
489          var hz = test.getHz($('test_normalize').checked);
490          cell.innerHTML = hz != Infinity ? hz : '&infin;';
491        } else {
492          cell.innerHTML = 'ready';
493        }
494      }
495      cell.className = cns.join(' ');
496    },
497
498    /**
499     * Create a new test
500     */
501    test: function(name, f) {
502      // Create the Test object
503      var test = new Test(name, f);
504      JSLitmus._tests.push(test);
505
506      // Re-render if the test state changes
507      test.onChange = JSLitmus.renderTest;
508
509      // Run the next test if this one finished
510      test.onStop = function(test) {
511        if (JSLitmus.onTestFinish) JSLitmus.onTestFinish(test);
512        JSLitmus.currentTest = null;
513        JSLitmus._nextTest();
514      }
515
516      // Render the new test
517      this.renderTest(test);
518    },
519
520    /**
521     * Add all tests to the run queue
522     */
523    runAll: function() {
524      for (var i =0; i < JSLitmus._tests.length; i++)
525        JSLitmus._queueTest(JSLitmus._tests[i]);
526    },
527
528    /**
529     * Remove all tests from the run queue.  The current test has to finish on
530     * it's own though
531     */
532    stop: function() {
533      while (JSLitmus._queue.length) {
534        var test = JSLitmus._queue.shift();
535        JSLitmus.renderTest(test);
536      }
537    },
538
539    /**
540     * Run the next test in the run queue
541     */
542    _nextTest: function() {
543      if (!JSLitmus.currentTest) {
544        var test = JSLitmus._queue.shift();
545        if (test) {
546          $('stop_button').disabled = false;
547          JSLitmus.currentTest = test;
548          test.run();
549          JSLitmus.renderTest(test);
550          if (JSLitmus.onTestStart) JSLitmus.onTestStart(test);
551        } else {
552          $('stop_button').disabled = true;
553          JSLitmus.renderChart();
554        }
555      }
556    },
557
558    /**
559     * Add a test to the run queue
560     */
561    _queueTest: function(test) {
562      if (JSLitmus._queue.indexOf(test) >= 0) return;
563      JSLitmus._queue.push(test);
564      JSLitmus.renderTest(test);
565      JSLitmus._nextTest();
566    },
567
568    /**
569     * Generate a Google Chart URL that shows the data for all tests
570     */
571    chartUrl: function() {
572      var n = JSLitmus._tests.length, markers = [], data = [];
573      var d, min = 0, max = -1e10;
574      var normalize = $('test_normalize').checked;
575
576      // Gather test data
577      for (var i=0; i < JSLitmus._tests.length; i++) {
578        var test = JSLitmus._tests[i];
579        if (test.count) {
580          var hz = test.getHz(normalize);
581          var v = hz != Infinity ? hz : 0;
582          data.push(v);
583          markers.push('t' + urlEscape(test.name + '(' + toLabel(hz)+ ')') + ',000000,0,' +
584            markers.length + ',10');
585          max = Math.max(v, max);
586        }
587      }
588      if (markers.length <= 0) return null;
589
590      // Build chart title
591      var title = document.getElementsByTagName('title');
592      title = (title && title.length) ? title[0].innerHTML : null;
593      var chart_title = [];
594      if (title) chart_title.push(title);
595      chart_title.push('Ops/sec (' + platform + ')');
596
597      // Build labels
598      var labels = [toLabel(min), toLabel(max)];
599
600      var w = 250, bw = 15;
601      var bs = 5;
602      var h = markers.length*(bw + bs) + 30 + chart_title.length*20;
603
604      var params = {
605        chtt: escape(chart_title.join('|')),
606        chts: '000000,10',
607        cht: 'bhg',                     // chart type
608        chd: 't:' + data.join(','),     // data set
609        chds: min + ',' + max,          // max/min of data
610        chxt: 'x',                      // label axes
611        chxl: '0:|' + labels.join('|'), // labels
612        chsp: '0,1',
613        chm: markers.join('|'),         // test names
614        chbh: [bw, 0, bs].join(','),    // bar widths
615        // chf: 'bg,lg,0,eeeeee,0,eeeeee,.5,ffffff,1', // gradient
616        chs: w + 'x' + h
617      }
618      return 'http://chart.apis.google.com/chart?' + objectJoin(params, '=', '&');
619    }
620  };
621
622  JSLitmus._init();
623})();