forked from mirror/Riven
629 lines
15 KiB
JavaScript
629 lines
15 KiB
JavaScript
// jslitmus.js
|
|
//
|
|
// Copyright (c) 2010, Robert Kieffer, http://broofa.com
|
|
// Available under MIT license (http://en.wikipedia.org/wiki/MIT_License)
|
|
|
|
(function() {
|
|
var root = this;
|
|
|
|
//
|
|
// Platform detect
|
|
//
|
|
|
|
var platform = (function() {
|
|
// Platform info object
|
|
var p = {
|
|
name: null,
|
|
version: null,
|
|
os: null,
|
|
description: 'unknown platform',
|
|
toString: function() {return this.description;}
|
|
};
|
|
|
|
if (root.navigator) {
|
|
var ua = navigator.userAgent;
|
|
|
|
// Detect OS
|
|
var oses = 'Windows|iPhone OS|(?:Intel |PPC )?Mac OS X|Linux';
|
|
p.os = new RegExp('((' + oses + ') +[^ \);]*)').test(ua) ? RegExp.$1.replace(/_/g, '.') : null;
|
|
|
|
// Detect expected names
|
|
p.name = /(Chrome|MSIE|Safari|Opera|Firefox|Minefield)/.test(ua) ? RegExp.$1 : null;
|
|
|
|
// Detect version
|
|
if (p.name == 'Opera') {
|
|
p.version = opera.name;
|
|
} else if (p.name) {
|
|
var vre = new RegExp('(Version|' + p.name + ')[ \/]([^ ;]*)');
|
|
p.version = vre.test(ua) ? RegExp.$2 : null;
|
|
}
|
|
} else if (root.process && process.platform) {
|
|
// Support node.js (see http://nodejs.org)
|
|
p.name = 'node';
|
|
p.version = process.version;
|
|
p.os = process.platform;
|
|
}
|
|
|
|
// Set the description
|
|
var d = [];
|
|
if (p.name) d.push(p.name);
|
|
if (p.version) d.push(' ' + p.version);
|
|
if (p.os) d.push(' on ' + p.os);
|
|
if (d.length) p.description = d.join('');
|
|
|
|
return p;
|
|
})();
|
|
|
|
//
|
|
// Context-specific initialization
|
|
//
|
|
|
|
var sys = null, querystring = null;
|
|
if (platform.name == 'node') {
|
|
util = require('util');
|
|
querystring = require('querystring');
|
|
}
|
|
|
|
//
|
|
// Misc convenience methods
|
|
//
|
|
|
|
function log(msg) {
|
|
if (typeof(console) != 'undefined') {
|
|
console.log(msg);
|
|
} else if (sys) {
|
|
util.log(msg);
|
|
}
|
|
}
|
|
|
|
// nil function
|
|
function nilf(x) {
|
|
return x;
|
|
}
|
|
|
|
// Copy properties
|
|
function extend(dst, src) {
|
|
for (var k in src) {
|
|
dst[k] = src[k];
|
|
}
|
|
return dst;
|
|
}
|
|
|
|
// Array: apply f to each item in a
|
|
function forEach(a, f) {
|
|
for (var i = 0, il = (a && a.length); i < il; i++) {
|
|
var o = a[i];
|
|
f(o, i);
|
|
}
|
|
}
|
|
|
|
// Array: return array of all results of f(item)
|
|
function map(a, f) {
|
|
var o, res = [];
|
|
for (var i = 0, il = (a && a.length); i < il; i++) {
|
|
var o = a[i];
|
|
res.push(f(o, i));
|
|
}
|
|
return res;
|
|
}
|
|
|
|
// Array: filter out items for which f(item) is falsy
|
|
function filter(a, f) {
|
|
var o, res = [];
|
|
for (var i = 0, il = (a && a.length); i < il; i++) {
|
|
var o = a[i];
|
|
if (f(o, i)) res.push(o);
|
|
}
|
|
return res;
|
|
}
|
|
|
|
// Array: IE doesn't have indexOf in some cases
|
|
function indexOf(a, o) {
|
|
if (a.indexOf) return a.indexOf(o);
|
|
for (var i = 0, l = a.length; i < l; i++) if (a[i] === o) return i;
|
|
return -1;
|
|
}
|
|
|
|
// Enhanced escape()
|
|
function escape2(s) {
|
|
s = s.replace(/,/g, '\\,');
|
|
s = querystring ? querystring.escape(s) : escape(s);
|
|
s = s.replace(/\+/g, '%2b');
|
|
s = s.replace(/ /g, '+');
|
|
return s;
|
|
}
|
|
|
|
// join(), for objects. Creates url query param-style strings by default
|
|
function join(o, delimit1, delimit2) {
|
|
var asQuery = !delimit1 && !delimit2;
|
|
if (asQuery) {
|
|
delimit1 = '&';
|
|
delimit2 = '=';
|
|
}
|
|
|
|
var pairs = [];
|
|
for (var key in o) {
|
|
var value = o[key];
|
|
if (asQuery) value = escape2(value);
|
|
pairs.push(key + delimit2 + o[key]);
|
|
}
|
|
return pairs.join(delimit1);
|
|
}
|
|
|
|
// split(), for object strings. Parses url query param strings by default
|
|
function split(s, delimit1, delimit2) {
|
|
var asQuery = !delimit1 && !delimit2;
|
|
if (asQuery) {
|
|
s = s.replace(/.*[?#]/, '');
|
|
delimit1 = '&';
|
|
delimit2 = '=';
|
|
}
|
|
|
|
if (match) {
|
|
var o = query.split(delimit1);
|
|
for (var i = 0; i < o.length; i++) {
|
|
var pair = o[i].split(new RegExp(delimit2 + '+'));
|
|
var key = pair.shift();
|
|
var value = (asQuery && pair.length > 1) ? pair.join(delimit2) : pair[0];
|
|
o[key] = value;
|
|
}
|
|
}
|
|
|
|
return o;
|
|
}
|
|
|
|
// Round x to d significant digits
|
|
function sig(x, d) {
|
|
var exp = Math.ceil(Math.log(Math.abs(x))/Math.log(10)),
|
|
f = Math.pow(10, exp-d);
|
|
return Math.round(x/f)*f;
|
|
}
|
|
|
|
// Convert x to a readable string version
|
|
function humanize(x, sd) {
|
|
var ax = Math.abs(x), res;
|
|
sd = sd | 4; // significant digits
|
|
if (ax == Infinity) {
|
|
res = ax > 0 ? 'Infinity' : '-Infinity';
|
|
} else if (ax > 1e9) {
|
|
res = sig(x/1e9, sd) + 'G';
|
|
} else if (ax > 1e6) {
|
|
res = sig(x/1e6, sd) + 'M';
|
|
} else if (ax > 1e3) {
|
|
res = sig(x/1e3, sd) + 'k';
|
|
} else if (ax > .01) {
|
|
res = sig(x, sd);
|
|
} else if (ax > 1e-3) {
|
|
res = sig(x/1e-3, sd) + 'm';
|
|
} else if (ax > 1e-6) {
|
|
res = sig(x/1e-6, sd) + '\u00b5'; // Greek mu
|
|
} else if (ax > 1e-9) {
|
|
res = sig(x/1e-9, sd) + 'n';
|
|
} else {
|
|
res = x ? sig(x, sd) : 0;
|
|
}
|
|
// Turn values like "1.1000000000005" -> "1.1"
|
|
res = (res + '').replace(/0{5,}\d*/, '');
|
|
|
|
return res;
|
|
}
|
|
|
|
// Node.js-inspired event emitter API, with some enhancements.
|
|
function EventEmitter() {
|
|
var ee = this;
|
|
var listeners = {};
|
|
extend(ee, {
|
|
on: function(e, f) {
|
|
if (!listeners[e]) listeners[e] = [];
|
|
listeners[e].push(f);
|
|
},
|
|
removeListener: function(e, f) {
|
|
listeners[e] = filter(listeners[e], function(l) {
|
|
return l != f;
|
|
});
|
|
},
|
|
removeAllListeners: function(e) {
|
|
listeners[e] = [];
|
|
},
|
|
emit: function(e) {
|
|
var args = Array.prototype.slice.call(arguments, 1);
|
|
forEach([].concat(listeners[e], listeners['*']), function(l) {
|
|
ee._emitting = e;
|
|
if (l) l.apply(ee, args);
|
|
});
|
|
delete ee._emitting;
|
|
}
|
|
});
|
|
}
|
|
|
|
//
|
|
// Test class
|
|
//
|
|
|
|
/**
|
|
* Test manages a single test (created with JSLitmus.test())
|
|
*/
|
|
function Test(name, f) {
|
|
var test = this;
|
|
|
|
// Test instances get EventEmitter API
|
|
EventEmitter.call(test);
|
|
|
|
if (!f) throw new Error('Undefined test function');
|
|
if (!/function[^\(]*\(([^,\)]*)/.test(f)) {
|
|
throw new Error('"' + name + '" test: Invalid test function');
|
|
}
|
|
|
|
// If the test function takes an argument, we assume it does the iteration
|
|
// for us
|
|
var isLoop = !!RegExp.$1;
|
|
|
|
/**
|
|
* Reset test state
|
|
*/
|
|
function reset() {
|
|
delete test.count;
|
|
delete test.time;
|
|
delete test.running;
|
|
test.emit('reset', test);
|
|
return test;
|
|
}
|
|
|
|
function clone() {
|
|
var test = extend(new Test(name, f), test);
|
|
return test.reset();
|
|
}
|
|
|
|
/**
|
|
* Run the test n times, and use the best results
|
|
*/
|
|
function bestOf(n) {
|
|
var best = null;
|
|
while (n--) {
|
|
var t = clone();
|
|
t.run(null, true);
|
|
if (!best || t.period < best.period) {
|
|
best = t;
|
|
}
|
|
}
|
|
extend(test, best);
|
|
}
|
|
|
|
/**
|
|
* Start running a test. Default is to run the test asynchronously (via
|
|
* setTimeout). Can be made synchronous by passing true for 2nd param
|
|
*/
|
|
function run(count, synchronous) {
|
|
count = count || test.INIT_COUNT;
|
|
test.running = true;
|
|
|
|
if (synchronous) {
|
|
_run(count, synchronous);
|
|
} else {
|
|
setTimeout(function() {
|
|
_run(count);
|
|
}, 1);
|
|
}
|
|
return test;
|
|
}
|
|
|
|
/**
|
|
* Run, for real
|
|
*/
|
|
function _run(count, noTimeout) {
|
|
|
|
try {
|
|
var start, f = test.f, now, i = count;
|
|
|
|
// Start the timer
|
|
start = new Date();
|
|
|
|
// Run the test code
|
|
test.count = count;
|
|
test.time = 0;
|
|
test.period = 0;
|
|
|
|
test.emit('start', test);
|
|
|
|
if (isLoop) {
|
|
// Test code does it's own iteration
|
|
f(count);
|
|
} else {
|
|
// Do the iteration ourselves
|
|
while (i--) f();
|
|
}
|
|
|
|
// Get time test took (in secs)
|
|
test.time = Math.max(1,new Date() - start)/1000;
|
|
|
|
// Store iteration count and per-operation time taken
|
|
test.count = count;
|
|
test.period = test.time/count;
|
|
|
|
// Do we need to keep running?
|
|
test.running = test.time < test.MIN_TIME;
|
|
|
|
// Publish results
|
|
test.emit('results', test);
|
|
|
|
// Set up for another run, if needed
|
|
if (test.running) {
|
|
// Use an iteration count that will (we hope) get us close to the
|
|
// MAX_COUNT time.
|
|
var x = test.MIN_TIME/test.time;
|
|
var pow = Math.pow(2, Math.max(1, Math.ceil(Math.log(x)/Math.log(2))));
|
|
count *= pow;
|
|
if (count > test.MAX_COUNT) {
|
|
throw new Error('Max count exceeded. If this test uses a looping function, make sure the iteration loop is working properly.');
|
|
}
|
|
|
|
if (noTimeout) {
|
|
_run(count, noTimeout);
|
|
} else {
|
|
run(count);
|
|
}
|
|
} else {
|
|
test.emit('complete', test);
|
|
}
|
|
} catch (err) {
|
|
log(err);
|
|
// Exceptions are caught and displayed in the test UI
|
|
test.emit('error', err);
|
|
}
|
|
|
|
return test;
|
|
}
|
|
|
|
/**
|
|
* Get the number of operations per second for this test.
|
|
*
|
|
* @param normalize if true, iteration loop overhead taken into account.
|
|
* Note that normalized tests may return Infinity if the
|
|
* test time is of the same order as the calibration time.
|
|
*/
|
|
function getHz(normalize) {
|
|
var p = test.period;
|
|
|
|
// Adjust period based on the calibration test time
|
|
if (normalize) {
|
|
var cal = test.isLoop ? Test.LOOP_CAL : Test.NOLOOP_CAL;
|
|
if (!cal.period) {
|
|
// Run calibration if needed
|
|
cal.MIN_TIME = .3;
|
|
cal.bestOf(3);
|
|
}
|
|
|
|
// Subtract off calibration time. In theory this should never be
|
|
// negative, but in practice the calibration times are affected by a
|
|
// variety of factors so just clip to zero and let users test for
|
|
// getHz() == Infinity
|
|
p = Math.max(0, p - cal.period);
|
|
}
|
|
|
|
return sig(1/p, 4);
|
|
}
|
|
|
|
// Set properties that are specific to this instance
|
|
extend(test, {
|
|
// Test name
|
|
name: name,
|
|
|
|
// Test function
|
|
f: f,
|
|
|
|
// True if the test function does it's own looping (i.e. takes an arg)
|
|
isLoop: isLoop,
|
|
|
|
clone: clone,
|
|
run: run,
|
|
bestOf: bestOf,
|
|
getHz: getHz,
|
|
reset: reset
|
|
});
|
|
|
|
// IE7 doesn't do 'toString' or 'toValue' in object enumerations, so set
|
|
// it explicitely here.
|
|
test.toString = function() {
|
|
if (this.time) {
|
|
return this.name + ', f = ' +
|
|
humanize(this.getHz()) + 'hz (' +
|
|
humanize(this.count) + '/' + humanize(this.time) + 'secs)';
|
|
} else {
|
|
return this.name + ', count = ' + humanize(this.count);
|
|
}
|
|
};
|
|
};
|
|
|
|
// Set static properties
|
|
extend(Test, {
|
|
LOOP_CAL: new Test('loop cal', function(count) {while (count--) {}}),
|
|
NOLOOP_CAL: new Test('noloop cal', nilf)
|
|
});
|
|
|
|
// Set default property values
|
|
extend(Test.prototype, {
|
|
// Initial number of iterations
|
|
INIT_COUNT: 10,
|
|
|
|
// Max iterations allowed (used to detect bad looping functions)
|
|
MAX_COUNT: 1e9,
|
|
|
|
// Minimum time test should take to get valid results (secs)
|
|
MIN_TIME: 1.0
|
|
});
|
|
|
|
//
|
|
// jslitmus
|
|
//
|
|
|
|
// Set up jslitmus context
|
|
var jslitmus;
|
|
if (platform.name == 'node') {
|
|
jslitmus = exports;
|
|
} else {
|
|
jslitmus = root.jslitmus = {};
|
|
}
|
|
|
|
var tests = [], // test store (all tests added w/ jslitmus.test())
|
|
queue = [], // test queue (to be run)
|
|
currentTest; // currently running test
|
|
|
|
// jslitmus gets EventEmitter API
|
|
EventEmitter.call(jslitmus);
|
|
|
|
/**
|
|
* Create a new test
|
|
*/
|
|
function test(name, f) {
|
|
// Create the Test object
|
|
var test = new Test(name, f);
|
|
tests.push(test);
|
|
|
|
// Run the next test if this one finished
|
|
test.on('*', function() {
|
|
// Forward test events to jslitmus listeners
|
|
var args = Array.prototype.slice.call(arguments);
|
|
args.unshift(test._emitting);
|
|
jslitmus.emit.apply(jslitmus, args);
|
|
|
|
// Auto-run the next test
|
|
if (test._emitting == 'complete') {
|
|
currentTest = null;
|
|
_nextTest();
|
|
}
|
|
});
|
|
|
|
jslitmus.emit('added', test);
|
|
|
|
return test;
|
|
}
|
|
|
|
/**
|
|
* Add all tests to the run queue
|
|
*/
|
|
function runAll(e) {
|
|
forEach(tests, _queueTest);
|
|
}
|
|
|
|
/**
|
|
* Remove all tests from the run queue. The current test has to finish on
|
|
* it's own though
|
|
*/
|
|
function stop() {
|
|
while (queue.length) {
|
|
var test = queue.shift();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Run the next test in the run queue
|
|
*/
|
|
function _nextTest() {
|
|
if (!currentTest) {
|
|
var test = queue.shift();
|
|
if (test) {
|
|
currentTest = test;
|
|
test.run();
|
|
} else {
|
|
jslitmus.emit('all_complete');
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add a test to the run queue
|
|
*/
|
|
function _queueTest(test) {
|
|
if (indexOf(queue, test) >= 0) return;
|
|
queue.push(test);
|
|
_nextTest();
|
|
}
|
|
|
|
function clearAll() {
|
|
tests = [];
|
|
}
|
|
|
|
/**
|
|
* Generate a Google Chart URL that shows the data for all tests
|
|
*/
|
|
function getGoogleChart(normalize) {
|
|
var chart_title = [
|
|
'Operations/second on ' + platform.name,
|
|
'(' + platform.version + ' / ' + platform.os + ')'
|
|
];
|
|
|
|
var n = tests.length, markers = [], data = [];
|
|
var d, min = 0, max = -1e10;
|
|
|
|
// Gather test data
|
|
|
|
var markers = map(tests, function(test, i) {
|
|
if (test.count) {
|
|
var hz = test.getHz();
|
|
var v = hz != Infinity ? hz : 0;
|
|
data.push(v);
|
|
var label = test.name + '(' + humanize(hz)+ ')';
|
|
var marker = 't' + escape2(label) + ',000000,0,' + i + ',10';
|
|
max = Math.max(v, max);
|
|
|
|
return marker;
|
|
}
|
|
});
|
|
|
|
if (markers.length <= 0) return null;
|
|
|
|
// Build labels
|
|
var labels = [humanize(min), humanize(max)];
|
|
|
|
var w = 250, bw = 15;
|
|
var bs = 5;
|
|
var h = markers.length*(bw + bs) + 30 + chart_title.length*20;
|
|
|
|
var params = {
|
|
chtt: escape(chart_title.join('|')),
|
|
chts: '000000,10',
|
|
cht: 'bhg', // chart type
|
|
chd: 't:' + data.join(','), // data set
|
|
chds: min + ',' + max, // max/min of data
|
|
chxt: 'x', // label axes
|
|
chxl: '0:|' + labels.join('|'), // labels
|
|
chsp: '0,1',
|
|
chm: markers.join('|'), // test names
|
|
chbh: [bw, 0, bs].join(','), // bar widths
|
|
// chf: 'bg,lg,0,eeeeee,0,eeeeee,.5,ffffff,1', // gradient
|
|
chs: w + 'x' + h
|
|
};
|
|
|
|
var url = 'http://chart.apis.google.com/chart?' + join(params);
|
|
|
|
return url;
|
|
}
|
|
|
|
// Public API
|
|
extend(jslitmus, {
|
|
Test: Test,
|
|
platform: platform,
|
|
test: test,
|
|
runAll: runAll,
|
|
getGoogleChart: getGoogleChart,
|
|
clearAll: clearAll
|
|
});
|
|
|
|
// Expose code goodness we've got here, since it's useful, but do so in a way
|
|
// that doesn't commit us to supporting it in future versions.
|
|
jslitmus.unsupported = {
|
|
nilf: nilf,
|
|
log: log,
|
|
extend: extend,
|
|
forEach: forEach,
|
|
filter: filter,
|
|
map: map,
|
|
indexOf: indexOf,
|
|
escape2: escape2,
|
|
join: join,
|
|
split: split,
|
|
sig: sig,
|
|
humanize: humanize
|
|
};
|
|
})();
|