Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / tools / telemetry / support / html_output / results-template.html
blob3801aded5dca0a034389a590b4fabcd265dd5eb1
1 <!DOCTYPE html>
2 <html>
3 <head>
4 <title>Telemetry Performance Test Results</title>
5 <style type="text/css">
7 section {
8 background: white;
9 padding: 10px;
10 position: relative;
13 .collapsed:before {
14 color: #ccc;
15 content: '\25B8\00A0';
18 .expanded:before {
19 color: #eee;
20 content: '\25BE\00A0';
23 .line-plots {
24 padding-left: 25px;
27 .line-plots > div {
28 display: inline-block;
29 width: 90px;
30 height: 40px;
31 margin-right: 10px;
34 .lage-line-plots {
35 padding-left: 25px;
38 .large-line-plots > div, .histogram-plots > div {
39 display: inline-block;
40 width: 400px;
41 height: 200px;
42 margin-right: 10px;
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;
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;
68 .closeButton:hover {
69 background: #F09C9C;
72 .label {
73 cursor: text;
76 .label:hover {
77 background: #ffcc66;
80 section h1 {
81 text-align: center;
82 font-size: 1em;
85 section .tooltip {
86 position: absolute;
87 text-align: center;
88 background: #ffcc66;
89 border-radius: 5px;
90 padding: 0px 5px;
93 body {
94 padding: 0px;
95 margin: 0px;
96 font-family: sans-serif;
99 table {
100 background: white;
101 width: 100%;
104 table, td, th {
105 border-collapse: collapse;
106 padding: 5px;
107 white-space: nowrap;
110 .highlight:hover {
111 color: #202020;
112 background: #e0e0e0;
115 .nestedRow {
116 background: #f8f8f8;
119 .importantNestedRow {
120 background: #e0e0e0;
121 font-weight: bold;
124 table td {
125 position: relative;
128 th, td {
129 cursor: pointer;
130 cursor: hand;
133 th {
134 background: #e6eeee;
135 background: linear-gradient(rgb(244, 244, 244), rgb(217, 217, 217));
136 border: 1px solid #ccc;
139 th.sortUp:after {
140 content: ' \25BE';
143 th.sortDown:after {
144 content: ' \25B4';
147 td.comparison, td.result {
148 text-align: right;
151 td.better {
152 color: #6c6;
155 td.fadeOut {
156 opacity: 0.5;
159 td.unknown {
160 color: #ccc;
163 td.worse {
164 color: #c66;
167 td.reference {
168 font-style: italic;
169 font-weight: bold;
170 color: #444;
173 td.missing {
174 color: #ccc;
175 text-align: center;
178 td.missingReference {
179 color: #ccc;
180 text-align: center;
181 font-style: italic;
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;
198 .checkbox span {
199 display: inline-block;
200 line-height: 100%;
201 padding: 5px 8px;
202 border: outset 1px transparent;
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;
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;
226 .openAllButton:hover {
227 background: #60f060;
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;
244 .closeAllButton:hover {
245 background: #f04040;
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 class="checked">Time</span><span>Memory</span></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>
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;
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;
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);
309 catch (e) {
310 console.error(e, e.stack);
313 } else {
314 values = [];
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);
328 return metric.scalingFactor() * Statistics.confidenceIntervalDelta(0.95, values.length,
329 Statistics.sum(values), Statistics.squareSum(values));
331 this.confidenceIntervalDeltaRatio = function () { return this.confidenceIntervalDelta() / this.mean(); }
332 this.percentDifference = function(other) {
333 if (other === undefined) {
334 return undefined;
336 return (other.unscaledMean() - this.unscaledMean()) / this.unscaledMean();
338 this.isStatisticallySignificant = function (other) {
339 if (other === undefined) {
340 return false;
342 var diff = Math.abs(other.mean() - this.mean());
343 return diff > this.confidenceIntervalDelta() && diff > other.confidenceIntervalDelta();
345 this.run = function () { return associatedRun; }
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'];
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();
363 var labelKey = 'telemetry_label_' + this.id();
364 var hiddenKey = 'telemetry_hide_' + this.id();
367 function PerfTestMetric(name, metric, unit, isImportant) {
368 var testResults = [];
369 var cachedUnit = null;
370 var cachedScalingFactor = null;
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;
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;
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.'));
403 this.addResult = function (newResult) {
404 testResults.push(newResult);
405 cachedUnit = null;
406 cachedScalingFactor = null;
408 this.results = function () { return testResults; }
409 this.scalingFactor = function() {
410 computeScalingFactorIfNeeded();
411 return cachedScalingFactor;
413 this.unit = function () {
414 computeScalingFactorIfNeeded();
415 return cachedUnit;
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;
427 return window.unitToBiggerIsBetter[unit];
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 = [];
440 this.ondelete = function(id) {
441 undeleteIds.push(id);
442 localStorage[key] = JSON.stringify(undeleteIds);
444 this.undeleteMostRecent = function() {
445 if (!this.mostRecentlyDeletedId())
446 return;
447 undeleteIds.pop();
448 localStorage[key] = JSON.stringify(undeleteIds);
450 this.mostRecentlyDeletedId = function() {
451 if (!undeleteIds.length)
452 return undefined;
453 return undeleteIds[undeleteIds.length-1];
456 var undeleteManager = new UndeleteManager();
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}};
465 var mainPlotOptions = {
466 xaxis: {
467 min: -0.5,
468 tickSize: 1,
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,
484 var linePlotOptions = {
485 yaxis: { show: false },
486 xaxis: { show: false },
487 lines: { show: true },
488 grid: { borderWidth: 1, borderColor: '#ccc' },
489 colors: [ plotColor ]
492 var largeLinePlotOptions = {
493 xaxis: {
494 show: true,
495 tickDecimals: 0,
497 lines: { show: true },
498 grid: { borderWidth: 1, borderColor: '#ccc' },
499 colors: [ plotColor ]
502 var histogramPlotOptions = {
503 bars: {show: true, fill: 1}
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'));
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);
527 var plotContainer = section.children('.plot');
528 var minIsZero = true;
529 attachPlot(test, plotContainer, minIsZero);
531 attachLinePlots(test, section.children('.line-plots'), useLargeLinePlots);
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();
544 plotContainer.mouseout(function () {
545 tooltip.hide();
547 plotContainer.click(function (event) {
548 event.preventDefault();
549 minIsZero = !minIsZero;
550 attachPlot(test, plotContainer, minIsZero);
553 return section;
556 function attachLinePlots(test, container, useLargeLinePlots) {
557 var results = test.results();
558 var attachedPlot = false;
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;
572 for (var i = 0; i < results.length; i++) {
573 container.append('<div></div>');
574 var values = results[i].values();
575 if (!values)
576 continue;
577 attachedPlot = true;
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}});
590 $.plot(container.children().last(), [values.map(function (value, index) { return [index, value]; })], options);
592 if (!attachedPlot)
593 container.children().remove();
596 function attachHistogramPlots(test, container) {
597 var results = test.results();
598 var attachedPlot = false;
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;
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);
614 var xmax = bucket.high * 1.1;
615 var ymax = max_count * 1.1;
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);
635 if (!attachedPlot)
636 container.children().remove();
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>');
647 function attachPlot(test, plotContainer, minIsZero) {
648 var results = test.results();
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 }, []);
655 var plotData = [$.extend(true, {}, subpointsPlotOptions, {data: values})];
656 plotData.push({id: '&mu;', data: results.map(function (result, index) { return [index, result.mean()]; }), color: plotColor});
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}});
665 currentPlotOptions.xaxis.max = results.length - 0.5;
666 currentPlotOptions.xaxis.ticks = results.map(function (result, index) { return [index, result.run().label()]; });
668 $.plot(plotContainer, plotData, currentPlotOptions);
671 function toFixedWidthPrecision(value) {
672 var decimal = value.toFixed(2);
673 return decimal;
676 function formatPercentage(fraction) {
677 var percentage = fraction * 100;
678 return (fraction * 100).toFixed(2) + '%';
681 function setUpSortClicks(runs)
683 $('#nameColumn').click(sortByName);
685 $('#unitColumn').click(sortByUnit);
687 runs.forEach(function(run) {
688 $('#' + run.id()).click(sortByResult);
689 $('#' + run.id() + COMPARISON_SUFFIX).click(sortByReference);
693 var topLevelRows;
694 var allTableRows;
696 function createTable(tests, runs, shouldIgnoreMemory, referenceIndex, useLargeLinePlots) {
697 var resultHeaders = runs.map(function (run, index) {
698 var header = '<th id="' + run.id() + '" ' +
699 'colspan=2 ' +
700 'title="' + run.description() + '">' +
701 '<span class="label" ' +
702 'title="Edit run label">' +
703 run.label() +
704 '</span>' +
705 '<div class="closeButton" ' +
706 'title="Delete run">' +
707 '&times;' +
708 '</div>' +
709 '</th>';
710 if (index !== referenceIndex) {
711 header += '<th id="' + run.id() + COMPARISON_SUFFIX + '" ' +
712 'title="Sort by better/worse">' +
713 '&Delta;' +
714 '</th>';
716 return header;
719 resultHeaders = resultHeaders.join('');
721 htmlString = '<thead>' +
722 '<tr>' +
723 '<th id="nameColumn">' +
724 '<div class="openAllButton" ' +
725 'title="Open all rows or graphs">' +
726 'Open All' +
727 '</div>' +
728 '<div class="closeAllButton" ' +
729 'title="Close all rows">' +
730 'Close All' +
731 '</div>' +
732 'Test' +
733 '</th>' +
734 '<th id="unitColumn">' +
735 'Unit' +
736 '</th>' +
737 resultHeaders +
738 '</tr>' +
739 '</head>' +
740 '<tbody>' +
741 '</tbody>';
743 $('#container').html(htmlString);
745 var testNames = [];
746 for (testName in tests)
747 testNames.push(testName);
749 allTableRows = [];
750 testNames.forEach(function(testName) {
751 var test = tests[testName];
752 if (test.isMemoryTest() === shouldIgnoreMemory) {
753 return;
755 allTableRows.push(new TableRow(runs, test, referenceIndex, useLargeLinePlots));
758 // Build a list of top level rows with attached children
759 topLevelRows = [];
760 allTableRows.forEach(function(row) {
761 // Add us to top level if we are a top-level row...
762 if (row.hasNoURL) {
763 topLevelRows.push(row);
764 // Add a duplicate child row that holds the graph for the parent
765 var graphHolder = new TableRow(runs, row.test, referenceIndex, useLargeLinePlots);
766 graphHolder.isImportant = true;
767 graphHolder.URL = 'Summary';
768 graphHolder.hideRowData();
769 allTableRows.push(graphHolder);
770 row.addNestedChild(graphHolder);
771 return;
774 // ...or add us to our parent if we have one ...
775 for (var i = 0; i < allTableRows.length; i++) {
776 if (allTableRows[i].isParentOf(row)) {
777 allTableRows[i].addNestedChild(row);
778 return;
782 // ...otherwise this result is orphaned, display it at top level with a graph
783 row.hasGraph = true;
784 topLevelRows.push(row);
787 buildTable(topLevelRows);
789 $('.closeButton').click(function(event) {
790 for (var i = 0; i < runs.length; i++) {
791 if (runs[i].id() == event.target.parentNode.id) {
792 runs[i].hide();
793 undeleteManager.ondelete(runs[i].id());
794 location.reload();
795 break;
798 event.stopPropagation();
801 $('.closeAllButton').click(function(event) {
802 for (var i = 0; i < allTableRows.length; i++) {
803 allTableRows[i].closeRow();
805 event.stopPropagation();
808 $('.openAllButton').click(function(event) {
809 for (var i = 0; i < topLevelRows.length; i++) {
810 topLevelRows[i].openRow();
812 event.stopPropagation();
815 setUpSortClicks(runs);
817 $('.label').click(function(event) {
818 for (var i = 0; i < runs.length; i++) {
819 if (runs[i].id() == event.target.parentNode.id) {
820 $(event.target).replaceWith('<input id="labelEditor" type="text" value="' + runs[i].label() + '">');
821 $('#labelEditor').focusout(function() {
822 runs[i].setLabel(this.value);
823 location.reload();
825 $('#labelEditor').keypress(function(event) {
826 if (event.which == 13) {
827 runs[i].setLabel(this.value);
828 location.reload();
831 $('#labelEditor').click(function (event) {
832 event.stopPropagation();
834 $('#labelEditor').mousedown(function (event) {
835 event.stopPropagation();
837 $('#labelEditor').select();
838 break;
841 event.stopPropagation();
845 function validForSorting(row) {
846 return ($.type(row.sortValue) === 'string') || !isNaN(row.sortValue);
849 var sortDirection = 1;
851 function sortRows(rows) {
852 rows.sort(
853 function(rowA,rowB) {
854 if (validForSorting(rowA) !== validForSorting(rowB)) {
855 // Sort valid values upwards when compared to invalid
856 if (validForSorting(rowA)) {
857 return -1;
859 if (validForSorting(rowB)) {
860 return 1;
864 // Some rows always sort to the top
865 if (rowA.isImportant) {
866 return -1;
868 if (rowB.isImportant) {
869 return 1;
872 if (rowA.sortValue === rowB.sortValue) {
873 // Sort identical values by name to keep the sort stable,
874 // always keep name alphabetical (even if a & b sort values
875 // are invalid)
876 return rowA.test.name() > rowB.test.name() ? 1 : -1;
879 return rowA.sortValue > rowB.sortValue ? sortDirection : -sortDirection;
880 } );
882 // Sort the rows' children
883 rows.forEach(function(row) {
884 sortRows(row.children);
888 function buildTable(rows) {
889 rows.forEach(function(row) {
890 row.removeFromPage();
893 sortRows(rows);
895 rows.forEach(function(row) {
896 row.addToPage();
900 var activeSortHeaderElement = undefined;
901 var columnSortDirection = {};
903 function determineColumnSortDirection(element) {
904 columnDirection = columnSortDirection[element.id];
906 if (columnDirection === undefined) {
907 // First time we've sorted this row, default to down
908 columnSortDirection[element.id] = SORT_DOWN_CLASS;
909 } else if (element === activeSortHeaderElement) {
910 // Clicking on same header again, swap direction
911 columnSortDirection[element.id] = (columnDirection === SORT_UP_CLASS) ? SORT_DOWN_CLASS : SORT_UP_CLASS;
915 function updateSortDirection(element) {
916 // Remove old header's sort arrow
917 if (activeSortHeaderElement !== undefined) {
918 activeSortHeaderElement.classList.remove(columnSortDirection[activeSortHeaderElement.id]);
921 determineColumnSortDirection(element);
923 sortDirection = (columnSortDirection[element.id] === SORT_UP_CLASS) ? 1 : -1;
925 // Add new header's sort arrow
926 element.classList.add(columnSortDirection[element.id]);
927 activeSortHeaderElement = element;
930 function sortByName(event) {
931 updateSortDirection(event.toElement);
933 allTableRows.forEach(function(row) {
934 row.prepareToSortByName();
937 buildTable(topLevelRows);
940 function sortByUnit(event) {
941 updateSortDirection(event.toElement);
943 allTableRows.forEach(function(row) {
944 row.prepareToSortByUnit();
947 buildTable(topLevelRows);
950 function sortByResult(event) {
951 updateSortDirection(event.toElement);
953 var runId = event.target.id;
955 allTableRows.forEach(function(row) {
956 row.prepareToSortByTestResults(runId);
959 buildTable(topLevelRows);
962 function sortByReference(event) {
963 updateSortDirection(event.toElement);
965 // The element ID has _compare appended to allow us to set up a click event
966 // remove the _compare to return a useful Id
967 var runIdWithCompare = event.target.id;
968 var runId = runIdWithCompare.split('_')[0];
970 allTableRows.forEach(function(row) {
971 row.prepareToSortRelativeToReference(runId);
974 buildTable(topLevelRows);
977 function linearRegression(points) {
978 // Implement http://www.easycalculation.com/statistics/learn-correlation.php.
979 // x = magnitude
980 // y = iterations
981 var sumX = 0;
982 var sumY = 0;
983 var sumXSquared = 0;
984 var sumYSquared = 0;
985 var sumXTimesY = 0;
987 for (var i = 0; i < points.length; i++) {
988 var x = i;
989 var y = points[i];
990 sumX += x;
991 sumY += y;
992 sumXSquared += x * x;
993 sumYSquared += y * y;
994 sumXTimesY += x * y;
997 var r = (points.length * sumXTimesY - sumX * sumY) /
998 Math.sqrt((points.length * sumXSquared - sumX * sumX) *
999 (points.length * sumYSquared - sumY * sumY));
1001 if (isNaN(r) || r == Math.Infinity)
1002 r = 0;
1004 var slope = (points.length * sumXTimesY - sumX * sumY) / (points.length * sumXSquared - sumX * sumX);
1005 var intercept = sumY / points.length - slope * sumX / points.length;
1006 return {slope: slope, intercept: intercept, rSquared: r * r};
1009 var warningSign = '<svg viewBox="0 0 100 100" style="width: 18px; height: 18px; vertical-align: bottom;" version="1.1">'
1010 + '<polygon fill="red" points="50,10 90,80 10,80 50,10" stroke="red" stroke-width="10" stroke-linejoin="round" />'
1011 + '<polygon fill="white" points="47,30 48,29, 50, 28.7, 52,29 53,30 50,60" stroke="white" stroke-width="10" stroke-linejoin="round" />'
1012 + '<circle cx="50" cy="73" r="6" fill="white" />'
1013 + '</svg>';
1015 function TableRow(runs, test, referenceIndex, useLargeLinePlots) {
1016 this.runs = runs;
1017 this.test = test;
1018 this.referenceIndex = referenceIndex;
1019 this.useLargeLinePlots = useLargeLinePlots;
1020 this.children = [];
1022 this.tableRow = $('<tr class="highlight">' +
1023 '<td class="test collapsed" >' +
1024 this.test.name() +
1025 '</td>' +
1026 '<td class="unit">' +
1027 this.test.unit() +
1028 '</td>' +
1029 '</tr>');
1031 var runIndex = 0;
1032 var results = this.test.results();
1033 var referenceResult = undefined;
1035 this.resultIndexMap = {};
1036 for (var i = 0; i < results.length; i++) {
1037 while (this.runs[runIndex] !== results[i].run())
1038 runIndex++;
1039 if (runIndex === this.referenceIndex)
1040 referenceResult = results[i];
1041 this.resultIndexMap[runIndex] = i;
1043 for (var i = 0; i < this.runs.length; i++) {
1044 var resultIndex = this.resultIndexMap[i];
1045 if (resultIndex === undefined)
1046 this.tableRow.append(this.markupForMissingRun(i == this.referenceIndex));
1047 else
1048 this.tableRow.append(this.markupForRun(results[resultIndex], referenceResult));
1051 // Use the test name (without URL) to bind parents and their children
1052 var nameAndURL = this.test.name().split('.');
1053 var benchmarkName = nameAndURL.shift();
1054 this.testName = nameAndURL.shift();
1055 this.hasNoURL = (nameAndURL.length === 0);
1057 if (!this.hasNoURL) {
1058 // Re-join the URL
1059 this.URL = nameAndURL.join('.');
1062 this.isImportant = false;
1063 this.hasGraph = false;
1064 this.currentIndentationClass = ''
1065 this.indentLevel = 0;
1066 this.setRowNestedState(COLLAPSED);
1067 this.setVisibility(VISIBLE);
1068 this.prepareToSortByName();
1071 TableRow.prototype.hideRowData = function() {
1072 data = this.tableRow.children('td');
1074 for (index in data) {
1075 if (index > 0) {
1076 // Blank out everything except the test name
1077 data[index].innerHTML = '';
1082 TableRow.prototype.prepareToSortByTestResults = function(runId) {
1083 var testResults = this.test.results();
1084 // Find the column in this row that matches the runId and prepare to
1085 // sort by the mean of that test.
1086 for (index in testResults) {
1087 sourceId = testResults[index].run().id();
1088 if (runId === sourceId) {
1089 this.sortValue = testResults[index].mean();
1090 return;
1093 // This row doesn't have any results for the passed runId
1094 this.sortValue = undefined;
1097 TableRow.prototype.prepareToSortRelativeToReference = function(runId) {
1098 var testResults = this.test.results();
1100 // Get index of test results that correspond to the reference column.
1101 var remappedReferenceIndex = this.resultIndexMap[this.referenceIndex];
1103 if (remappedReferenceIndex === undefined) {
1104 // This test has no results in the reference run.
1105 this.sortValue = undefined;
1106 return;
1109 otherResults = testResults[remappedReferenceIndex];
1111 // Find the column in this row that matches the runId and prepare to
1112 // sort by the difference from the reference.
1113 for (index in testResults) {
1114 sourceId = testResults[index].run().id();
1115 if (runId === sourceId) {
1116 this.sortValue = testResults[index].percentDifference(otherResults);
1117 if (this.test.biggerIsBetter()) {
1118 // For this test bigger is not better
1119 this.sortValue = -this.sortValue;
1121 return;
1124 // This row doesn't have any results for the passed runId
1125 this.sortValue = undefined;
1128 TableRow.prototype.prepareToSortByUnit = function() {
1129 this.sortValue = this.test.unit().toLowerCase();
1132 TableRow.prototype.prepareToSortByName = function() {
1133 this.sortValue = this.test.name().toLowerCase();
1136 TableRow.prototype.isParentOf = function(row) {
1137 return this.hasNoURL && (this.testName === row.testName);
1140 TableRow.prototype.addNestedChild = function(child) {
1141 this.children.push(child);
1143 // Indent child one step in from parent
1144 child.indentLevel = this.indentLevel + INDENTATION;
1145 child.hasGraph = true;
1146 // Start child off as hidden (i.e. collapsed inside parent)
1147 child.setVisibility(INVISIBLE);
1148 child.updateIndentation();
1149 // Show URL in the title column
1150 child.tableRow.children()[0].innerHTML = child.URL;
1151 // Set up class to change background colour of nested rows
1152 if (child.isImportant) {
1153 child.tableRow.addClass('importantNestedRow');
1154 } else {
1155 child.tableRow.addClass('nestedRow');
1159 TableRow.prototype.setVisibility = function(visibility) {
1160 this.visibility = visibility;
1161 this.tableRow[0].style.display = (visibility === INVISIBLE) ? 'none' : '';
1164 TableRow.prototype.setRowNestedState = function(newState) {
1165 this.rowState = newState;
1166 this.updateIndentation();
1169 TableRow.prototype.updateIndentation = function() {
1170 var element = this.tableRow.children('td').first();
1172 element.removeClass(this.currentIndentationClass);
1174 this.currentIndentationClass = (this.rowState === COLLAPSED) ? 'collapsed' : 'expanded';
1176 element[0].style.marginLeft = this.indentLevel.toString() + 'px';
1177 element[0].style.float = 'left';
1179 element.addClass(this.currentIndentationClass);
1182 TableRow.prototype.addToPage = function() {
1183 $('#container').children('tbody').last().append(this.tableRow);
1185 // Set up click callback
1186 var owningObject = this;
1187 this.tableRow.click(function(event) {
1188 event.preventDefault();
1189 owningObject.toggle();
1192 // Add children to the page too
1193 this.children.forEach(function(child) {
1194 child.addToPage();
1198 TableRow.prototype.removeFromPage = function() {
1199 // Remove children
1200 this.children.forEach(function(child) {
1201 child.removeFromPage();
1203 // Remove us
1204 this.tableRow.remove();
1208 TableRow.prototype.markupForRun = function(result, referenceResult) {
1209 var comparisonCell = '';
1210 var shouldCompare = result !== referenceResult;
1211 if (shouldCompare) {
1212 var comparisonText = '';
1213 var className = '';
1215 if (referenceResult) {
1216 var percentDifference = referenceResult.percentDifference(result);
1217 if (isNaN(percentDifference)) {
1218 comparisonText = 'Unknown';
1219 className = UNKNOWN_CLASS;
1220 } else if (Math.abs(percentDifference) < SMALLEST_PERCENT_DISPLAYED) {
1221 comparisonText = 'Equal';
1222 // Show equal values in green
1223 className = BETTER_CLASS;
1224 } else {
1225 var better = this.test.biggerIsBetter() ? percentDifference > 0 : percentDifference < 0;
1226 comparisonText = formatPercentage(Math.abs(percentDifference)) + (better ? ' Better' : ' Worse');
1227 className = better ? BETTER_CLASS : WORSE_CLASS;
1230 if (!referenceResult.isStatisticallySignificant(result)) {
1231 // Put result in brackets and fade if not statistically significant
1232 className += ' fadeOut';
1233 comparisonText = '(' + comparisonText + ')';
1236 comparisonCell = '<td class="comparison ' + className + '">' + comparisonText + '</td>';
1239 var values = result.values();
1240 var warning = '';
1241 var regressionAnalysis = '';
1242 if (result.histogramValues) {
1243 // Don't calculate regression result for histograms.
1244 } else if (values && values.length > 3) {
1245 regressionResult = linearRegression(values);
1246 regressionAnalysis = 'slope=' + toFixedWidthPrecision(regressionResult.slope)
1247 + ', R^2=' + toFixedWidthPrecision(regressionResult.rSquared);
1248 if (regressionResult.rSquared > 0.6 && Math.abs(regressionResult.slope) > 0.01) {
1249 warning = ' <span class="regression-warning" title="Detected a time dependency with ' + regressionAnalysis + '">' + warningSign + ' </span>';
1253 var referenceClass = shouldCompare ? '' : 'reference';
1255 var statistics = '&sigma;=' + toFixedWidthPrecision(result.confidenceIntervalDelta()) + ', min=' + toFixedWidthPrecision(result.min())
1256 + ', max=' + toFixedWidthPrecision(result.max()) + '\n' + regressionAnalysis;
1258 var confidence;
1259 if (isNaN(result.confidenceIntervalDeltaRatio())) {
1260 // Don't bother showing +- Nan as it is meaningless
1261 confidence = '';
1262 } else {
1263 confidence = '&plusmn; ' + formatPercentage(result.confidenceIntervalDeltaRatio());
1266 return '<td class="result ' + referenceClass + '" title="' + statistics + '">' + toFixedWidthPrecision(result.mean())
1267 + '</td><td class="confidenceIntervalDelta ' + referenceClass + '" title="' + statistics + '">' + confidence + warning + '</td>' + comparisonCell;
1270 TableRow.prototype.markupForMissingRun = function(isReference) {
1271 if (isReference) {
1272 return '<td colspan=2 class="missingReference">Missing</td>';
1274 return '<td colspan=3 class="missing">Missing</td>';
1277 TableRow.prototype.openRow = function() {
1278 if (this.rowState === EXPANDED) {
1279 // If we're already expanded, open our children instead
1280 this.children.forEach(function(child) {
1281 child.openRow();
1283 return;
1286 this.setRowNestedState(EXPANDED);
1288 if (this.hasGraph) {
1289 var firstCell = this.tableRow.children('td').first();
1290 var plot = createPlot(firstCell, this.test, this.useLargeLinePlots);
1291 plot.css({'position': 'absolute', 'z-index': 2});
1292 var offset = this.tableRow.offset();
1293 offset.left += GRAPH_INDENT;
1294 offset.top += this.tableRow.outerHeight();
1295 plot.offset(offset);
1296 this.tableRow.children('td').css({'padding-bottom': plot.outerHeight() + PADDING_UNDER_GRAPH});
1299 this.children.forEach(function(child) {
1300 child.setVisibility(VISIBLE);
1303 if (this.children.length === 1) {
1304 // If we only have a single child...
1305 var child = this.children[0];
1306 if (child.isImportant) {
1307 // ... and it is important (i.e. the summary row) just open it when
1308 // parent is opened to save needless clicking
1309 child.openRow();
1314 TableRow.prototype.closeRow = function() {
1315 if (this.rowState === COLLAPSED) {
1316 return;
1319 this.setRowNestedState(COLLAPSED);
1321 if (this.hasGraph) {
1322 var firstCell = this.tableRow.children('td').first();
1323 firstCell.children('section').remove();
1324 this.tableRow.children('td').css({'padding-bottom': ''});
1327 this.children.forEach(function(child) {
1328 // Make children invisible, but leave their collapsed status alone
1329 child.setVisibility(INVISIBLE);
1333 TableRow.prototype.toggle = function() {
1334 if (this.rowState === EXPANDED) {
1335 this.closeRow();
1336 } else {
1337 this.openRow();
1339 return false;
1342 function init() {
1343 var runs = [];
1344 var metrics = {};
1345 var deletedRunsById = {};
1346 $.each(JSON.parse(document.getElementById('results-json').textContent), function (index, entry) {
1347 var run = new TestRun(entry);
1348 if (run.isHidden()) {
1349 deletedRunsById[run.id()] = run;
1350 return;
1353 runs.push(run);
1355 function addTests(tests) {
1356 for (var testName in tests) {
1357 var rawMetrics = tests[testName].metrics;
1359 for (var metricName in rawMetrics) {
1360 var fullMetricName = testName + ':' + metricName;
1361 var metric = metrics[fullMetricName];
1362 if (!metric) {
1363 metric = new PerfTestMetric(testName, metricName, rawMetrics[metricName].units, rawMetrics[metricName].important);
1364 metrics[fullMetricName] = metric;
1366 // std & degrees_of_freedom could be undefined
1367 metric.addResult(
1368 new TestResult(metric, rawMetrics[metricName].current,
1369 run, rawMetrics[metricName]['std'], rawMetrics[metricName]['degrees_of_freedom']));
1374 addTests(entry.tests);
1377 var useLargeLinePlots = false;
1378 var shouldIgnoreMemory= true;
1379 var referenceIndex = 0;
1381 createTable(metrics, runs, shouldIgnoreMemory, referenceIndex, useLargeLinePlots);
1383 $('#time-memory').bind('change', function (event, checkedElement) {
1384 shouldIgnoreMemory = checkedElement.textContent == 'Time';
1385 createTable(metrics, runs, shouldIgnoreMemory, referenceIndex, useLargeLinePlots);
1388 $('#scatter-line').bind('change', function (event, checkedElement) {
1389 useLargeLinePlots = checkedElement.textContent == 'Line';
1390 createTable(metrics, runs, shouldIgnoreMemory, referenceIndex, useLargeLinePlots);
1393 runs.map(function (run, index) {
1394 $('#reference').append('<span value="' + index + '"' + (index == referenceIndex ? ' class="checked"' : '') + ' title="' + run.description() + '">' + run.label() + '</span>');
1397 $('#reference').bind('change', function (event, checkedElement) {
1398 referenceIndex = parseInt(checkedElement.getAttribute('value'));
1399 createTable(metrics, runs, shouldIgnoreMemory, referenceIndex, useLargeLinePlots);
1402 $('.checkbox').each(function (index, checkbox) {
1403 $(checkbox).children('span').click(function (event) {
1404 if ($(this).hasClass('checked'))
1405 return;
1406 $(checkbox).children('span').removeClass('checked');
1407 $(this).addClass('checked');
1408 $(checkbox).trigger('change', $(this));
1412 runToUndelete = deletedRunsById[undeleteManager.mostRecentlyDeletedId()];
1414 if (runToUndelete) {
1415 $('#undelete').html('Undelete ' + runToUndelete.label());
1416 $('#undelete').attr('title', runToUndelete.description());
1417 $('#undelete').click(function (event) {
1418 runToUndelete.show();
1419 undeleteManager.undeleteMostRecent();
1420 location.reload();
1422 } else {
1423 $('#undelete').hide();
1427 </script>
1428 <script id="results-json" type="application/json">%json_results%</script>
1429 <script id="units-json" type="application/json">%json_units%</script>
1430 </body>
1431 </html>