// JSLitmus.js // // History: // 2008-10-27: Initial release // 2008-11-09: Account for iteration loop overhead // 2008-11-13: Added OS detection // // Copyright (c) 2008, Robert Kieffer // All Rights Reserved // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the // Software), to deal in the Software without restriction, including // without limitation the rights to use, copy, modify, merge, publish, // distribute, sublicense, and/or sell copies of the Software, and to permit // persons to whom the Software is furnished to do so, subject to the // following conditions: // // THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN // NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE // USE OR OTHER DEALINGS IN THE SOFTWARE. (function() { // Private methods and state // Get platform info but don't go crazy trying to everything that's out // there. This is just for the major platforms and OSes. var platform = 'unknown platform', ua = navigator.userAgent; // Detect OS var oses = ['Windows','iPhone OS','(Intel |PPC )?Mac OS X','Linux'].join('|'); var pOS = new RegExp('((' + oses + ') [^ \);]*)').test(ua) ? RegExp.$1 : null; if (!pOS) pOS = new RegExp('((' + oses + ')[^ \);]*)').test(ua) ? RegExp.$1 : null; // Detect browser var pName = /(Chrome|MSIE|Safari|Opera|Firefox)/.test(ua) ? RegExp.$1 : null; // Detect version var vre = new RegExp('(Version|' + pName + ')[ \/]([^ ;]*)'); var pVersion = (pName && vre.test(ua)) ? RegExp.$2 : null; var platform = (pOS && pName && pVersion) ? pName + ' ' + pVersion + ' on ' + pOS : 'unknown platform'; /* Enhanced version of escape() */ var urlEscape = function(s) { return escape(s).replace(/\+/g, '%2b'); }; /* Find an element by id */ var $ = function(id) {return document.getElementById(id)}; /* Show a status message */ var status = function(msg) { var el = $('jsl_status'); if (el) el.innerHTML = msg || ''; } /* Convert a number to an abbreviated string like, "15K" or "10M" */ var toLabel = function(n) { if (n == Infinity) { return 'Infinity'; } else if (n > 1e9) { n = Math.round(n/1e8); return n/10 + 'B'; } else if (n > 1e6) { n = Math.round(n/1e5); return n/10 + 'M'; } else if (n > 1e3) { n = Math.round(n/1e2); return n/10 + 'K'; } return n; }; /* Copy properties from src to dst */ var objectExtend = function(dst, src) { for (var k in src) dst[k] = src[k]; return dst; }; /* Like Array.join(), but for the key-value pairs in an object */ var objectJoin = function(o, delimit1, delimit2) { var pairs = []; for (var k in o) pairs.push(k + delimit1 + o[k]); return pairs.join(delimit2); }; // IE workaround - monkey patch Array.indexOf() if it's not defined if (!Array.prototype.indexOf) { Array.prototype.indexOf = function(o) { for (var i = 0; i < this.length; i++) if (this[i] === o) return i; return -1; } } /** * (private) Test manages a single test (created with * JSLitmus.test()) */ var Test = function (name, f) { if (!f || !/function[^\(]*\(([^,\)]*)/.test(f.toString())) { throw new Error('"' + name + '" test: Test is not a valid Function object'); } this.loopArg = RegExp.$1; this.name = name; this.f = f; } // // Test - static members // objectExtend(Test, { // Calibration tests for establishing iteration loop overhead CALIBRATIONS: [ new Test('empty (looping)', function(count) {while (count--);}), new Test('empty (non-looping)', function() {}) ], /* * Run calibration tests. Returns true if calibrations are not yet * complete (in which calling code should run the tests yet). * onCalibrated - Callback to invoke when calibrations have finished */ calibrate: function(onCalibrated) { for (var i = 0; i < Test.CALIBRATIONS.length; i++) { var cal = Test.CALIBRATIONS[i]; if (cal.running) return true; if (!cal.count) { cal.isCalibration = true; cal.onStop = onCalibrated; //cal.MIN_TIME = .1; // Do calibrations quickly cal.run(2e4); return true; } } return false; } }); // Test instance members objectExtend(Test.prototype, { INIT_COUNT: 10, // Initial number of iterations MAX_COUNT: 1e9, // Max iterations allowed (i.e. used to detect bad looping functions) MIN_TIME: .5, // Minimum time a test should take to get valid results (secs). /** Called when the test state changes */ onChange: function() {}, /** Called when the test is finished */ onStop: function() {}, /** * Reset test state */ reset: function() { delete this.count; delete this.time; delete this.running; delete this.error; }, /** * Public(ish) method for running the test. We call the actual run method, * _run(), in a timeout to make sure the browser has a chance to finish * rendering any UI changes we've made, like updating the status message. */ run: function(count) { count = count || this.INIT_COUNT status('Testing ' + this.name + ' x ' + count); this.running = true; var me = this; setTimeout(function() {me._run(count);}, 200); }, /** * Run the test */ _run: function(count) { var me = this; // Make sure calibration tests have run if (!me.isCalibration && Test.calibrate(function() {me.run(count);})) return; this.error = null; try { var start, f = this.f, now, i = count; // Start the timer start = new Date(); // Now for the money shot. If this is a looping function ... if (this.loopArg) { // ... let it do the iteration itself f(count); } else { // ... otherwise do the iteration for it while (i--) f(); } // Get time test took (in secs) this.time = Math.max(1,new Date() - start)/1000; // Store iteration count and per-operation time taken this.count = count; this.period = this.time/count; // Do we need to do another run? this.running = this.time <= this.MIN_TIME; // ... if so, compute how many times we should iterate if (this.running) { // Bump the count to the nearest power of 2 var x = this.MIN_TIME/this.time; var pow = Math.pow(2, Math.max(1, Math.ceil(Math.log(x)/Math.log(2)))); count *= pow; if (count > this.MAX_COUNT) { throw new Error('Max count exceeded. If this test uses a looping function, make sure the iteration loop is working properly.') } } } catch (e) { // Exceptions are caught and displayed in the test UI this.reset(); this.error = e; } // Figure out what to do next if (this.running) { me.run(count); } else { status(''); me.onStop(me); } // Finish up this.onChange(this); }, /** * Get the number of operations per second for this test. * normalize - if true, iteration loop overhead taken into account */ getHz: function(normalize) { var p = this.period; // Adjust period based on the calibration test time if (normalize && !this.isCalibration) { var cal = Test.CALIBRATIONS[this.loopArg ? 0 : 1]; // If the period is within 20% of the calibration time, then zero the // it out p = p < cal.period*1.2 ? 0 : p - cal.period; } return Math.round(1/p); }, toString: function() { return this.name + ' - ' + this.time/this.count + ' secs'; } }); // CSS we need for the UI var STYLESHEET = ''; // HTML markup for the UI var MARKUP = '
' + platform + ' | |
---|---|
Test | Ops/sec |