1 <!DOCTYPE html>
2 <html>
3 <head>
4 <title>Telemetry Performance Test Results</title>
5 <style type="text/css">
6
7 section {
8 background: white;
9 padding: 10px;
10 position: relative;
11 }
12
13 .collapsed:before {
14 color: #ccc;
15 content: '\25B8\00A0';
16 }
17
18 .expanded:before {
19 color: #eee;
20 content: '\25BE\00A0';
21 }
22
23 .line-plots {
24 padding-left: 25px;
25 }
26
27 .line-plots > div {
28 display: inline-block;
29 width: 90px;
30 height: 40px;
31 margin-right: 10px;
32 }
33
34 .lage-line-plots {
35 padding-left: 25px;
36 }
37
38 .large-line-plots > div, .histogram-plots > div {
39 display: inline-block;
40 width: 400px;
41 height: 200px;
42 margin-right: 10px;
43 }
44
45 .large-line-plot-labels > div, .histogram-plot-labels > div {
46 display: inline-block;
47 width: 400px;
48 height: 11px;
49 margin-right: 10px;
50 color: #545454;
51 text-align: center;
52 font-size: 11px;
53 }
54
55 .closeButton {
56 display: inline-block;
57 background: #eee;
58 background: linear-gradient(rgb(220, 220, 220), rgb(255, 255, 255));
59 border: inset 1px #ddd;
60 border-radius: 4px;
61 float: right;
62 font-size: small;
63 -webkit-user-select: none;
64 font-weight: bold;
65 padding: 1px 4px;
66 }
67
68 .closeButton:hover {
69 background: #F09C9C;
70 }
71
72 .label {
73 cursor: text;
74 }
75
76 .label:hover {
77 background: #ffcc66;
78 }
79
80 section h1 {
81 text-align: center;
82 font-size: 1em;
83 }
84
85 section .tooltip {
86 position: absolute;
87 text-align: center;
88 background: #ffcc66;
89 border-radius: 5px;
90 padding: 0px 5px;
91 }
92
93 body {
94 padding: 0px;
95 margin: 0px;
96 font-family: sans-serif;
97 }
98
99 table {
100 background: white;
101 width: 100%;
102 }
103
104 table, td, th {
105 border-collapse: collapse;
106 padding: 5px;
107 white-space: nowrap;
108 }
109
110 .highlight:hover {
111 color: #202020;
112 background: #e0e0e0;
113 }
114
115 .nestedRow {
116 background: #f8f8f8;
117 }
118
119 .importantNestedRow {
120 background: #e0e0e0;
121 font-weight: bold;
122 }
123
124 table td {
125 position: relative;
126 }
127
128 th, td {
129 cursor: pointer;
130 cursor: hand;
131 }
132
133 th {
134 background: #e6eeee;
135 background: linear-gradient(rgb(244, 244, 244), rgb(217, 217, 217));
136 border: 1px solid #ccc;
137 }
138
139 th.sortUp:after {
140 content: ' \25BE';
141 }
142
143 th.sortDown:after {
144 content: ' \25B4';
145 }
146
147 td.comparison, td.result {
148 text-align: right;
149 }
150
151 td.better {
152 color: #6c6;
153 }
154
155 td.fadeOut {
156 opacity: 0.5;
157 }
158
159 td.unknown {
160 color: #ccc;
161 }
162
163 td.worse {
164 color: #c66;
165 }
166
167 td.reference {
168 font-style: italic;
169 font-weight: bold;
170 color: #444;
171 }
172
173 td.missing {
174 color: #ccc;
175 text-align: center;
176 }
177
178 td.missingReference {
179 color: #ccc;
180 text-align: center;
181 font-style: italic;
182 }
183
184 .checkbox {
185 display: inline-block;
186 background: #eee;
187 background: linear-gradient(rgb(220, 220, 220), rgb(200, 200, 200));
188 border: inset 1px #ddd;
189 border-radius: 5px;
190 margin: 10px;
191 font-size: small;
192 cursor: pointer;
193 cursor: hand;
194 -webkit-user-select: none;
195 font-weight: bold;
196 }
197
198 .checkbox span {
199 display: inline-block;
200 line-height: 100%;
201 padding: 5px 8px;
202 border: outset 1px transparent;
203 }
204
205 .checkbox .checked {
206 background: #e6eeee;
207 background: linear-gradient(rgb(255, 255, 255), rgb(235, 235, 235));
208 border: outset 1px #eee;
209 border-radius: 5px;
210 }
211
212 .openAllButton {
213 display: inline-block;
214 colour: #6c6
215 background: #eee;
216 background: linear-gradient(rgb(220, 220, 220), rgb(255, 255, 255));
217 border: inset 1px #ddd;
218 border-radius: 5px;
219 float: left;
220 font-size: small;
221 -webkit-user-select: none;
222 font-weight: bold;
223 padding: 1px 4px;
224 }
225
226 .openAllButton:hover {
227 background: #60f060;
228 }
229
230 .closeAllButton {
231 display: inline-block;
232 colour: #c66
233 background: #eee;
234 background: linear-gradient(rgb(220, 220, 220),rgb(255, 255, 255));
235 border: inset 1px #ddd;
236 border-radius: 5px;
237 float: left;
238 font-size: small;
239 -webkit-user-select: none;
240 font-weight: bold;
241 padding: 1px 4px;
242 }
243
244 .closeAllButton:hover {
245 background: #f04040;
246 }
247
248 </style>
249 </head>
250 <body onload="init()">
251 <div style="padding: 0 10px; white-space: nowrap;">
252 Result <span id="time-memory" class="checkbox"></span>
253 Reference <span id="reference" class="checkbox"></span>
254 Style <span id="scatter-line" class="checkbox"><span class="checked">Scatter</span><span>Line</span></span>
255 <span class="checkbox"><span class="checked" id="undelete">Undelete</span></span><br>
256 Run your test with --reset-results to clear all runs
257 </div>
258 <table id="container"></table>
259 <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.4/jquery.min.js"></script>
260 <script>
261 %plugins%
262 </script>
263 <script>
264
265 var EXPANDED = true;
266 var COLLAPSED = false;
267 var SMALLEST_PERCENT_DISPLAYED = 0.01;
268 var INVISIBLE = false;
269 var VISIBLE = true;
270 var COMPARISON_SUFFIX = '_compare';
271 var SORT_DOWN_CLASS = 'sortDown';
272 var SORT_UP_CLASS = 'sortUp';
273 var BETTER_CLASS = 'better';
274 var WORSE_CLASS = 'worse';
275 var UNKNOWN_CLASS = 'unknown'
276 // px Indentation for graphs
277 var GRAPH_INDENT = 64;
278 var PADDING_UNDER_GRAPH = 5;
279 // px Indentation for nested children left-margins
280 var INDENTATION = 40;
281
282 function TestResult(metric, values, associatedRun, std, degreesOfFreedom) {
283 if (values) {
284 if (values[0] instanceof Array) {
285 var flattenedValues = [];
286 for (var i = 0; i < values.length; i++)
287 flattenedValues = flattenedValues.concat(values[i]);
288 values = flattenedValues;
289 }
290
291 if (jQuery.type(values[0]) === 'string') {
292 try {
293 var current = JSON.parse(values[0]);
294 if (current.params.type === 'HISTOGRAM') {
295 this.histogramValues = current;
296 // Histogram results have no values (per se). Instead we calculate
297 // the values from the histogram bins.
298 var values = [];
299 var buckets = current.buckets
300 for (var i = 0; i < buckets.length; i++) {
301 var bucket = buckets[i];
302 var bucket_mean = (bucket.high + bucket.low) / 2;
303 for (var b = 0; b < bucket.count; b++) {
304 values.push(bucket_mean);
305 }
306 }
307 }
308 }
309 catch (e) {
310 console.error(e, e.stack);
311 }
312 }
313 } else {
314 values = [];
315 }
316
317 this.test = function() { return metric; }
318 this.values = function() { return values.map(function(value) { return metric.scalingFactor() * value; }); }
319 this.unscaledMean = function() { return Statistics.sum(values) / values.length; }
320 this.mean = function() { return metric.scalingFactor() * this.unscaledMean(); }
321 this.min = function() { return metric.scalingFactor() * Statistics.min(values); }
322 this.max = function() { return metric.scalingFactor() * Statistics.max(values); }
323 this.confidenceIntervalDelta = function() {
324 if (std !== undefined) {
325 return metric.scalingFactor() * Statistics.confidenceIntervalDeltaFromStd(0.95, values.length,
326 std, degreesOfFreedom);
327 }
328 return metric.scalingFactor() * Statistics.confidenceIntervalDelta(0.95, values.length,
329 Statistics.sum(values), Statistics.squareSum(values));
330 }
331 this.confidenceIntervalDeltaRatio = function() { return this.confidenceIntervalDelta() / this.mean(); }
332 this.percentDifference = function(other) {
333 if (other === undefined) {
334 return undefined;
335 }
336 return (other.unscaledMean() - this.unscaledMean()) / this.unscaledMean();
337 }
338 this.isStatisticallySignificant = function(other) {
339 if (other === undefined) {
340 return false;
341 }
342 var diff = Math.abs(other.mean() - this.mean());
343 return diff > this.confidenceIntervalDelta() && diff > other.confidenceIntervalDelta();
344 }
345 this.run = function() { return associatedRun; }
346 }
347
348 function TestRun(entry) {
349 this.id = function() { return entry['buildTime'].replace(/[:.-]/g,''); }
350 this.label = function() {
351 if (labelKey in localStorage)
352 return localStorage[labelKey];
353 return entry['label'];
354 }
355 this.setLabel = function(label) { localStorage[labelKey] = label; }
356 this.isHidden = function() { return localStorage[hiddenKey]; }
357 this.hide = function() { localStorage[hiddenKey] = true; }
358 this.show = function() { localStorage.removeItem(hiddenKey); }
359 this.description = function() {
360 return new Date(entry['buildTime']).toLocaleString() + '\n' + entry['platform'] + ' ' + this.label();
361 }
362
363 var labelKey = 'telemetry_label_' + this.id();
364 var hiddenKey = 'telemetry_hide_' + this.id();
365 }
366
367 function PerfTestMetric(name, metric, unit, isImportant) {
368 var testResults = [];
369 var cachedUnit = null;
370 var cachedScalingFactor = null;
371
372 // We can't do this in TestResult because all results for each test need to share the same unit and the same scaling factor.
373 function computeScalingFactorIfNeeded() {
374 // FIXME: We shouldn't be adjusting units on every test result.
375 // We can only do this on the first test.
376 if (!testResults.length || cachedUnit)
377 return;
378
379 var mean = testResults[0].unscaledMean(); // FIXME: We should look at all values.
380 var kilo = unit == 'bytes' ? 1024 : 1000;
381 if (mean > 10 * kilo * kilo && unit != 'ms') {
382 cachedScalingFactor = 1 / kilo / kilo;
383 cachedUnit = 'M ' + unit;
384 } else if (mean > 10 * kilo) {
385 cachedScalingFactor = 1 / kilo;
386 cachedUnit = unit == 'ms' ? 's' : ('K ' + unit);
387 } else {
388 cachedScalingFactor = 1;
389 cachedUnit = unit;
390 }
391 }
392
393 this.name = function() { return name + ':' + metric; }
394 this.isImportant = isImportant;
395 this.isMemoryTest = function() {
396 return (unit == 'kb' ||
397 unit == 'KB' ||
398 unit == 'MB' ||
399 unit == 'bytes' ||
400 unit == 'count' ||
401 !metric.indexOf('V8.'));
402 }
403 this.addResult = function(newResult) {
404 testResults.push(newResult);
405 cachedUnit = null;
406 cachedScalingFactor = null;
407 }
408 this.results = function() { return testResults; }
409 this.scalingFactor = function() {
410 computeScalingFactorIfNeeded();
411 return cachedScalingFactor;
412 }
413 this.unit = function() {
414 computeScalingFactorIfNeeded();
415 return cachedUnit;
416 }
417 this.biggerIsBetter = function() {
418 if (window.unitToBiggerIsBetter == undefined) {
419 window.unitToBiggerIsBetter = {};
420 var units = JSON.parse(document.getElementById('units-json').textContent);
421 for (var u in units) {
422 if (units[u].improvement_direction == 'up') {
423 window.unitToBiggerIsBetter[u] = true;
424 }
425 }
426 }
427 return window.unitToBiggerIsBetter[unit];
428 }
429 }
430
431 function UndeleteManager() {
432 var key = 'telemetry_undeleteIds'
433 var undeleteIds = localStorage[key];
434 if (undeleteIds) {
435 undeleteIds = JSON.parse(undeleteIds);
436 } else {
437 undeleteIds = [];
438 }
439
440 this.ondelete = function(id) {
441 undeleteIds.push(id);
442 localStorage[key] = JSON.stringify(undeleteIds);
443 }
444 this.undeleteMostRecent = function() {
445 if (!this.mostRecentlyDeletedId())
446 return;
447 undeleteIds.pop();
448 localStorage[key] = JSON.stringify(undeleteIds);
449 }
450 this.mostRecentlyDeletedId = function() {
451 if (!undeleteIds.length)
452 return undefined;
453 return undeleteIds[undeleteIds.length-1];
454 }
455 }
456 var undeleteManager = new UndeleteManager();
457
458 var plotColor = 'rgb(230,50,50)';
459 var subpointsPlotOptions = {
460 lines: {show:true, lineWidth: 0},
461 color: plotColor,
462 points: {show: true, radius: 1},
463 bars: {show: false}};
464
465 var mainPlotOptions = {
466 xaxis: {
467 min: -0.5,
468 tickSize: 1,
469 },
470 crosshair: { mode: 'y' },
471 series: { shadowSize: 0 },
472 bars: {show: true, align: 'center', barWidth: 0.5},
473 lines: { show: false },
474 points: { show: true },
475 grid: {
476 borderWidth: 1,
477 borderColor: '#ccc',
478 backgroundColor: '#fff',
479 hoverable: true,
480 autoHighlight: false,
481 }
482 };
483
484 var linePlotOptions = {
485 yaxis: { show: false },
486 xaxis: { show: false },
487 lines: { show: true },
488 grid: { borderWidth: 1, borderColor: '#ccc' },
489 colors: [ plotColor ]
490 };
491
492 var largeLinePlotOptions = {
493 xaxis: {
494 show: true,
495 tickDecimals: 0,
496 },
497 lines: { show: true },
498 grid: { borderWidth: 1, borderColor: '#ccc' },
499 colors: [ plotColor ]
500 };
501
502 var histogramPlotOptions = {
503 bars: {show: true, fill: 1}
504 };
505
506 function createPlot(container, test, useLargeLinePlots) {
507 if (test.results()[0].histogramValues) {
508 var section = $('<section><div class="histogram-plots"></div>'
509 + '<div class="histogram-plot-labels"></div>'
510 + '<span class="tooltip"></span></section>');
511 $(container).append(section);
512 attachHistogramPlots(test, section.children('.histogram-plots'));
513 }
514 else if (useLargeLinePlots) {
515 var section = $('<section><div class="large-line-plots"></div>'
516 + '<div class="large-line-plot-labels"></div>'
517 + '<span class="tooltip"></span></section>');
518 $(container).append(section);
519 attachLinePlots(test, section.children('.large-line-plots'), useLargeLinePlots);
520 attachLinePlotLabels(test, section.children('.large-line-plot-labels'));
521 } else {
522 var section = $('<section><div class="plot"></div><div class="line-plots"></div>'
523 + '<span class="tooltip"></span></section>');
524 section.children('.plot').css({'width': (100 * test.results().length + 25) + 'px', 'height': '300px'});
525 $(container).append(section);
526
527 var plotContainer = section.children('.plot');
528 var minIsZero = true;
529 attachPlot(test, plotContainer, minIsZero);
530
531 attachLinePlots(test, section.children('.line-plots'), useLargeLinePlots);
532
533 var tooltip = section.children('.tooltip');
534 plotContainer.bind('plothover', function(event, position, item) {
535 if (item) {
536 var postfix = item.series.id ? ' (' + item.series.id + ')' : '';
537 tooltip.html(item.datapoint[1].toPrecision(4) + postfix);
538 var sectionOffset = $(section).offset();
539 tooltip.css({left: item.pageX - sectionOffset.left - tooltip.outerWidth() / 2, top: item.pageY - sectionOffset.top + 10});
540 tooltip.fadeIn(200);
541 } else
542 tooltip.hide();
543 });
544 plotContainer.mouseout(function() {
545 tooltip.hide();
546 });
547 plotContainer.click(function(event) {
548 event.preventDefault();
549 minIsZero = !minIsZero;
550 attachPlot(test, plotContainer, minIsZero);
551 });
552 }
553 return section;
554 }
555
556 function attachLinePlots(test, container, useLargeLinePlots) {
557 var results = test.results();
558 var attachedPlot = false;
559
560 if (useLargeLinePlots) {
561 var maximum = 0;
562 for (var i = 0; i < results.length; i++) {
563 var values = results[i].values();
564 if (!values)
565 continue;
566 var local_max = Math.max.apply(Math, values);
567 if (local_max > maximum)
568 maximum = local_max;
569 }
570 }
571
572 for (var i = 0; i < results.length; i++) {
573 container.append('');
574 var values = results[i].values();
575 if (!values)
576 continue;
577 attachedPlot = true;
578
579 if (useLargeLinePlots) {
580 var options = $.extend(true, {}, largeLinePlotOptions,
581 {yaxis: {min: 0.0, max: maximum},
582 xaxis: {min: 0.0, max: values.length - 1},
583 points: {show: (values.length < 2) ? true : false}});
584 } else {
585 var options = $.extend(true, {}, linePlotOptions,
586 {yaxis: {min: Math.min.apply(Math, values) * 0.9, max: Math.max.apply(Math, values) * 1.1},
587 xaxis: {min: -0.5, max: values.length - 0.5},
588 points: {show: (values.length < 2) ? true : false}});
589 }
590 $.plot(container.children().last(), [values.map(function(value, index) { return [index, value]; })], options);
591 }
592 if (!attachedPlot)
593 container.children().remove();
594 }
595
596 function attachHistogramPlots(test, container) {
597 var results = test.results();
598 var attachedPlot = false;
599
600 for (var i = 0; i < results.length; i++) {
601 container.append('<div></div>');
602 var histogram = results[i].histogramValues
603 if (!histogram)
604 continue;
605 attachedPlot = true;
606
607 var buckets = histogram.buckets
608 var bucket;
609 var max_count = 0;
610 for (var j = 0; j < buckets.length; j++) {
611 bucket = buckets[j];
612 max_count = Math.max(max_count, bucket.count);
613 }
614 var xmax = bucket.high * 1.1;
615 var ymax = max_count * 1.1;
616
617 var options = $.extend(true, {}, histogramPlotOptions,
618 {yaxis: {min: 0.0, max: ymax},
619 xaxis: {min: histogram.params.min, max: xmax}});
620 var plot = $.plot(container.children().last(), [[]], options);
621 // Flot only supports fixed with bars and our histogram's buckets are
622 // variable width, so we need to do our own bar drawing.
623 var ctx = plot.getCanvas().getContext("2d");
624 ctx.lineWidth="1";
625 ctx.fillStyle = "rgba(255, 0, 0, 0.2)";
626 ctx.strokeStyle="red";
627 for (var j = 0; j < buckets.length; j++) {
628 bucket = buckets[j];
629 var bl = plot.pointOffset({ x: bucket.low, y: 0});
630 var tr = plot.pointOffset({ x: bucket.high, y: bucket.count});
631 ctx.fillRect(bl.left, bl.top, tr.left - bl.left, tr.top - bl.top);
632 ctx.strokeRect(bl.left, bl.top, tr.left - bl.left, tr.top - bl.top);
633 }
634 }
635 if (!attachedPlot)
636 container.children().remove();
637 }
638
639 function attachLinePlotLabels(test, container) {
640 var results = test.results();
641 var attachedPlot = false;
642 for (var i = 0; i < results.length; i++) {
643 container.append('<div>' + results[i].run().label() + '</div>');
644 }
645 }
646
647 function attachPlot(test, plotContainer, minIsZero) {
648 var results = test.results();
649
650 var values = results.reduce(function(values, result, index) {
651 var newValues = result.values();
652 return newValues ? values.concat(newValues.map(function(value) { return [index, value]; })) : values;
653 }, []);
654
655 var plotData = [$.extend(true, {}, subpointsPlotOptions, {data: values})];
656 plotData.push({id: 'μ', data: results.map(function(result, index) { return [index, result.mean()]; }), color: plotColor});
657
658 var overallMax = Statistics.max(results.map(function(result, index) { return result.max(); }));
659 var overallMin = Statistics.min(results.map(function(result, index) { return result.min(); }));
660 var margin = (overallMax - overallMin) * 0.1;
661 var currentPlotOptions = $.extend(true, {}, mainPlotOptions, {yaxis: {
662 min: minIsZero ? 0 : overallMin - margin,
663 max: minIsZero ? overallMax * 1.1 : overallMax + margin}});
664
665 currentPlotOptions.xaxis.max = results.length - 0.5;
666 currentPlotOptions.xaxis.ticks = results.map(function(result, index) { return [index, result.run().label()]; });
667
668 $.plot(plotContainer, plotData, currentPlotOptions);
669 }
670
671 function toFixedWidthPrecision(value) {
672 var decimal = value.toFixed(2);
673 return decimal;
674 }
675
676 function formatPercentage(fraction) {
677 var percentage = fraction * 100;
678 return (fraction * 100).toFixed(2) + '%';
679 }
680
681 function setUpSortClicks(runs)
682 {
683 $('#nameColumn').click(sortByName);
684
685 $('#unitColumn').click(sortByUnit);
686
687 runs.forEach(function(run) {
688 $('#' + run.id()).click(sortByResult);
689 $('#' + run.id() + COMPARISON_SUFFIX).click(sortByReference);
690 });
691 }
692
693 function TestTypeSelector(tests) {
694 this.recognizers = {
695 'Time': function(test) { return !test.isMemoryTest(); },
696 'Memory': function(test) { return test.isMemoryTest(); }
697 };
698 this.testTypeNames = this.generateUsedTestTypeNames(tests);
699 // Default to selecting the first test-type name in the list.
700 this.testTypeName = this.testTypeNames[0];
701 }
702
703 TestTypeSelector.prototype = {
704 set testTypeName(testTypeName) {
705 this._testTypeName = testTypeName;
706 this.shouldShowTest = this.recognizers[testTypeName];
707 },
708
709 generateUsedTestTypeNames: function(allTests) {
710 var testTypeNames = [];
711
712 for (var recognizedTestName in this.recognizers) {
713 var recognizes = this.recognizers[recognizedTestName];
714 for (var testName in allTests) {
715 var test = allTests[testName];
716 if (recognizes(test)) {
717 testTypeNames.push(recognizedTestName);
718 break;
719 }
720 }
721 }
722
723 if (testTypeNames.length === 0) {
724 // No test types we recognize, add 'No Results' with a dummy recognizer.
725 var noResults = 'No Results';
726 this.recognizers[noResults] = function() { return false; };
727 testTypeNames.push(noResults);
728 } else if (testTypeNames.length > 1) {
729 // We have more than one test type, so add 'All' with a recognizer that always succeeds.
730 var allResults = 'All';
731 this.recognizers[allResults] = function() { return true; };
732 testTypeNames.push(allResults);
733 }
734
735 return testTypeNames;
736 },
737
738 buildButtonHTMLForUsedTestTypes: function() {
739 var selectedTestTypeName = this._testTypeName;
740 // Build spans for all recognised test names with the selected test highlighted.
741 return this.testTypeNames.map(function(testTypeName) {
742 var classAttribute = testTypeName === selectedTestTypeName ? ' class=checked' : '';
743 return '<span' + classAttribute + '>' + testTypeName + '</span>';
744 }).join('');
745 }
746 };
747
748 var topLevelRows;
749 var allTableRows;
750
751 function displayTable(tests, runs, testTypeSelector, referenceIndex, useLargeLinePlots) {
752 var resultHeaders = runs.map(function(run, index) {
753 var header = '<th id="' + run.id() + '" ' +
754 'colspan=2 ' +
755 'title="' + run.description() + '">' +
756 '<span class="label" ' +
757 'title="Edit run label">' +
758 run.label() +
759 '</span>' +
760 '<div class="closeButton" ' +
761 'title="Delete run">' +
762 '×' +
763 '</div>' +
764 '</th>';
765 if (index !== referenceIndex) {
766 header += '<th id="' + run.id() + COMPARISON_SUFFIX + '" ' +
767 'title="Sort by better/worse">' +
768 'Δ' +
769 '</th>';
770 }
771 return header;
772 });
773
774 resultHeaders = resultHeaders.join('');
775
776 htmlString = '<thead>' +
777 '<tr>' +
778 '<th id="nameColumn">' +
779 '<div class="openAllButton" ' +
780 'title="Open all rows or graphs">' +
781 'Open All' +
782 '</div>' +
783 '<div class="closeAllButton" ' +
784 'title="Close all rows">' +
785 'Close All' +
786 '</div>' +
787 'Test' +
788 '</th>' +
789 '<th id="unitColumn">' +
790 'Unit' +
791 '</th>' +
792 resultHeaders +
793 '</tr>' +
794 '</head>' +
795 '<tbody>' +
796 '</tbody>';
797
798 $('#container').html(htmlString);
799
800 var testNames = [];
801 for (testName in tests)
802 testNames.push(testName);
803
804 allTableRows = [];
805 testNames.forEach(function(testName) {
806 var test = tests[testName];
807 if (testTypeSelector.shouldShowTest(test)) {
808 allTableRows.push(new TableRow(runs, test, referenceIndex, useLargeLinePlots));
809 }
810 });
811
812 // Build a list of top level rows with attached children
813 topLevelRows = [];
814 allTableRows.forEach(function(row) {
815 // Add us to top level if we are a top-level row...
816 if (row.hasNoURL) {
817 topLevelRows.push(row);
818 // Add a duplicate child row that holds the graph for the parent
819 var graphHolder = new TableRow(runs, row.test, referenceIndex, useLargeLinePlots);
820 graphHolder.isImportant = true;
821 graphHolder.URL = 'Summary';
822 graphHolder.hideRowData();
823 allTableRows.push(graphHolder);
824 row.addNestedChild(graphHolder);
825 return;
826 }
827
828 // ...or add us to our parent if we have one ...
829 for (var i = 0; i < allTableRows.length; i++) {
830 if (allTableRows[i].isParentOf(row)) {
831 allTableRows[i].addNestedChild(row);
832 return;
833 }
834 }
835
836 // ...otherwise this result is orphaned, display it at top level with a graph
837 row.hasGraph = true;
838 topLevelRows.push(row);
839 });
840
841 buildTable(topLevelRows);
842
843 $('.closeButton').click(function(event) {
844 for (var i = 0; i < runs.length; i++) {
845 if (runs[i].id() == event.target.parentNode.id) {
846 runs[i].hide();
847 undeleteManager.ondelete(runs[i].id());
848 location.reload();
849 break;
850 }
851 }
852 event.stopPropagation();
853 });
854
855 $('.closeAllButton').click(function(event) {
856 for (var i = 0; i < allTableRows.length; i++) {
857 allTableRows[i].closeRow();
858 }
859 event.stopPropagation();
860 });
861
862 $('.openAllButton').click(function(event) {
863 for (var i = 0; i < topLevelRows.length; i++) {
864 topLevelRows[i].openRow();
865 }
866 event.stopPropagation();
867 });
868
869 setUpSortClicks(runs);
870
871 $('.label').click(function(event) {
872 for (var i = 0; i < runs.length; i++) {
873 if (runs[i].id() == event.target.parentNode.id) {
874 $(event.target).replaceWith('runs[i].label() + '">');
875 $('#labelEditor').focusout(function() {
876 runs[i].setLabel(this.value);
877 location.reload();
878 });
879 $('#labelEditor').keypress(function(event) {
880 if (event.which == 13) {
881 runs[i].setLabel(this.value);
882 location.reload();
883 }
884 });
885 $('#labelEditor').click(function(event) {
886 event.stopPropagation();
887 });
888 $('#labelEditor').mousedown(function(event) {
889 event.stopPropagation();
890 });
891 $('#labelEditor').select();
892 break;
893 }
894 }
895 event.stopPropagation();
896 });
897 }
898
899 function validForSorting(row) {
900 return ($.type(row.sortValue) === 'string') || !isNaN(row.sortValue);
901 }
902
903 var sortDirection = 1;
904
905 function sortRows(rows) {
906 rows.sort(
907 function(rowA,rowB) {
908 if (validForSorting(rowA) !== validForSorting(rowB)) {
909 // Sort valid values upwards when compared to invalid
910 if (validForSorting(rowA)) {
911 return -1;
912 }
913 if (validForSorting(rowB)) {
914 return 1;
915 }
916 }
917
918 // Some rows always sort to the top
919 if (rowA.isImportant) {
920 return -1;
921 }
922 if (rowB.isImportant) {
923 return 1;
924 }
925
926 if (rowA.sortValue === rowB.sortValue) {
927 // Sort identical values by name to keep the sort stable,
928 // always keep name alphabetical (even if a & b sort values
929 // are invalid)
930 return rowA.test.name() > rowB.test.name() ? 1 : -1;
931 }
932
933 return rowA.sortValue > rowB.sortValue ? sortDirection : -sortDirection;
934 } );
935
936 // Sort the rows' children
937 rows.forEach(function(row) {
938 sortRows(row.children);
939 });
940 }
941
942 function buildTable(rows) {
943 rows.forEach(function(row) {
944 row.removeFromPage();
945 });
946
947 sortRows(rows);
948
949 rows.forEach(function(row) {
950 row.addToPage();
951 });
952 }
953
954 var activeSortHeaderElement = undefined;
955 var columnSortDirection = {};
956
957 function determineColumnSortDirection(element) {
958 columnDirection = columnSortDirection[element.id];
959
960 if (columnDirection === undefined) {
961 // First time we've sorted this row, default to down
962 columnSortDirection[element.id] = SORT_DOWN_CLASS;
963 } else if (element === activeSortHeaderElement) {
964 // Clicking on same header again, swap direction
965 columnSortDirection[element.id] = (columnDirection === SORT_UP_CLASS) ? SORT_DOWN_CLASS : SORT_UP_CLASS;
966 }
967 }
968
969 function updateSortDirection(element) {
970 // Remove old header's sort arrow
971 if (activeSortHeaderElement !== undefined) {
972 activeSortHeaderElement.classList.remove(columnSortDirection[activeSortHeaderElement.id]);
973 }
974
975 determineColumnSortDirection(element);
976
977 sortDirection = (columnSortDirection[element.id] === SORT_UP_CLASS) ? 1 : -1;
978
979 // Add new header's sort arrow
980 element.classList.add(columnSortDirection[element.id]);
981 activeSortHeaderElement = element;
982 }
983
984 function sortByName(event) {
985 updateSortDirection(event.toElement);
986
987 allTableRows.forEach(function(row) {
988 row.prepareToSortByName();
989 });
990
991 buildTable(topLevelRows);
992 }
993
994 function sortByUnit(event) {
995 updateSortDirection(event.toElement);
996
997 allTableRows.forEach(function(row) {
998 row.prepareToSortByUnit();
999 });
1000
1001 buildTable(topLevelRows);
1002 }
1003
1004 function sortByResult(event) {
1005 updateSortDirection(event.toElement);
1006
1007 var runId = event.target.id;
1008
1009 allTableRows.forEach(function(row) {
1010 row.prepareToSortByTestResults(runId);
1011 });
1012
1013 buildTable(topLevelRows);
1014 }
1015
1016 function sortByReference(event) {
1017 updateSortDirection(event.toElement);
1018
1019 // The element ID has _compare appended to allow us to set up a click event
1020 // remove the _compare to return a useful Id
1021 var runIdWithCompare = event.target.id;
1022 var runId = runIdWithCompare.split('_')[0];
1023
1024 allTableRows.forEach(function(row) {
1025 row.prepareToSortRelativeToReference(runId);
1026 });
1027
1028 buildTable(topLevelRows);
1029 }
1030
1031 function linearRegression(points) {
1032 // Implement http://www.easycalculation.com/statistics/learn-correlation.php.
1033 // x = magnitude
1034 // y = iterations
1035 var sumX = 0;
1036 var sumY = 0;
1037 var sumXSquared = 0;
1038 var sumYSquared = 0;
1039 var sumXTimesY = 0;
1040
1041 for (var i = 0; i < points.length; i++) {
1042 var x = i;
1043 var y = points[i];
1044 sumX += x;
1045 sumY += y;
1046 sumXSquared += x * x;
1047 sumYSquared += y * y;
1048 sumXTimesY += x * y;
1049 }
1050
1051 var r = (points.length * sumXTimesY - sumX * sumY) /
1052 Math.sqrt((points.length * sumXSquared - sumX * sumX) *
1053 (points.length * sumYSquared - sumY * sumY));
1054
1055 if (isNaN(r) || r == Math.Infinity)
1056 r = 0;
1057
1058 var slope = (points.length * sumXTimesY - sumX * sumY) / (points.length * sumXSquared - sumX * sumX);
1059 var intercept = sumY / points.length - slope * sumX / points.length;
1060 return {slope: slope, intercept: intercept, rSquared: r * r};
1061 }
1062
1063 var warningSign = '
1064 + ''
1065 + ''
1066 + ''
1067 + '';
1068
1069 function TableRow(runs, test, referenceIndex, useLargeLinePlots) {
1070 this.runs = runs;
1071 this.test = test;
1072 this.referenceIndex = referenceIndex;
1073 this.useLargeLinePlots = useLargeLinePlots;
1074 this.children = [];
1075
1076 this.tableRow = $('' +
1077 '' +
1078 this.test.name() +
1079 ' | ' +
1080 '' +
1081 this.test.unit() +
1082 ' | ' +
1083 '
');
1084
1085 var runIndex = 0;
1086 var results = this.test.results();
1087 var referenceResult = undefined;
1088
1089 this.resultIndexMap = {};
1090 for (var i = 0; i < results.length; i++) {
1091 while (this.runs[runIndex] !== results[i].run())
1092 runIndex++;
1093 if (runIndex === this.referenceIndex)
1094 referenceResult = results[i];
1095 this.resultIndexMap[runIndex] = i;
1096 }
1097 for (var i = 0; i < this.runs.length; i++) {
1098 var resultIndex = this.resultIndexMap[i];
1099 if (resultIndex === undefined)
1100 this.tableRow.append(this.markupForMissingRun(i == this.referenceIndex));
1101 else
1102 this.tableRow.append(this.markupForRun(results[resultIndex], referenceResult));
1103 }
1104
1105 // Use the test name (without URL) to bind parents and their children
1106 var nameAndURL = this.test.name().split('.');
1107 var benchmarkName = nameAndURL.shift();
1108 this.testName = nameAndURL.shift();
1109 this.hasNoURL = (nameAndURL.length === 0);
1110
1111 if (!this.hasNoURL) {
1112 // Re-join the URL
1113 this.URL = nameAndURL.join('.');
1114 }
1115
1116 this.isImportant = false;
1117 this.hasGraph = false;
1118 this.currentIndentationClass = ''
1119 this.indentLevel = 0;
1120 this.setRowNestedState(COLLAPSED);
1121 this.setVisibility(VISIBLE);
1122 this.prepareToSortByName();
1123 }
1124
1125 TableRow.prototype.hideRowData = function() {
1126 data = this.tableRow.children('td');
1127
1128 for (index in data) {
1129 if (index > 0) {
1130 // Blank out everything except the test name
1131 data[index].innerHTML = '';
1132 }
1133 }
1134 }
1135
1136 TableRow.prototype.prepareToSortByTestResults = function(runId) {
1137 var testResults = this.test.results();
1138 // Find the column in this row that matches the runId and prepare to
1139 // sort by the mean of that test.
1140 for (index in testResults) {
1141 sourceId = testResults[index].run().id();
1142 if (runId === sourceId) {
1143 this.sortValue = testResults[index].mean();
1144 return;
1145 }
1146 }
1147 // This row doesn't have any results for the passed runId
1148 this.sortValue = undefined;
1149 }
1150
1151 TableRow.prototype.prepareToSortRelativeToReference = function(runId) {
1152 var testResults = this.test.results();
1153
1154 // Get index of test results that correspond to the reference column.
1155 var remappedReferenceIndex = this.resultIndexMap[this.referenceIndex];
1156
1157 if (remappedReferenceIndex === undefined) {
1158 // This test has no results in the reference run.
1159 this.sortValue = undefined;
1160 return;
1161 }
1162
1163 otherResults = testResults[remappedReferenceIndex];
1164
1165 // Find the column in this row that matches the runId and prepare to
1166 // sort by the difference from the reference.
1167 for (index in testResults) {
1168 sourceId = testResults[index].run().id();
1169 if (runId === sourceId) {
1170 this.sortValue = testResults[index].percentDifference(otherResults);
1171 if (this.test.biggerIsBetter()) {
1172 // For this test bigger is not better
1173 this.sortValue = -this.sortValue;
1174 }
1175 return;
1176 }
1177 }
1178 // This row doesn't have any results for the passed runId
1179 this.sortValue = undefined;
1180 }
1181
1182 TableRow.prototype.prepareToSortByUnit = function() {
1183 this.sortValue = this.test.unit().toLowerCase();
1184 }
1185
1186 TableRow.prototype.prepareToSortByName = function() {
1187 this.sortValue = this.test.name().toLowerCase();
1188 }
1189
1190 TableRow.prototype.isParentOf = function(row) {
1191 return this.hasNoURL && (this.testName === row.testName);
1192 }
1193
1194 TableRow.prototype.addNestedChild = function(child) {
1195 this.children.push(child);
1196
1197 // Indent child one step in from parent
1198 child.indentLevel = this.indentLevel + INDENTATION;
1199 child.hasGraph = true;
1200 // Start child off as hidden (i.e. collapsed inside parent)
1201 child.setVisibility(INVISIBLE);
1202 child.updateIndentation();
1203 // Show URL in the title column
1204 child.tableRow.children()[0].innerHTML = child.URL;
1205 // Set up class to change background colour of nested rows
1206 if (child.isImportant) {
1207 child.tableRow.addClass('importantNestedRow');
1208 } else {
1209 child.tableRow.addClass('nestedRow');
1210 }
1211 }
1212
1213 TableRow.prototype.setVisibility = function(visibility) {
1214 this.visibility = visibility;
1215 this.tableRow[0].style.display = (visibility === INVISIBLE) ? 'none' : '';
1216 }
1217
1218 TableRow.prototype.setRowNestedState = function(newState) {
1219 this.rowState = newState;
1220 this.updateIndentation();
1221 }
1222
1223 TableRow.prototype.updateIndentation = function() {
1224 var element = this.tableRow.children('td').first();
1225
1226 element.removeClass(this.currentIndentationClass);
1227
1228 this.currentIndentationClass = (this.rowState === COLLAPSED) ? 'collapsed' : 'expanded';
1229
1230 element[0].style.marginLeft = this.indentLevel.toString() + 'px';
1231 element[0].style.float = 'left';
1232
1233 element.addClass(this.currentIndentationClass);
1234 }
1235
1236 TableRow.prototype.addToPage = function() {
1237 $('#container').children('tbody').last().append(this.tableRow);
1238
1239 // Set up click callback
1240 var owningObject = this;
1241 this.tableRow.click(function(event) {
1242 event.preventDefault();
1243 owningObject.toggle();
1244 });
1245
1246 // Add children to the page too
1247 this.children.forEach(function(child) {
1248 child.addToPage();
1249 });
1250 }
1251
1252 TableRow.prototype.removeFromPage = function() {
1253 // Remove children
1254 this.children.forEach(function(child) {
1255 child.removeFromPage();
1256 });
1257 // Remove us
1258 this.tableRow.remove();
1259 }
1260
1261
1262 TableRow.prototype.markupForRun = function(result, referenceResult) {
1263 var comparisonCell = '';
1264 var shouldCompare = result !== referenceResult;
1265 if (shouldCompare) {
1266 var comparisonText = '';
1267 var className = '';
1268
1269 if (referenceResult) {
1270 var percentDifference = referenceResult.percentDifference(result);
1271 if (isNaN(percentDifference)) {
1272 comparisonText = 'Unknown';
1273 className = UNKNOWN_CLASS;
1274 } else if (Math.abs(percentDifference) < SMALLEST_PERCENT_DISPLAYED) {
1275 comparisonText = 'Equal';
1276 // Show equal values in green
1277 className = BETTER_CLASS;
1278 } else {
1279 var better = this.test.biggerIsBetter() ? percentDifference > 0 : percentDifference < 0;
1280 comparisonText = formatPercentage(Math.abs(percentDifference)) + (better ? ' Better' : ' Worse');
1281 className = better ? BETTER_CLASS : WORSE_CLASS;
1282 }
1283
1284 if (!referenceResult.isStatisticallySignificant(result)) {
1285 // Put result in brackets and fade if not statistically significant
1286 className += ' fadeOut';
1287 comparisonText = '(' + comparisonText + ')';
1288 }
1289 }
1290 comparisonCell = 'className + '">' + comparisonText + ' | ';
1291 }
1292
1293 var values = result.values();
1294 var warning = '';
1295 var regressionAnalysis = '';
1296 if (result.histogramValues) {
1297 // Don't calculate regression result for histograms.
1298 } else if (values && values.length > 3) {
1299 regressionResult = linearRegression(values);
1300 regressionAnalysis = 'slope=' + toFixedWidthPrecision(regressionResult.slope)
1301 + ', R^2=' + toFixedWidthPrecision(regressionResult.rSquared);
1302 if (regressionResult.rSquared > 0.6 && Math.abs(regressionResult.slope) > 0.01) {
1303 warning = ' <span class="regression-warning" title="Detected a time dependency with ' + regressionAnalysis + '">' + warningSign + ' </span>';
1304 }
1305 }
1306
1307 var referenceClass = shouldCompare ? '' : 'reference';
1308
1309 var statistics = 'σ=' + toFixedWidthPrecision(result.confidenceIntervalDelta()) + ', min=' + toFixedWidthPrecision(result.min())
1310 + ', max=' + toFixedWidthPrecision(result.max()) + '\n' + regressionAnalysis;
1311
1312 var confidence;
1313 if (isNaN(result.confidenceIntervalDeltaRatio())) {
1314 // Don't bother showing +- Nan as it is meaningless
1315 confidence = '';
1316 } else {
1317 confidence = '± ' + formatPercentage(result.confidenceIntervalDeltaRatio());
1318 }
1319
1320 return '<td class="result ' + referenceClass + '" title="' + statistics + '">' + toFixedWidthPrecision(result.mean())
1321 + '</td><td class="confidenceIntervalDelta ' + referenceClass + '" title="' + statistics + '">' + confidence + warning + '</td>' + comparisonCell;
1322 }
1323
1324 TableRow.prototype.markupForMissingRun = function(isReference) {
1325 if (isReference) {
1326 return '<td colspan=2 class="missingReference">Missing</td>';
1327 }
1328 return '<td colspan=3 class="missing">Missing</td>';
1329 }
1330
1331 TableRow.prototype.openRow = function() {
1332 if (this.rowState === EXPANDED) {
1333 // If we're already expanded, open our children instead
1334 this.children.forEach(function(child) {
1335 child.openRow();
1336 });
1337 return;
1338 }
1339
1340 this.setRowNestedState(EXPANDED);
1341
1342 if (this.hasGraph) {
1343 var firstCell = this.tableRow.children('td').first();
1344 var plot = createPlot(firstCell, this.test, this.useLargeLinePlots);
1345 plot.css({'position': 'absolute', 'z-index': 2});
1346 var offset = this.tableRow.offset();
1347 offset.left += GRAPH_INDENT;
1348 offset.top += this.tableRow.outerHeight();
1349 plot.offset(offset);
1350 this.tableRow.children('td').css({'padding-bottom': plot.outerHeight() + PADDING_UNDER_GRAPH});
1351 }
1352
1353 this.children.forEach(function(child) {
1354 child.setVisibility(VISIBLE);
1355 });
1356
1357 if (this.children.length === 1) {
1358 // If we only have a single child...
1359 var child = this.children[0];
1360 if (child.isImportant) {
1361 // ... and it is important (i.e. the summary row) just open it when
1362 // parent is opened to save needless clicking
1363 child.openRow();
1364 }
1365 }
1366 }
1367
1368 TableRow.prototype.closeRow = function() {
1369 if (this.rowState === COLLAPSED) {
1370 return;
1371 }
1372
1373 this.setRowNestedState(COLLAPSED);
1374
1375 if (this.hasGraph) {
1376 var firstCell = this.tableRow.children('td').first();
1377 firstCell.children('section').remove();
1378 this.tableRow.children('td').css({'padding-bottom': ''});
1379 }
1380
1381 this.children.forEach(function(child) {
1382 // Make children invisible, but leave their collapsed status alone
1383 child.setVisibility(INVISIBLE);
1384 });
1385 }
1386
1387 TableRow.prototype.toggle = function() {
1388 if (this.rowState === EXPANDED) {
1389 this.closeRow();
1390 } else {
1391 this.openRow();
1392 }
1393 return false;
1394 }
1395
1396 function init() {
1397 var runs = [];
1398 var metrics = {};
1399 var deletedRunsById = {};
1400 $.each(JSON.parse(document.getElementById('results-json').textContent), function(index, entry) {
1401 var run = new TestRun(entry);
1402 if (run.isHidden()) {
1403 deletedRunsById[run.id()] = run;
1404 return;
1405 }
1406
1407 runs.push(run);
1408
1409 function addTests(tests) {
1410 for (var testName in tests) {
1411 var rawMetrics = tests[testName].metrics;
1412
1413 for (var metricName in rawMetrics) {
1414 var fullMetricName = testName + ':' + metricName;
1415 var metric = metrics[fullMetricName];
1416 if (!metric) {
1417 metric = new PerfTestMetric(testName, metricName, rawMetrics[metricName].units, rawMetrics[metricName].important);
1418 metrics[fullMetricName] = metric;
1419 }
1420 // std & degrees_of_freedom could be undefined
1421 metric.addResult(
1422 new TestResult(metric, rawMetrics[metricName].current,
1423 run, rawMetrics[metricName]['std'], rawMetrics[metricName]['degrees_of_freedom']));
1424 }
1425 }
1426 }
1427
1428 addTests(entry.tests);
1429 });
1430
1431 var useLargeLinePlots = false;
1432 var referenceIndex = 0;
1433
1434 var testTypeSelector = new TestTypeSelector(metrics);
1435 var buttonHTML = testTypeSelector.buildButtonHTMLForUsedTestTypes();
1436 $('#time-memory').append(buttonHTML);
1437
1438 $('#scatter-line').bind('change', function(event, checkedElement) {
1439 useLargeLinePlots = checkedElement.textContent == 'Line';
1440 displayTable(metrics, runs, testTypeSelector, referenceIndex, useLargeLinePlots);
1441 });
1442
1443 runs.map(function(run, index) {
1444 $('#reference').append('<span value="' + index + '"' + (index == referenceIndex ? ' class="checked"' : '') + ' title="' + run.description() + '">' + run.label() + '</span>');
1445 })
1446
1447 $('#time-memory').bind('change', function(event, checkedElement) {
1448 testTypeSelector.testTypeName = checkedElement.textContent;
1449 displayTable(metrics, runs, testTypeSelector, referenceIndex, useLargeLinePlots);
1450 });
1451
1452 $('#reference').bind('change', function(event, checkedElement) {
1453 referenceIndex = parseInt(checkedElement.getAttribute('value'));
1454 displayTable(metrics, runs, testTypeSelector, referenceIndex, useLargeLinePlots);
1455 });
1456
1457 displayTable(metrics, runs, testTypeSelector, referenceIndex, useLargeLinePlots);
1458
1459 $('.checkbox').each(function(index, checkbox) {
1460 $(checkbox).children('span').click(function(event) {
1461 if ($(this).hasClass('checked'))
1462 return;
1463 $(checkbox).children('span').removeClass('checked');
1464 $(this).addClass('checked');
1465 $(checkbox).trigger('change', $(this));
1466 });
1467 });
1468
1469 runToUndelete = deletedRunsById[undeleteManager.mostRecentlyDeletedId()];
1470
1471 if (runToUndelete) {
1472 $('#undelete').html('Undelete ' + runToUndelete.label());
1473 $('#undelete').attr('title', runToUndelete.description());
1474 $('#undelete').click(function(event) {
1475 runToUndelete.show();
1476 undeleteManager.undeleteMostRecent();
1477 location.reload();
1478 });
1479 } else {
1480 $('#undelete').hide();
1481 }
1482 }
1483
1484 </script>
1485 <script id="results-json" type="application/json">%json_results%</script>
1486 <script id="units-json" type="application/json">%json_units%</script>
1487 </body>
1488 </html>
1489