jslitmus.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670
  1. // JSLitmus.js
  2. //
  3. // History:
  4. // 2008-10-27: Initial release
  5. // 2008-11-09: Account for iteration loop overhead
  6. // 2008-11-13: Added OS detection
  7. // 2009-02-25: Create tinyURL automatically, shift-click runs tests in reverse
  8. //
  9. // Copyright (c) 2008-2009, Robert Kieffer
  10. // All Rights Reserved
  11. //
  12. // Permission is hereby granted, free of charge, to any person obtaining a copy
  13. // of this software and associated documentation files (the
  14. // Software), to deal in the Software without restriction, including
  15. // without limitation the rights to use, copy, modify, merge, publish,
  16. // distribute, sublicense, and/or sell copies of the Software, and to permit
  17. // persons to whom the Software is furnished to do so, subject to the
  18. // following conditions:
  19. //
  20. // THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND,
  21. // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
  22. // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
  23. // NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
  24. // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
  25. // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
  26. // USE OR OTHER DEALINGS IN THE SOFTWARE.
  27. (function() {
  28. // Private methods and state
  29. // Get platform info but don't go crazy trying to recognize everything
  30. // that's out there. This is just for the major platforms and OSes.
  31. var platform = 'unknown platform', ua = navigator.userAgent;
  32. // Detect OS
  33. var oses = ['Windows','iPhone OS','(Intel |PPC )?Mac OS X','Linux'].join('|');
  34. var pOS = new RegExp('((' + oses + ') [^ \);]*)').test(ua) ? RegExp.$1 : null;
  35. if (!pOS) pOS = new RegExp('((' + oses + ')[^ \);]*)').test(ua) ? RegExp.$1 : null;
  36. // Detect browser
  37. var pName = /(Chrome|MSIE|Safari|Opera|Firefox)/.test(ua) ? RegExp.$1 : null;
  38. // Detect version
  39. var vre = new RegExp('(Version|' + pName + ')[ \/]([^ ;]*)');
  40. var pVersion = (pName && vre.test(ua)) ? RegExp.$2 : null;
  41. var platform = (pOS && pName && pVersion) ? pName + ' ' + pVersion + ' on ' + pOS : 'unknown platform';
  42. /**
  43. * A smattering of methods that are needed to implement the JSLitmus testbed.
  44. */
  45. var jsl = {
  46. /**
  47. * Enhanced version of escape()
  48. */
  49. escape: function(s) {
  50. s = s.replace(/,/g, '\\,');
  51. s = escape(s);
  52. s = s.replace(/\+/g, '%2b');
  53. s = s.replace(/ /g, '+');
  54. return s;
  55. },
  56. /**
  57. * Get an element by ID.
  58. */
  59. $: function(id) {
  60. return document.getElementById(id);
  61. },
  62. /**
  63. * Null function
  64. */
  65. F: function() {},
  66. /**
  67. * Set the status shown in the UI
  68. */
  69. status: function(msg) {
  70. var el = jsl.$('jsl_status');
  71. if (el) el.innerHTML = msg || '';
  72. },
  73. /**
  74. * Convert a number to an abbreviated string like, "15K" or "10M"
  75. */
  76. toLabel: function(n) {
  77. if (n == Infinity) {
  78. return 'Infinity';
  79. } else if (n > 1e9) {
  80. n = Math.round(n/1e8);
  81. return n/10 + 'B';
  82. } else if (n > 1e6) {
  83. n = Math.round(n/1e5);
  84. return n/10 + 'M';
  85. } else if (n > 1e3) {
  86. n = Math.round(n/1e2);
  87. return n/10 + 'K';
  88. }
  89. return n;
  90. },
  91. /**
  92. * Copy properties from src to dst
  93. */
  94. extend: function(dst, src) {
  95. for (var k in src) dst[k] = src[k]; return dst;
  96. },
  97. /**
  98. * Like Array.join(), but for the key-value pairs in an object
  99. */
  100. join: function(o, delimit1, delimit2) {
  101. if (o.join) return o.join(delimit1); // If it's an array
  102. var pairs = [];
  103. for (var k in o) pairs.push(k + delimit1 + o[k]);
  104. return pairs.join(delimit2);
  105. },
  106. /**
  107. * Array#indexOf isn't supported in IE, so we use this as a cross-browser solution
  108. */
  109. indexOf: function(arr, o) {
  110. if (arr.indexOf) return arr.indexOf(o);
  111. for (var i = 0; i < this.length; i++) if (arr[i] === o) return i;
  112. return -1;
  113. }
  114. };
  115. /**
  116. * Test manages a single test (created with
  117. * JSLitmus.test())
  118. *
  119. * @private
  120. */
  121. var Test = function (name, f) {
  122. if (!f) throw new Error('Undefined test function');
  123. if (!(/function[^\(]*\(([^,\)]*)/).test(f.toString())) {
  124. throw new Error('"' + name + '" test: Test is not a valid Function object');
  125. }
  126. this.loopArg = RegExp.$1;
  127. this.name = name;
  128. this.f = f;
  129. };
  130. jsl.extend(Test, /** @lends Test */ {
  131. /** Calibration tests for establishing iteration loop overhead */
  132. CALIBRATIONS: [
  133. new Test('calibrating loop', function(count) {while (count--);}),
  134. new Test('calibrating function', jsl.F)
  135. ],
  136. /**
  137. * Run calibration tests. Returns true if calibrations are not yet
  138. * complete (in which case calling code should run the tests yet again).
  139. * onCalibrated - Callback to invoke when calibrations have finished
  140. */
  141. calibrate: function(onCalibrated) {
  142. for (var i = 0; i < Test.CALIBRATIONS.length; i++) {
  143. var cal = Test.CALIBRATIONS[i];
  144. if (cal.running) return true;
  145. if (!cal.count) {
  146. cal.isCalibration = true;
  147. cal.onStop = onCalibrated;
  148. //cal.MIN_TIME = .1; // Do calibrations quickly
  149. cal.run(2e4);
  150. return true;
  151. }
  152. }
  153. return false;
  154. }
  155. });
  156. jsl.extend(Test.prototype, {/** @lends Test.prototype */
  157. /** Initial number of iterations */
  158. INIT_COUNT: 10,
  159. /** Max iterations allowed (i.e. used to detect bad looping functions) */
  160. MAX_COUNT: 1e9,
  161. /** Minimum time a test should take to get valid results (secs) */
  162. MIN_TIME: .5,
  163. /** Callback invoked when test state changes */
  164. onChange: jsl.F,
  165. /** Callback invoked when test is finished */
  166. onStop: jsl.F,
  167. /**
  168. * Reset test state
  169. */
  170. reset: function() {
  171. delete this.count;
  172. delete this.time;
  173. delete this.running;
  174. delete this.error;
  175. },
  176. /**
  177. * Run the test (in a timeout). We use a timeout to make sure the browser
  178. * has a chance to finish rendering any UI changes we've made, like
  179. * updating the status message.
  180. */
  181. run: function(count) {
  182. count = count || this.INIT_COUNT;
  183. jsl.status(this.name + ' x ' + count);
  184. this.running = true;
  185. var me = this;
  186. setTimeout(function() {me._run(count);}, 200);
  187. },
  188. /**
  189. * The nuts and bolts code that actually runs a test
  190. */
  191. _run: function(count) {
  192. var me = this;
  193. // Make sure calibration tests have run
  194. if (!me.isCalibration && Test.calibrate(function() {me.run(count);})) return;
  195. this.error = null;
  196. try {
  197. var start, f = this.f, now, i = count;
  198. // Start the timer
  199. start = new Date();
  200. // Now for the money shot. If this is a looping function ...
  201. if (this.loopArg) {
  202. // ... let it do the iteration itself
  203. f(count);
  204. } else {
  205. // ... otherwise do the iteration for it
  206. while (i--) f();
  207. }
  208. // Get time test took (in secs)
  209. this.time = Math.max(1,new Date() - start)/1000;
  210. // Store iteration count and per-operation time taken
  211. this.count = count;
  212. this.period = this.time/count;
  213. // Do we need to do another run?
  214. this.running = this.time <= this.MIN_TIME;
  215. // ... if so, compute how many times we should iterate
  216. if (this.running) {
  217. // Bump the count to the nearest power of 2
  218. var x = this.MIN_TIME/this.time;
  219. var pow = Math.pow(2, Math.max(1, Math.ceil(Math.log(x)/Math.log(2))));
  220. count *= pow;
  221. if (count > this.MAX_COUNT) {
  222. throw new Error('Max count exceeded. If this test uses a looping function, make sure the iteration loop is working properly.');
  223. }
  224. }
  225. } catch (e) {
  226. // Exceptions are caught and displayed in the test UI
  227. this.reset();
  228. this.error = e;
  229. }
  230. // Figure out what to do next
  231. if (this.running) {
  232. me.run(count);
  233. } else {
  234. jsl.status('');
  235. me.onStop(me);
  236. }
  237. // Finish up
  238. this.onChange(this);
  239. },
  240. /**
  241. * Get the number of operations per second for this test.
  242. *
  243. * @param normalize if true, iteration loop overhead taken into account
  244. */
  245. getHz: function(/**Boolean*/ normalize) {
  246. var p = this.period;
  247. // Adjust period based on the calibration test time
  248. if (normalize && !this.isCalibration) {
  249. var cal = Test.CALIBRATIONS[this.loopArg ? 0 : 1];
  250. // If the period is within 20% of the calibration time, then zero the
  251. // it out
  252. p = p < cal.period*1.2 ? 0 : p - cal.period;
  253. }
  254. return Math.round(1/p);
  255. },
  256. /**
  257. * Get a friendly string describing the test
  258. */
  259. toString: function() {
  260. return this.name + ' - ' + this.time/this.count + ' secs';
  261. }
  262. });
  263. // CSS we need for the UI
  264. var STYLESHEET = '<style> \
  265. #jslitmus {font-family:sans-serif; font-size: 12px;} \
  266. #jslitmus a {text-decoration: none;} \
  267. #jslitmus a:hover {text-decoration: underline;} \
  268. #jsl_status { \
  269. margin-top: 10px; \
  270. font-size: 10px; \
  271. color: #888; \
  272. } \
  273. A IMG {border:none} \
  274. #test_results { \
  275. margin-top: 10px; \
  276. font-size: 12px; \
  277. font-family: sans-serif; \
  278. border-collapse: collapse; \
  279. border-spacing: 0px; \
  280. } \
  281. #test_results th, #test_results td { \
  282. border: solid 1px #ccc; \
  283. vertical-align: top; \
  284. padding: 3px; \
  285. } \
  286. #test_results th { \
  287. vertical-align: bottom; \
  288. background-color: #ccc; \
  289. padding: 1px; \
  290. font-size: 10px; \
  291. } \
  292. #test_results #test_platform { \
  293. color: #444; \
  294. text-align:center; \
  295. } \
  296. #test_results .test_row { \
  297. color: #006; \
  298. cursor: pointer; \
  299. } \
  300. #test_results .test_nonlooping { \
  301. border-left-style: dotted; \
  302. border-left-width: 2px; \
  303. } \
  304. #test_results .test_looping { \
  305. border-left-style: solid; \
  306. border-left-width: 2px; \
  307. } \
  308. #test_results .test_name {white-space: nowrap;} \
  309. #test_results .test_pending { \
  310. } \
  311. #test_results .test_running { \
  312. font-style: italic; \
  313. } \
  314. #test_results .test_done {} \
  315. #test_results .test_done { \
  316. text-align: right; \
  317. font-family: monospace; \
  318. } \
  319. #test_results .test_error {color: #600;} \
  320. #test_results .test_error .error_head {font-weight:bold;} \
  321. #test_results .test_error .error_body {font-size:85%;} \
  322. #test_results .test_row:hover td { \
  323. background-color: #ffc; \
  324. text-decoration: underline; \
  325. } \
  326. #chart { \
  327. margin: 10px 0px; \
  328. width: 250px; \
  329. } \
  330. #chart img { \
  331. border: solid 1px #ccc; \
  332. margin-bottom: 5px; \
  333. } \
  334. #chart #tiny_url { \
  335. height: 40px; \
  336. width: 250px; \
  337. } \
  338. #jslitmus_credit { \
  339. font-size: 10px; \
  340. color: #888; \
  341. margin-top: 8px; \
  342. } \
  343. </style>';
  344. // HTML markup for the UI
  345. var MARKUP = '<div id="jslitmus"> \
  346. <button onclick="JSLitmus.runAll(event)">Run Tests</button> \
  347. <button id="stop_button" disabled="disabled" onclick="JSLitmus.stop()">Stop Tests</button> \
  348. <br \> \
  349. <br \> \
  350. <input type="checkbox" style="vertical-align: middle" id="test_normalize" checked="checked" onchange="JSLitmus.renderAll()""> Normalize results \
  351. <table id="test_results"> \
  352. <colgroup> \
  353. <col /> \
  354. <col width="100" /> \
  355. </colgroup> \
  356. <tr><th id="test_platform" colspan="2">' + platform + '</th></tr> \
  357. <tr><th>Test</th><th>Ops/sec</th></tr> \
  358. <tr id="test_row_template" class="test_row" style="display:none"> \
  359. <td class="test_name"></td> \
  360. <td class="test_result">Ready</td> \
  361. </tr> \
  362. </table> \
  363. <div id="jsl_status"></div> \
  364. <div id="chart" style="display:none"> \
  365. <a id="chart_link" target="_blank"><img id="chart_image"></a> \
  366. TinyURL (for chart): \
  367. <iframe id="tiny_url" frameBorder="0" scrolling="no" src=""></iframe> \
  368. </div> \
  369. <a id="jslitmus_credit" title="JSLitmus home page" href="http://code.google.com/p/jslitmus" target="_blank">Powered by JSLitmus</a> \
  370. </div>';
  371. /**
  372. * The public API for creating and running tests
  373. */
  374. window.JSLitmus = {
  375. /** The list of all tests that have been registered with JSLitmus.test */
  376. _tests: [],
  377. /** The queue of tests that need to be run */
  378. _queue: [],
  379. /**
  380. * The parsed query parameters the current page URL. This is provided as a
  381. * convenience for test functions - it's not used by JSLitmus proper
  382. */
  383. params: {},
  384. /**
  385. * Initialize
  386. */
  387. _init: function() {
  388. // Parse query params into JSLitmus.params[] hash
  389. var match = (location + '').match(/([^?#]*)(#.*)?$/);
  390. if (match) {
  391. var pairs = match[1].split('&');
  392. for (var i = 0; i < pairs.length; i++) {
  393. var pair = pairs[i].split('=');
  394. if (pair.length > 1) {
  395. var key = pair.shift();
  396. var value = pair.length > 1 ? pair.join('=') : pair[0];
  397. this.params[key] = value;
  398. }
  399. }
  400. }
  401. // Write out the stylesheet. We have to do this here because IE
  402. // doesn't honor sheets written after the document has loaded.
  403. document.write(STYLESHEET);
  404. // Setup the rest of the UI once the document is loaded
  405. if (window.addEventListener) {
  406. window.addEventListener('load', this._setup, false);
  407. } else if (document.addEventListener) {
  408. document.addEventListener('load', this._setup, false);
  409. } else if (window.attachEvent) {
  410. window.attachEvent('onload', this._setup);
  411. }
  412. return this;
  413. },
  414. /**
  415. * Set up the UI
  416. */
  417. _setup: function() {
  418. var el = jsl.$('jslitmus_container');
  419. if (!el) document.body.appendChild(el = document.createElement('div'));
  420. el.innerHTML = MARKUP;
  421. // Render the UI for all our tests
  422. for (var i=0; i < JSLitmus._tests.length; i++)
  423. JSLitmus.renderTest(JSLitmus._tests[i]);
  424. },
  425. /**
  426. * (Re)render all the test results
  427. */
  428. renderAll: function() {
  429. for (var i = 0; i < JSLitmus._tests.length; i++)
  430. JSLitmus.renderTest(JSLitmus._tests[i]);
  431. JSLitmus.renderChart();
  432. },
  433. /**
  434. * (Re)render the chart graphics
  435. */
  436. renderChart: function() {
  437. var url = JSLitmus.chartUrl();
  438. jsl.$('chart_link').href = url;
  439. jsl.$('chart_image').src = url;
  440. jsl.$('chart').style.display = '';
  441. // Update the tiny URL
  442. jsl.$('tiny_url').src = 'http://tinyurl.com/api-create.php?url='+escape(url);
  443. },
  444. /**
  445. * (Re)render the results for a specific test
  446. */
  447. renderTest: function(test) {
  448. // Make a new row if needed
  449. if (!test._row) {
  450. var trow = jsl.$('test_row_template');
  451. if (!trow) return;
  452. test._row = trow.cloneNode(true);
  453. test._row.style.display = '';
  454. test._row.id = '';
  455. test._row.onclick = function() {JSLitmus._queueTest(test);};
  456. test._row.title = 'Run ' + test.name + ' test';
  457. trow.parentNode.appendChild(test._row);
  458. test._row.cells[0].innerHTML = test.name;
  459. }
  460. var cell = test._row.cells[1];
  461. var cns = [test.loopArg ? 'test_looping' : 'test_nonlooping'];
  462. if (test.error) {
  463. cns.push('test_error');
  464. cell.innerHTML =
  465. '<div class="error_head">' + test.error + '</div>' +
  466. '<ul class="error_body"><li>' +
  467. jsl.join(test.error, ': ', '</li><li>') +
  468. '</li></ul>';
  469. } else {
  470. if (test.running) {
  471. cns.push('test_running');
  472. cell.innerHTML = 'running';
  473. } else if (jsl.indexOf(JSLitmus._queue, test) >= 0) {
  474. cns.push('test_pending');
  475. cell.innerHTML = 'pending';
  476. } else if (test.count) {
  477. cns.push('test_done');
  478. var hz = test.getHz(jsl.$('test_normalize').checked);
  479. cell.innerHTML = hz != Infinity ? hz : '&infin;';
  480. } else {
  481. cell.innerHTML = 'ready';
  482. }
  483. }
  484. cell.className = cns.join(' ');
  485. },
  486. /**
  487. * Create a new test
  488. */
  489. test: function(name, f) {
  490. // Create the Test object
  491. var test = new Test(name, f);
  492. JSLitmus._tests.push(test);
  493. // Re-render if the test state changes
  494. test.onChange = JSLitmus.renderTest;
  495. // Run the next test if this one finished
  496. test.onStop = function(test) {
  497. if (JSLitmus.onTestFinish) JSLitmus.onTestFinish(test);
  498. JSLitmus.currentTest = null;
  499. JSLitmus._nextTest();
  500. };
  501. // Render the new test
  502. this.renderTest(test);
  503. },
  504. /**
  505. * Add all tests to the run queue
  506. */
  507. runAll: function(e) {
  508. e = e || window.event;
  509. var reverse = e && e.shiftKey, len = JSLitmus._tests.length;
  510. for (var i = 0; i < len; i++) {
  511. JSLitmus._queueTest(JSLitmus._tests[!reverse ? i : (len - i - 1)]);
  512. }
  513. },
  514. /**
  515. * Remove all tests from the run queue. The current test has to finish on
  516. * it's own though
  517. */
  518. stop: function() {
  519. while (JSLitmus._queue.length) {
  520. var test = JSLitmus._queue.shift();
  521. JSLitmus.renderTest(test);
  522. }
  523. },
  524. /**
  525. * Run the next test in the run queue
  526. */
  527. _nextTest: function() {
  528. if (!JSLitmus.currentTest) {
  529. var test = JSLitmus._queue.shift();
  530. if (test) {
  531. jsl.$('stop_button').disabled = false;
  532. JSLitmus.currentTest = test;
  533. test.run();
  534. JSLitmus.renderTest(test);
  535. if (JSLitmus.onTestStart) JSLitmus.onTestStart(test);
  536. } else {
  537. jsl.$('stop_button').disabled = true;
  538. JSLitmus.renderChart();
  539. }
  540. }
  541. },
  542. /**
  543. * Add a test to the run queue
  544. */
  545. _queueTest: function(test) {
  546. if (jsl.indexOf(JSLitmus._queue, test) >= 0) return;
  547. JSLitmus._queue.push(test);
  548. JSLitmus.renderTest(test);
  549. JSLitmus._nextTest();
  550. },
  551. /**
  552. * Generate a Google Chart URL that shows the data for all tests
  553. */
  554. chartUrl: function() {
  555. var n = JSLitmus._tests.length, markers = [], data = [];
  556. var d, min = 0, max = -1e10;
  557. var normalize = jsl.$('test_normalize').checked;
  558. // Gather test data
  559. for (var i=0; i < JSLitmus._tests.length; i++) {
  560. var test = JSLitmus._tests[i];
  561. if (test.count) {
  562. var hz = test.getHz(normalize);
  563. var v = hz != Infinity ? hz : 0;
  564. data.push(v);
  565. markers.push('t' + jsl.escape(test.name + '(' + jsl.toLabel(hz)+ ')') + ',000000,0,' +
  566. markers.length + ',10');
  567. max = Math.max(v, max);
  568. }
  569. }
  570. if (markers.length <= 0) return null;
  571. // Build chart title
  572. var title = document.getElementsByTagName('title');
  573. title = (title && title.length) ? title[0].innerHTML : null;
  574. var chart_title = [];
  575. if (title) chart_title.push(title);
  576. chart_title.push('Ops/sec (' + platform + ')');
  577. // Build labels
  578. var labels = [jsl.toLabel(min), jsl.toLabel(max)];
  579. var w = 250, bw = 15;
  580. var bs = 5;
  581. var h = markers.length*(bw + bs) + 30 + chart_title.length*20;
  582. var params = {
  583. chtt: escape(chart_title.join('|')),
  584. chts: '000000,10',
  585. cht: 'bhg', // chart type
  586. chd: 't:' + data.join(','), // data set
  587. chds: min + ',' + max, // max/min of data
  588. chxt: 'x', // label axes
  589. chxl: '0:|' + labels.join('|'), // labels
  590. chsp: '0,1',
  591. chm: markers.join('|'), // test names
  592. chbh: [bw, 0, bs].join(','), // bar widths
  593. // chf: 'bg,lg,0,eeeeee,0,eeeeee,.5,ffffff,1', // gradient
  594. chs: w + 'x' + h
  595. };
  596. return 'http://chart.apis.google.com/chart?' + jsl.join(params, '=', '&');
  597. }
  598. };
  599. JSLitmus._init();
  600. })();