|
@@ -1,628 +0,0 @@
|
|
|
-// 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
|
|
|
- };
|
|
|
-})();
|