topical media & game development
mobile-query-three-bench-benchmark.js-examples-jsperf-ui.js / js
/*!
* 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) {
Java applet archive path
var archive = '../../nano.jar',
Google Analytics account id
gaId = '',
Benchmark results element id prefix (e.g. `results-1`)
prefix = 'results-',
Object containing various css class names
classNames = {
CSS class name used for error styles
'error': 'error',
CSS class name used to make content visible
'show': 'show',
CSS class name used to reset result styles
'results': 'results'
},
Object containing various text messages
texts = {
Inner text for the various run button states
'run': {
'ready': 'Run tests',
'again': 'Run again',
'running': 'Stop running'
},
Common status values
'status': {
'ready': 'Ready to run.',
'again': 'Done. Ready to run again.'
}
},
Used to flag environments/features
has = {
// used for pre-populating form fields
'localStorage': !!function() {
try {
return !localStorage.getItem(+new Date);
} catch(e) { }
}(),
// detects Opera Mini
'operaMini': platform.name == 'Opera Mini',
// used to distinguish between a regular test page and an embedded chart
'runner': !!$('runner')
},
Cache of error messages
errors = [],
Cache of event handlers
handlers = {},
A flag to indicate that the page has loaded
pageLoaded = false,
The options object for Benchmark.Suite#run
runOptions = { 'async': !has.operaMini, 'queued': true },
The element responsible for scrolling the page (assumes ui.js is just before </body>)
scrollEl = document.body,
Used to resolve a value's internal [[Class]]
toString = {}.toString,
Namespace
ui = new Benchmark.Suite,
API Shortcuts
each = Benchmark.each,
filter = Benchmark.filter,
forOwn = Benchmark.forOwn,
formatNumber = Benchmark.formatNumber,
indexOf = Benchmark.indexOf,
invoke = Benchmark.invoke,
join = Benchmark.join;
/*--------------------------------------------------------------------------*/
handlers.benchmark = {
The onCycle callback, used for onStart as well, assigned to new benchmarks.
@private
'cycle': function() {
var bench = this,
size = bench.stats.size;
if (!bench.aborted && !has.operaMini) {
setStatus(bench.name + ' × ' + formatNumber(bench.count) + ' (' +
size + ' sample' + (size == 1 ? '' : 's') + ')');
}
},
The onStart callback assigned to new benchmarks.
@private
'start': function() {
// call user provided init() function
if (isFunction(window.init)) {
init();
}
}
};
handlers.button = {
The "run" button click event handler used to run or abort the benchmarks.
@private
'run': function() {
var stopped = !ui.running;
ui.abort();
ui.length = 0;
if (stopped) {
logError({ 'clear': true });
ui.push.apply(ui, filter(ui.benchmarks, function(bench) {
return !bench.error && bench.reset();
}));
ui.run(runOptions);
}
}
};
handlers.title = {
The title table cell click event handler used to run the corresponding benchmark.
@private
parameter: {Object} event The event object.
'click': function(event) {
event || (event = window.event);
var id,
index,
target = event.target || event.srcElement;
while (target && !(id = target.id)) {
target = target.parentNode;
}
index = id && --id.split('-')[1] || 0;
ui.push(ui.benchmarks[index].reset());
ui.running ? ui.render(index) : ui.run(runOptions);
},
The title cell keyup event handler used to simulate a mouse click when hitting the ENTER key.
@private
parameter: {Object} event The event object.
'keyup': function(event) {
if (13 == (event || window.event).keyCode) {
handlers.title.click(event);
}
}
};
handlers.window = {
The window hashchange event handler supported by Chrome 5+, Firefox 3.6+, and IE8+.
@private
'hashchange': function() {
ui.parseHash();
var scrollTop,
params = ui.params,
chart = params.chart,
filterBy = params.filterby;
if (pageLoaded) {
// configure posting
ui.browserscope.postable = has.runner && !('nopost' in params);
// configure chart renderer
if (chart || filterBy) {
scrollTop = $('results').offsetTop;
ui.browserscope.render({ 'chart': chart, 'filterBy': filterBy });
}
if (has.runner) {
// call user provided init() function
if (isFunction(window.init)) {
init();
}
// auto-run
if ('run' in params) {
scrollTop = $('runner').offsetTop;
setTimeout(handlers.button.run, 1);
}
// scroll to the relevant section
if (scrollTop) {
scrollEl.scrollTop = scrollTop;
}
}
}
},
The window load event handler used to initialize the UI.
@private
'load': function() {
// only for pages with a comment form
if (has.runner) {
// init the ui
addClass('controls', classNames.show);
addListener('run', 'click', handlers.button.run);
setHTML('run', texts.run.ready);
setHTML('user-agent', Benchmark.platform);
setStatus(texts.status.ready);
// answer spammer question
$('question').value = 'no';
// prefill author details
if (has.localStorage) {
each([$('author'), $('author-email'), $('author-url')], function(element) {
element.value = localStorage[element.id] || '';
element.oninput = element.onkeydown = function(event) {
event && event.type < 'k' && (element.onkeydown = null);
localStorage[element.id] = element.value;
};
});
}
// show warning when Firebug is enabled (avoids showing for Firebug Lite)
try {
// Firebug 1.9 no longer has `console.firebug`
if (console.firebug || /firebug/i.test(console.table())) {
addClass('firebug', classNames.show);
}
} catch(e) { }
}
// evaluate hash values
// (delay in an attempt to ensure this is executed after all other window load handlers)
setTimeout(function() {
pageLoaded = true;
handlers.window.hashchange();
}, 1);
}
};
/*--------------------------------------------------------------------------*/
Shortcut for document.getElementById().
@private
parameter: {Element|String} id The id of the element to retrieve.
returns: {Element} 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
parameter: {Element|String} element The element or id of the element.
parameter: {String} className The class name.
returns: {Element} The element.
function addClass(element, className) {
if ((element = element) && !hasClass(element, className)) {
element.className += (element.className ? ' ' : '') + className;
}
return element;
}
Registers an event listener on an element.
@private
parameter: {Element|String} element The element or id of the element.
parameter: {String} eventName The name of the event.
parameter: {Function} handler The event handler.
returns: {Element} The element.
function addListener(element, eventName, handler) {
if ((element = element)) {
if (typeof element.addEventListener != 'undefined') {
element.addEventListener(eventName, handler, false);
} else if (typeof element.attachEvent != 'undefined') {
element.attachEvent('on' + eventName, handler);
}
}
return element;
}
Appends to an element's innerHTML property.
@private
parameter: {Element|String} element The element or id of the element.
parameter: {String} html The HTML to append.
returns: {Element} The element.
function appendHTML(element, html) {
if ((element = element) && html != null) {
element.innerHTML += html;
}
return element;
}
Shortcut for document.createElement().
@private
parameter: {String} tag The tag name of the element to create.
returns: {Element} A new element of the given tag name.
function createElement(tagName) {
return document.createElement(tagName);
}
Checks if an element is assigned the given class name.
@private
parameter: {Element|String} element The element or id of the element.
parameter: {String} className The class name.
returns: {Boolean} If assigned the class name return true, else false.
function hasClass(element, className) {
return !!(element = element) &&
(' ' + element.className + ' ').indexOf(' ' + className + ' ') > -1;
}
Set an element's innerHTML property.
@private
parameter: {Element|String} element The element or id of the element.
parameter: {String} html The HTML to set.
returns: {Element} The element.
function setHTML(element, html) {
if ((element = element)) {
element.innerHTML = html == null ? '' : html;
}
return element;
}
/*--------------------------------------------------------------------------*/
Copies own/inherited properties of a source object to the destination object.
@private
parameter: {Object} destination The destination object.
parameter: {Object} [source={}] The source object.
returns: {Object} The destination object.
function extend(destination, source) {
source || (source = {});
for (var key in source) {
destination[key] = source[key];
}
return destination;
}
Checks if a value has an internal [[Class]] of Function.
@private
parameter: {Mixed} value The value to check.
returns: {Boolean} Returns `true` if the value is a function, else `false`.
function isFunction(value) {
return toString.call(value) == '[object Function]';
}
Appends to or clears the error log.
@private
parameter: {String|Object} text The text to append or options object.
function logError(text) {
var table,
div = $('error-info'),
options = {};
// juggle arguments
if (typeof text == 'object' && text) {
options = text;
text = options.text;
}
else if (arguments.length) {
options.text = text;
}
if (!div) {
table = $('test-table');
div = createElement('div');
div.id = 'error-info';
table.parentNode.insertBefore(div, table.nextSibling);
}
if (options.clear) {
div.className = div.innerHTML = '';
errors.length = 0;
}
if ('text' in options && indexOf(errors, text) < 0) {
errors.push(text);
addClass(div, classNames.show);
appendHTML(div, text);
}
}
Sets the status text.
@private
parameter: {String} text The text to write to the status.
function setStatus(text) {
setHTML('status', text);
}
/*--------------------------------------------------------------------------*/
Parses the window.location.hash value into an object assigned to `ui.params`.
@static
@memberOf ui
returns: {Object} The suite instance.
function parseHash() {
var me = this,
hashes = location.hash.slice(1).split('&'),
params = me.params || (me.params = {});
// remove old params
forOwn(params, function(value, key) {
delete params[key];
});
// add new params
each(hashes[0] && hashes, function(value) {
value = value.split('=');
params[value[0].toLowerCase()] = (value[1] || '').toLowerCase();
});
return me;
}
Renders the results table cell of the corresponding benchmark(s).
@static
@memberOf ui
parameter: {Number} [index] The index of the benchmark to render.
returns: {Object} The suite instance.
function render(index) {
each(index == null ? (index = 0, ui.benchmarks) : [ui.benchmarks[index]], function(bench) {
var parsed,
cell = $(prefix + (++index)),
error = bench.error,
hz = bench.hz;
// reset title and class
cell.title = '';
cell.className = classNames.results;
// status: error
if (error) {
setHTML(cell, 'Error');
addClass(cell, classNames.error);
parsed = join(error, '</li><li>');
logError('<p>' + error + '.</p>' + (parsed ? '<ul><li>' + parsed + '</li></ul>' : ''));
}
else {
// status: running
if (bench.running) {
setHTML(cell, 'running…');
}
// status: completed
else if (bench.cycles) {
// obscure details until the suite has completed
if (ui.running) {
setHTML(cell, 'completed');
}
else {
cell.title = 'Ran ' + formatNumber(bench.count) + ' times in ' +
bench.times.cycle.toFixed(3) + ' seconds.';
setHTML(cell, formatNumber(hz.toFixed(hz < 100 ? 2 : 0)) +
' <small>±' + bench.stats.rme.toFixed(2) + '%</small>');
}
}
else {
// status: pending
if (ui.running && ui.indexOf(bench) > -1) {
setHTML(cell, 'pending…');
}
// status: ready
else {
setHTML(cell, 'ready');
}
}
}
});
return ui;
}
/*--------------------------------------------------------------------------*/
ui.on('add', function(event, bench) {
var index = ui.benchmarks.length,
id = index + 1,
title = $('title-' + id);
delete ui[--ui.length];
ui.benchmarks.push(bench);
if (has.runner) {
title.tabIndex = 0;
title.title = 'Click to run this test again.';
addListener(title, 'click', handlers.title.click);
addListener(title, 'keyup', handlers.title.keyup);
bench.on('start', handlers.benchmark.start);
bench.on('start cycle', handlers.benchmark.cycle);
ui.render(index);
}
})
.on('start cycle', function() {
if (!has.operaMini) {
ui.render();
setHTML('run', texts.run.running);
}
})
.on('complete', function() {
var benches = filter(ui.benchmarks, 'successful'),
fastest = filter(benches, 'fastest'),
slowest = filter(benches, 'slowest');
ui.render();
setHTML('run', texts.run.again);
setStatus(texts.status.again);
// highlight result cells
each(benches, function(bench) {
var percent,
cell = $(prefix + (indexOf(ui.benchmarks, bench) + 1)),
hz = bench.hz,
span = cell.getElementsByTagName('span')[0],
text = 'fastest';
if (indexOf(fastest, bench) > -1) {
// mark fastest
addClass(cell, text);
}
else {
percent = Math.round((1 - hz / fastest[0].hz) * 100);
text = isFinite(hz) ? percent + '% slower' : '';
// mark slowest
if (indexOf(slowest, bench) > -1) {
addClass(cell, 'slowest');
}
}
// write ranking
if (span) {
setHTML(span, text);
} else {
appendHTML(cell, '<span>' + text + '</span>');
}
});
ui.browserscope.post();
});
/*--------------------------------------------------------------------------*/
An array of benchmarks created from test cases.
@memberOf ui
@type Array
ui.benchmarks = [];
The parsed query parameters of the pages url hash.
@memberOf ui
@type Object
ui.params = {};
// parse query params into ui.params hash
ui.parseHash = parseHash;
// (re)render the results of one or more benchmarks
ui.render = render;
/*--------------------------------------------------------------------------*/
// expose
window.ui = ui;
// don't let users alert, confirm, prompt, or open new windows
window.alert = window.confirm = window.prompt = window.open = function() { };
// parse hash query params when it changes
addListener(window, 'hashchange', handlers.window.hashchange);
// bootstrap onload
addListener(window, 'load', handlers.window.load);
// parse location hash string
ui.parseHash();
// provide a simple UI for toggling between chart types and filtering results
// (assumes ui.js is just before </body>)
(function() {
var sibling = $('bs-results'),
p = createElement('p');
p.innerHTML =
'<span id=charts><strong>Chart type:</strong> <a href=#>bar</a>, ' +
'<a href=#>column</a>, <a href=#>line</a>, <a href=#>pie</a>, ' +
'<a href=#>table</a></span><br>' +
'<span id=filters><strong>Filter:</strong> <a href=#>popular</a>, ' +
'<a href=#>all</a>, <a href=#>desktop</a>, <a href=#>family</a>, ' +
'<a href=#>major</a>, <a href=#>minor</a>, <a href=#>mobile</a>, ' +
'<a href=#>prerelease</a></span>';
sibling.parentNode.insertBefore(p, sibling);
// use DOM0 event handler to simplify canceling the default action
$('charts').onclick =
$('filters').onclick = function(event) {
event || (event = window.event);
var target = event.target || event.srcElement;
if (target.href || (target = target.parentNode).href) {
ui.browserscope.render(
target.parentNode.id == 'charts'
? { 'chart': target.innerHTML }
: { 'filterBy': target.innerHTML }
);
}
// cancel the default action
return false;
};
}());
/*--------------------------------------------------------------------------*/
// fork for runner or embedded chart
if (has.runner) {
// detect the scroll element
(function() {
var scrollTop,
div = document.createElement('div'),
body = document.body,
bodyStyle = body.style,
bodyHeight = bodyStyle.height,
html = document.documentElement,
htmlStyle = html.style,
htmlHeight = htmlStyle.height;
bodyStyle.height = htmlStyle.height = 'auto';
div.style.cssText = 'display:block;height:9001px;';
body.insertBefore(div, body.firstChild);
scrollTop = html.scrollTop;
// set `scrollEl` that's used in `handlers.window.hashchange()`
if (html.clientWidth !== 0 && ++html.scrollTop && html.scrollTop == scrollTop + 1) {
scrollEl = html;
}
body.removeChild(div);
bodyStyle.height = bodyHeight;
htmlStyle.height = htmlHeight;
html.scrollTop = scrollTop;
}());
// catch and display errors from the "preparation code"
window.onerror = function(message, fileName, lineNumber) {
logError('<p>' + message + '.</p><ul><li>' + join({
'message': message,
'fileName': fileName,
'lineNumber': lineNumber
}, '</li><li>') + '</li></ul>');
scrollEl.scrollTop = $('error-info').offsetTop;
};
// inject nano applet
// (assumes ui.js is just before </body>)
if ('nojava' in ui.params) {
addClass('java', classNames.show);
} else {
// using innerHTML avoids an alert in some versions of IE6
document.body.insertBefore(setHTML(createElement('div'),
'<applet code=nano archive=' + archive + '>').lastChild, document.body.firstChild);
}
}
else {
// short circuit unusable methods
ui.render = function() { };
ui.removeAllListeners('start cycle complete');
setTimeout(function() {
ui.removeAllListeners();
ui.browserscope.post = function() { };
invoke(ui.benchmarks, 'removeAllListeners');
}, 1);
}
/*--------------------------------------------------------------------------*/
// optimized asynchronous Google Analytics snippet based on
// http://mathiasbynens.be/notes/async-analytics-snippet
if (gaId) {
(function() {
var script = createElement('script'),
sibling = document.getElementsByTagName('script')[0];
window._gaq = [['_setAccount', gaId], ['_trackPageview']];
script.src = '//www.google-analytics.com/ga.js';
sibling.parentNode.insertBefore(script, sibling);
}());
}
}(this, document));
(C) Æliens
04/09/2009
You may not copy or print any of this material without explicit permission of the author or the publisher.
In case of other copyright issues, contact the author.