benchmark
Version:
A benchmarking library that works on nearly all JavaScript platforms, supports high-resolution timers, and returns statistically significant results.
614 lines (525 loc) • 16.1 kB
JavaScript
/*!
* ui.js
* Copyright Mathias Bynens <http://mths.be/>
* Modified by John-David Dalton <http://allyoucanleet.com/>
* Available under MIT license <http://mths.be/mit>
*/
(function(window, document) {
/** CSS class name used for error styles */
var ERROR_CLASS = 'error',
/** CSS clasName used for `js enabled` styles */
JS_CLASS = 'js',
/** CSS class name used to display error-info */
SHOW_CLASS = 'show',
/** CSS class name used to reset result styles */
RESULTS_CLASS = 'results',
/** Google Analytics account id */
GA_ACCOUNT_ID = '',
/** Benchmark results element id prefix (e.g. `results-1`) */
RESULTS_PREFIX = 'results-',
/** Inner text for the various run button states */
RUN_TEXT = {
'READY': 'Run tests',
'READY_AGAIN': 'Run again',
'RUNNING': 'Stop running'
},
/** Common status values */
STATUS_TEXT = {
'READY': 'Ready to run.',
'READY_AGAIN': 'Done. Ready to run again.'
},
/** Cache of error messages */
cache = {
'errors': []
},
each = Benchmark.each,
formatNumber = Benchmark.formatNumber,
indexOf = Benchmark.indexOf,
invoke = Benchmark.invoke,
isArray = Benchmark.isArray;
/*--------------------------------------------------------------------------*/
/**
* Shortcut for document.getElementById().
* @private
* @param {String|Object} id The id of the element to retrieve.
* @returns {Object} The element, if found, or null.
*/
function $(id) {
return typeof id == 'string' ? document.getElementById(id) : id;
}
/**
* Adds a css class name to an element's className property.
* @private
* @param {Object} element The element.
* @param {String} className The class name.
*/
function addClass(element, className) {
if (!hasClass(element = $(element), className)) {
element.className += (element.className ? ' ' : '') + className;
}
}
/**
* Registers an event listener on an element.
* @private
* @param {Object} element The element.
* @param {String} eventName The name of the event to listen to.
* @param {Function} handler The event handler.
*/
function addListener(element, eventName, handler) {
element = $(element);
if (typeof element.addEventListener != 'undefined') {
element.addEventListener(eventName, handler, false);
} else if (element.attachEvent != 'undefined') {
element.attachEvent('on' + eventName, handler);
}
}
/**
* Appends to an element's innerHTML property.
* @private
* @param {Object} element The element.
* @param {String} html The HTML to append.
*/
function appendHTML(element, html) {
html != null && ($(element).innerHTML += html);
}
/**
* Shortcut for document.createElement().
* @private
* @param {String} tag The tag name of the element to create.
* @returns {Object} A new of the given tag name element.
*/
function createElement(tagName) {
return document.createElement(tagName);
}
/**
* Checks if an element is assigned the given class name.
* @private
* @param {Object} element The element.
* @param {String} className The class name.
* @returns {Boolean} If assigned the class name return true, else false.
*/
function hasClass(element, className) {
return (' ' + $(element).className + ' ').indexOf(' ' + className + ' ') > -1;
}
/**
* Appends to or clears the error log.
* @private
* @param {String|Boolean} text The the text to append or false to clear.
*/
function logError(text) {
var elTable,
elDiv = $('error-info');
if (!elDiv) {
elTable = $('test-table');
elDiv = createElement('div');
elDiv.id = 'error-info';
elTable.parentNode.insertBefore(elDiv, elTable.nextSibling);
}
if (text === false) {
elDiv.className = elDiv.innerHTML = '';
cache.errors.length = 0;
}
else {
if (indexOf(cache.errors, text) < 0) {
cache.errors.push(text);
addClass(elDiv, SHOW_CLASS);
appendHTML(elDiv, text);
}
}
}
/**
* Set an element's innerHTML property.
* @private
* @param {Object} element The element.
* @param {String} html The HTML to set.
*/
function setHTML(element, html) {
$(element).innerHTML = html == null ? '' : html;
}
/**
* Sets the status text.
* @private
* @param {String} text The text write to the status.
*/
function setStatus(text) {
setHTML('status', text);
}
/*--------------------------------------------------------------------------*/
/**
* The title table cell click event handler used to run the corresponding benchmark.
* @private
* @param {Object} e The event object.
*/
function onClick(e) {
e || (e = window.event);
var id = (e.target || e.srcElement).id.split('-')[1];
each(ui.benchmarks, function(bench) {
if (bench.id == id) {
ui.run(bench);
return false;
}
});
}
/**
* The onComplete callback assigned to new benchmarks.
* @private
*/
function onComplete() {
setStatus(STATUS_TEXT.READY_AGAIN);
ui.render(this);
}
/**
* The onCycle callback, used for onStart as well, assigned to new benchmarks.
* @private
*/
function onCycle() {
var bench = this,
cycles = bench.cycles;
if (!bench.aborted) {
setStatus(bench.name + ' × ' + formatNumber(bench.count) + ' (' +
cycles + ' cycle' + (cycles == 1 ? '' : 's') + ')');
}
}
/**
* The window hashchange event handler supported by Chrome 5+, Firefox 3.6+, and IE8+.
* @private
*/
function onHashChange() {
var params = ui.params;
ui.parseHash();
// call user provided init() function
if (typeof window.init == 'function') {
init();
}
// auto-run
if ('run' in params) {
onRun();
}
// clear stored results
if ('clear' in params) {
Benchmark.clearStorage();
}
}
/**
* The title cell keyup event handler used to simulate a mouse click when hitting the ENTER key.
* @private
* @param {Object} e The event object.
*/
function onKeyUp(e) {
if (13 == (e || window.event).keyCode) {
onClick.call(this);
}
}
/**
* The window load event handler used to initialize the UI.
* @private
*/
function onLoad() {
var hash = location.hash.slice(1, 4);
addClass('controls', 'show');
addClass('calgroup', 'show');
addListener('calibrate', 'click', onSetCalibrationState);
addListener('run', 'click', onRun);
setHTML('run', RUN_TEXT.READY);
setHTML('user-agent', Benchmark.platform);
setStatus(STATUS_TEXT.READY);
// enable calibrations by default
$('calibrate').checked = true;
// answer spammer question
$('question').value = 'no';
// show warning and disable calibrations when Firebug is enabled
if (typeof window.console != 'undefined' && typeof console.firebug == 'string') {
$('calibrate').checked = false;
onSetCalibrationState();
addClass('controls', 'reduce');
addClass('firebug', 'show');
}
// evaluate hash values
onHashChange();
}
/**
* The callback fired when the run queue is complete.
* @private
*/
function onQueueComplete() {
var benches = Benchmark.filter(ui.benchmarks, 'successful'),
fastest = Benchmark.filter(benches, 'fastest'),
slowest = Benchmark.filter(benches, 'slowest');
// display contextual information
each(benches, function(bench) {
var percent,
text = 'fastest',
elResult = $(RESULTS_PREFIX + bench.id),
elSpan = elResult.getElementsByTagName('span')[0];
if (indexOf(fastest, bench) > -1) {
// mark fastest
addClass(elResult, text);
}
else {
percent = Math.floor((1 - bench.hz / fastest[0].hz) * 100);
text = percent + '% slower';
// mark slowest
if (indexOf(slowest, bench) > -1) {
addClass(elResult, 'slowest');
}
}
// write ranking
if (elSpan) {
setHTML(elSpan, text);
} else {
appendHTML(elResult, '<span>' + text + '<\/span>');
}
});
// post results to Browserscope
if ($('calibrate').checked) {
ui.browserscope.post();
}
// all benchmarks are finished
ui.running = false;
setHTML('run', RUN_TEXT.READY_AGAIN);
}
/**
* The "run" button click event handler used to run or abort the benchmarks.
* @private
* @param {Object} e The event object.
*/
function onRun(e) {
var benches = ui.benchmarks,
run = $('run').innerHTML != RUN_TEXT.RUNNING;
ui.abort();
if (run) {
logError(false);
ui.run((e || window.event).shiftKey ? benches.slice(0).reverse() : benches);
}
}
/**
* The "calibrate" checkbox click event handler used to enable/disable calibrations.
* @private
*/
function onSetCalibrationState() {
var length = Benchmark.CALIBRATIONS.length;
if ($('calibrate').checked) {
if (!length) {
Benchmark.CALIBRATIONS = cache.CALIBRATIONS;
}
} else if (length) {
cache.CALIBRATIONS = [Benchmark.CALIBRATIONS, Benchmark.CALIBRATIONS = []][0];
}
}
/*--------------------------------------------------------------------------*/
/**
* Aborts any running benchmarks, clears the run queue, and renders results.
* @static
* @member ui
*/
function abort() {
var me = this,
benches = me.benchmarks;
me.queue.length = 0;
invoke(benches.concat(Benchmark.CALIBRATIONS), 'abort');
me.render(benches);
setHTML('run', RUN_TEXT.READY);
}
/**
* Adds a benchmark the the collection.
* @static
* @member ui
* @param {String} name The name assigned to the benchmark.
* @param {String} id The id assigned to the benchmark.
* @param {Function} fn The function to benchmark.
*/
function addTest(name, id, fn) {
var me = this,
elTitle = $('title-' + id),
bench = new Benchmark(fn, {
'id': id,
'name': name,
'onStart': onCycle,
'onCycle': onCycle,
'onComplete': onComplete
});
elTitle.tabIndex = 0;
elTitle.title = 'Click to run this test again.';
addListener(elTitle, 'click', onClick);
addListener(elTitle, 'keyup', onKeyUp);
me.benchmarks.push(bench);
me.elResults.push($(RESULTS_PREFIX + id));
me.render(bench);
}
/**
* Parses the window.location.hash value into an object assigned to `ui.params`.
* @static
* @member ui
*/
function parseHash() {
var hashes = location.hash.slice(1).split('&'),
params = this.params = { };
each(hashes[0] && hashes, function(hash) {
var pair = hashes[length].split('=');
params[pair[0]] = pair[1];
});
}
/**
* Renders the results table cell of the corresponding benchmark(s).
* @static
* @member ui
* @param {Array|Object} [benches=ui.benchmarks] One or an array of benchmarks.
*/
function render(benches) {
var me = this;
if (!isArray(benches)) {
benches = benches ? [benches] : me.benchmarks;
}
each(benches, function(bench) {
var cell = $(RESULTS_PREFIX + bench.id),
error = bench.error,
hz = bench.hz;
if (cell) {
cell.title = '';
// status: error
if (error) {
setHTML(cell, 'Error');
if (!hasClass(cell, ERROR_CLASS)) {
addClass(cell, ERROR_CLASS);
}
logError('<p>' + error + '.<\/p><ul><li>' +
Benchmark.join(error, '<\/li><li>') + '<\/li><\/ul>');
}
else {
// status: running
if (bench.running) {
setHTML(cell, 'running…');
}
// status: finished
else if (bench.cycles) {
cell.title = 'Ran ' + formatNumber(bench.count) + ' times in ' +
bench.times.cycle.toFixed(3) + ' seconds.';
setHTML(cell, hz == Infinity ? hz :
formatNumber(hz) + ' <small>±' + bench.stats.RME.toFixed(2) + '%<\/small>');
}
// status: pending
else if (indexOf(me.queue, bench) > -1) {
setHTML(cell, 'pending…');
}
// status: ready
else {
setHTML(cell, 'ready');
}
}
}
});
}
/**
* Adds given benchmark(s) to the queue and runs.
* @static
* @member ui
* @param {Array|Object} [benches=ui.benchmarks] One or an array of benchmarks.
*/
function run(benches) {
var added,
me = this,
queue = me.queue;
if (!isArray(benches)) {
benches = benches ? [benches] : me.benchmarks;
}
each(benches, function(bench) {
if (!bench.error && indexOf(queue, bench) < 0) {
// reset before (re)running so the previous run's results are
// not re-recorded if the operation is aborted mid queue
bench.reset();
queue.push(bench);
me.render(bench);
added = true;
if (!me.running) {
me.running = true;
setHTML('run', RUN_TEXT.RUNNING);
invoke(queue, {
'name': 'run',
'queued': true,
'onComplete': onQueueComplete
});
}
}
});
// reset result classNames
if (added) {
each(me.elResults, function(elResult) {
if (!hasClass(elResult, ERROR_CLASS)) {
elResult.className = RESULTS_CLASS;
}
});
}
}
/*--------------------------------------------------------------------------*/
// expose
window.ui = {
/**
* An array of table cells used to display benchmark results.
* @member ui
*/
'elResults': [],
/**
* The parsed query parameters of the pages url hash.
* @member ui
*/
'params': {},
/**
* The queue of benchmarks to run.
* @member ui
*/
'queue': [],
/**
* A flag to indicate if the benchmarks are running.
* @member ui
*/
'running': false,
/**
* An array of benchmarks created from test cases.
* @member ui
*/
'benchmarks': [],
// abort, dequeue, and render results
'abort': abort,
// create a new benchmark from a test case
'addTest': addTest,
// parse query params into ui.params[] hash
'parseHash': parseHash,
// (re)render the results of one or more benchmarks
'render': render,
// add one or more benchmarks to the queue and run
'run': run
};
/*--------------------------------------------------------------------------*/
// signal JavaScript detected
addClass(document.documentElement, JS_CLASS);
// don't let users alert, confirm, prompt, or open new windows
window.alert = window.confirm = window.prompt = window.open = Benchmark.noop;
// re-parse hash query params when it changes
addListener(window, 'hashchange', onHashChange);
// bootstrap onload
addListener(window, 'load', onLoad);
// parse location hash string
ui.parseHash();
// force benchmarks to run asynchronously for a more responsive UI
Benchmark.prototype.DEFAULT_ASYNC = true;
// customize calibration benchmarks
each(Benchmark.CALIBRATIONS, function(cal) {
cal.name = 'Calibrating';
cal.on('complete', onComplete)
.on('cycle', onCycle)
.on('start', onCycle);
});
// optimized asynchronous Google Analytics snippet based on
// http://mathiasbynens.be/notes/async-analytics-snippet
if (GA_ACCOUNT_ID) {
(function() {
var tag = 'script',
script = createElement(tag),
sibling = document.getElementsByTagName(tag)[0];
window._gaq = [['_setAccount', GA_ACCOUNT_ID], ['_trackPageview']];
script.async = 1;
script.src = '//www.google-analytics.com/ga.js';
sibling.parentNode.insertBefore(script, sibling);
}());
}
}(this, document));