benchmark
Version:
A benchmarking library that works on nearly all JavaScript platforms, supports high-resolution timers, and returns statistically significant results.
372 lines (318 loc) • 11 kB
JavaScript
(function(window, document) {
/** Cache used by various methods */
var cache = {
'counter': 0,
'lastAction': 'load',
'timer': null,
'trash': createElement('div')
},
interpolate = Benchmark.interpolate;
/*--------------------------------------------------------------------------*/
/**
* Registers an event listener.
* @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) {
if (typeof element.addEventListener != 'undefined') {
element.addEventListener(eventName, handler, false);
} else if (element.attachEvent != 'undefined') {
element.attachEvent('on' + eventName, handler);
}
}
/**
* 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);
}
/**
* Creates a Browserscope results object.
* @private
* @param {Array|Object} [benches=ui.benchmarks] One or an array of benchmarks.
* @returns {Object|Null} Browserscope results object or null.
*/
function createSnapshot(benches) {
if (!Benchmark.isArray(benches)) {
benches = benches ? [benches] : ui.benchmarks;
}
// remove unrun, errored, or Infinity hz
benches = Benchmark.filter(benches, function(bench) {
return bench.cycles && isFinite(bench.hz);
});
// clone benchmarks using lower limit of the confidence interval
benches = Benchmark.map(benches, function(bench) {
var clone = bench.clone();
clone.hz = Math.round(bench.hz - bench.stats.MoE);
return clone;
});
return Benchmark.reduce(benches, function(result, bench, key) {
// duplicate and non alphanumeric benchmark names have their ids appended
key = (bench.name.match(/[a-z0-9]+/ig) || []).join(' ');
result || (result = { });
result[key && !result[key] ? key : key + bench.id ] = bench.hz;
return result;
}, null);
}
/**
* Injects a script into the document.
* @private
* @param {String} src The external script source.
* @param {Object} sibling The element to inject the script after.
* @returns {Object} The new script element.
*/
function loadScript(src, sibling) {
var script = createElement('script'),
nextSibling = sibling ? sibling.nextSibling : query('script').pop();
script.src = src;
return (sibling || nextSibling).parentNode.insertBefore(script, nextSibling);
}
/**
* Returns the number of milliseconds elapsed since 1 January 1970 00:00:00 UTC.
* @private
* @returns {Number} Number of milliseconds.
*/
function now() {
return +new Date;
}
/**
* Queries the document for elements by id or tagName.
* @private
* @param {String} selector The css selector to match.
* @returns {Array} The array of results.
*/
function query(selector) {
var i = -1,
result = [],
nodes = /^#/.test(selector)
? [document.getElementById(selector.slice(1))]
: document.getElementsByTagName(selector);
while (result[++i] = nodes[i]) { }
return result.length-- && result;
}
/**
* Set an element's innerHTML property.
* @private
* @param {Object} element The element.
* @param {String} html The HTML to set.
* @param {Object} object The template object used to modify the html.
*/
function setHTML(element, html, object) {
element.innerHTML = interpolate(html, object);
}
/**
* Displays a message in the "results" element.
* @private
* @param {String} text The text to display.
*/
function setMessage(text) {
var me = ui.browserscope,
cont = me.container;
if (cont) {
cont.className = 'bs-rt-message';
cont.innerHTML = text;
}
}
/*--------------------------------------------------------------------------*/
/**
* Periodically executed callback that removes injected script and iframe elements.
* @private
*/
function onCleanup() {
var expire,
name,
me = ui.browserscope,
delay = me.CLEANUP_INTERVAL * 1e3,
trash = cache.trash;
// remove injected scripts and old iframes
if (onCleanup.id) {
Benchmark.each(query('script').concat(query('iframe')), function(element) {
// check if element is expired
name = element.name;
expire = +(/^browserscope-\d+-(\d+)$/.exec(name) || 0)[1] +
Math.max(delay, me.REQUEST_TIMEOUT * 1e3);
// destroy the element to prevent pseudo memory leaks.
// http://dl.dropbox.com/u/513327/removechild_ie_leak.html
if (now() > expire || /browserscope\.org/.test(element.src)) {
trash.appendChild(element);
trash.innerHTML = '';
}
});
}
// schedule another round
onCleanup.id = setTimeout(onCleanup, delay);
}
/**
* The window load event handler used to initialize the results table.
* @private
*/
function onLoad() {
var cont,
me = ui.browserscope,
key = me.KEY,
placeholder = query(me.PLACEHOLDER_SELECTOR)[0];
if (placeholder) {
// set html of placeholder using a template
setHTML(placeholder,
'<h1 id=bs-logo><a href=//www.browserscope.org/user/tests/table/#{key}>' +
'<span>Browserscope<\/span><\/a><\/h1>' +
'<div class=bs-rt><div id=bs-rt-usertest_#{key}><\/div><\/div>',
{
'key': key
}
);
// resolve container
cont = me.container = query('#bs-rt-usertest_' + key)[0];
// load ua script
loadScript('//www.browserscope.org/ua?o=js', cont).id = 'bs-ua-script';
// load google visualization script
// http://code.google.com/apis/loader/autoloader-wizard.html
loadScript('//www.google.com/jsapi?autoload=%7B%22modules%22%3A%5B%7B%22name%22%3A%22visualization%22%2C%22version%22%3A%221%22%2C%22packages%22%3A%5B%22table%22%5D%2C%22callback%22%3Aui.browserscope.load%7D%5D%7D');
}
// init garbage collector
onCleanup();
}
/*--------------------------------------------------------------------------*/
/**
* Loads Browserscope's cumulative results table.
* @static
* @member ui.browserscope
*/
function load() {
var me = ui.browserscope,
cont = me.container;
function onComplete(response) {
clearTimeout(cache.timer);
me.render(response);
}
// start timer
clearTimeout(cache.timer);
cache.timer = setTimeout(onComplete, me.REQUEST_TIMEOUT * 1e3);
// request data
if (cont) {
setMessage(me.LOADING_TEXT);
cache.lastAction = 'load';
(new google.visualization.Query(
'//www.browserscope.org/gviz_table_data?' +
'category=usertest_' + me.KEY + '&' +
'ua=&' +
'v=3&' +
'o=gviz_data&' +
'highlight=&' +
'score=&' +
'tqx=reqId:0&' +
'rid=' + now(),
{'sendMethod': 'scriptInjection'}))
.send(onComplete);
}
}
/**
* Creates a Browserscope beacon and posts the benchmark results.
* @static
* @member ui.browserscope
* @param {Array|Object} [benches=ui.benchmarks] One or an array of benchmarks.
*/
function post(benches) {
var idoc,
iframe,
body = document.body,
me = ui.browserscope,
key = me.KEY,
name = 'browserscope-' + (cache.counter++) + '-' + now(),
snapshot = createSnapshot(benches);
if (key && snapshot) {
// create new beacon
try {
iframe = createElement('<iframe name=' + name + '>');
} catch(e) {
(iframe = createElement('iframe')).name = name;
}
// inject beacon
body.insertBefore(iframe, body.firstChild);
idoc = frames[name].document;
iframe.style.display = 'none';
// expose results snapshot
me.snapshot = snapshot;
setMessage(me.POST_TEXT);
cache.lastAction = 'post';
// perform inception :3
idoc.write(interpolate(
'<html><body><script>' +
'with(parent.ui.browserscope){' +
'var _bTestResults=snapshot,' +
'_bC=function(){clearTimeout(_bT);parent.setTimeout(load,#{refresh}*1e3)},' +
'_bT=setTimeout(function(){document.body.innerHTML=render()},#{timeout}*1e3)' +
'}<\/script>' +
'<script src=//www.browserscope.org/user/beacon/#{key}?callback=_bC><\/script>' +
'<\/body><\/html>',
{
'key': key,
'refresh': me.REFRESH_DELAY,
'timeout': me.REQUEST_TIMEOUT
}
));
idoc.close();
}
}
/**
* Renders the cumulative results table.
* @static
* @member ui.browserscope
* @param {Object} response The data object.
*/
function render(response) {
var me = this,
cont = me.container;
// visualization table options
// http://code.google.com/apis/visualization/documentation/gallery/table.html
if (cont) {
if (response && !response.isError()) {
cont.className = '';
(new google.visualization.Table(cont)).draw(response.getDataTable(), {
'width': 'auto',
'height': 'auto',
'alternatingRowStyle': false
});
}
else {
setMessage(me.ERROR_TEXT);
setTimeout(me[cache.lastAction], me.RETRY_INTERVAL * 1e3);
}
}
}
/*--------------------------------------------------------------------------*/
// expose
ui.browserscope = {
/** Your Browserscope API key */
'KEY': '',
/** Selector of the element used for displaying the cumulative results table */
'PLACEHOLDER_SELECTOR': '',
/** The delay between removing abandoned script and iframe elements (secs) */
'CLEANUP_INTERVAL': 10,
/** The delay before refreshing the cumulative results after posting (secs) */
'REFRESH_DELAY': 3,
/** The time to wait for a request to finish (secs) */
'REQUEST_TIMEOUT': 10,
/** The delay between load attempts (secs) */
'RETRY_INTERVAL': 5,
/** Text shown when the cumulative results data cannot be retrieved */
'ERROR_TEXT': 'The get/post request has failed :(',
/** Text shown while waiting for the cumulative results data to load */
'LOADING_TEXT': 'Loading cumulative results data…',
/** Text shown while posting the results snapshot to Browserscope */
'POST_TEXT': 'Posting results snapshot…',
// loads cumulative results table
'load': load,
// posts benchmark snapshot to Browserscope
'post': post,
// renders cumulative results table
'render': render
};
// create results table chrome
addListener(window, 'load', onLoad);
}(this, document));