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