Change next_proto member type.
[chromium-blink-merge.git] / tools / telemetry / support / html_output / results-template.html
blobebc98a074690e1e71d2fe0c733e0f368a7aba420
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>
256 Run Telemetry 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) {
283 if (values[0] instanceof Array) {
284 var flattenedValues = [];
285 for (var i = 0; i < values.length; i++)
286 flattenedValues = flattenedValues.concat(values[i]);
287 values = flattenedValues;
290 if (jQuery.type(values[0]) === 'string') {
291 try {
292 var current = JSON.parse(values[0]);
293 if (current.params.type === 'HISTOGRAM') {
294 this.histogramValues = current;
295 // Histogram results have no values (per se). Instead we calculate
296 // the values from the histogram bins.
297 var values = [];
298 var buckets = current.buckets
299 for (var i = 0; i < buckets.length; i++) {
300 var bucket = buckets[i];
301 var bucket_mean = (bucket.high + bucket.low) / 2;
302 for (var b = 0; b < bucket.count; b++) {
303 values.push(bucket_mean);
308 catch (e) { /* ignore, assume not a JSON string */ }
311 this.test = function () { return metric; }
312 this.values = function () { return values.map(function (value) { return metric.scalingFactor() * value; }); }
313 this.unscaledMean = function () { return Statistics.sum(values) / values.length; }
314 this.mean = function () { return metric.scalingFactor() * this.unscaledMean(); }
315 this.min = function () { return metric.scalingFactor() * Statistics.min(values); }
316 this.max = function () { return metric.scalingFactor() * Statistics.max(values); }
317 this.confidenceIntervalDelta = function () {
318 return metric.scalingFactor() * Statistics.confidenceIntervalDelta(0.95, values.length,
319 Statistics.sum(values), Statistics.squareSum(values));
321 this.confidenceIntervalDeltaRatio = function () { return this.confidenceIntervalDelta() / this.mean(); }
322 this.percentDifference = function(other) {
323 if (other === undefined) {
324 return undefined;
326 return (other.unscaledMean() - this.unscaledMean()) / this.unscaledMean();
328 this.isStatisticallySignificant = function (other) {
329 if (other === undefined) {
330 return false;
332 var diff = Math.abs(other.mean() - this.mean());
333 return diff > this.confidenceIntervalDelta() && diff > other.confidenceIntervalDelta();
335 this.run = function () { return associatedRun; }
338 function TestRun(entry) {
339 this.id = function() { return entry['buildTime'].replace(/[:.-]/g,''); }
340 this.revision = function () { return entry['revision']; }
341 this.label = function () {
342 if (labelKey in localStorage)
343 return localStorage[labelKey];
344 if (entry['label'])
345 return entry['label'];
346 return 'r' + this.revision();
348 this.setLabel = function(label) { localStorage[labelKey] = label; }
349 this.isHidden = function() { return localStorage[hiddenKey]; }
350 this.hide = function() { localStorage[hiddenKey] = true; }
351 this.show = function() { localStorage.removeItem(hiddenKey); }
352 this.description = function() {
353 var label = this.label();
354 if (label != 'r' + this.revision())
355 label = ' ' + label;
356 else
357 label = '';
358 return new Date(entry['buildTime']).toLocaleString() + '\n' + entry['platform'] + ' r' + entry['revision'] + label;
361 var labelKey = 'telemetry_label_' + this.id();
362 var hiddenKey = 'telemetry_hide_' + this.id();
365 function PerfTestMetric(name, metric, unit, isImportant) {
366 var testResults = [];
367 var cachedUnit = null;
368 var cachedScalingFactor = null;
370 // We can't do this in TestResult because all results for each test need to share the same unit and the same scaling factor.
371 function computeScalingFactorIfNeeded() {
372 // FIXME: We shouldn't be adjusting units on every test result.
373 // We can only do this on the first test.
374 if (!testResults.length || cachedUnit)
375 return;
377 var mean = testResults[0].unscaledMean(); // FIXME: We should look at all values.
378 var kilo = unit == 'bytes' ? 1024 : 1000;
379 if (mean > 10 * kilo * kilo && unit != 'ms') {
380 cachedScalingFactor = 1 / kilo / kilo;
381 cachedUnit = 'M ' + unit;
382 } else if (mean > 10 * kilo) {
383 cachedScalingFactor = 1 / kilo;
384 cachedUnit = unit == 'ms' ? 's' : ('K ' + unit);
385 } else {
386 cachedScalingFactor = 1;
387 cachedUnit = unit;
391 this.name = function () { return name + ':' + metric; }
392 this.isImportant = isImportant;
393 this.isMemoryTest = function () {
394 return (unit == 'kb' ||
395 unit == 'KB' ||
396 unit == 'MB' ||
397 unit == 'bytes' ||
398 unit == 'count' ||
399 !metric.indexOf('V8.'));
401 this.addResult = function (newResult) {
402 testResults.push(newResult);
403 cachedUnit = null;
404 cachedScalingFactor = null;
406 this.results = function () { return testResults; }
407 this.scalingFactor = function() {
408 computeScalingFactorIfNeeded();
409 return cachedScalingFactor;
411 this.unit = function () {
412 computeScalingFactorIfNeeded();
413 return cachedUnit;
415 this.biggerIsBetter = function () {
416 if (window.unitToBiggerIsBetter == undefined) {
417 window.unitToBiggerIsBetter = {};
418 var units = JSON.parse(document.getElementById('units-json').textContent);
419 for (var u in units) {
420 if (units[u].improvement_direction == 'up') {
421 window.unitToBiggerIsBetter[u] = true;
425 return window.unitToBiggerIsBetter[unit];
429 function UndeleteManager() {
430 var key = 'telemetry_undeleteIds'
431 var undeleteIds = localStorage[key];
432 if (undeleteIds) {
433 undeleteIds = JSON.parse(undeleteIds);
434 } else {
435 undeleteIds = [];
438 this.ondelete = function(id) {
439 undeleteIds.push(id);
440 localStorage[key] = JSON.stringify(undeleteIds);
442 this.undeleteMostRecent = function() {
443 if (!this.mostRecentlyDeletedId())
444 return;
445 undeleteIds.pop();
446 localStorage[key] = JSON.stringify(undeleteIds);
448 this.mostRecentlyDeletedId = function() {
449 if (!undeleteIds.length)
450 return undefined;
451 return undeleteIds[undeleteIds.length-1];
454 var undeleteManager = new UndeleteManager();
456 var plotColor = 'rgb(230,50,50)';
457 var subpointsPlotOptions = {
458 lines: {show:true, lineWidth: 0},
459 color: plotColor,
460 points: {show: true, radius: 1},
461 bars: {show: false}};
463 var mainPlotOptions = {
464 xaxis: {
465 min: -0.5,
466 tickSize: 1,
468 crosshair: { mode: 'y' },
469 series: { shadowSize: 0 },
470 bars: {show: true, align: 'center', barWidth: 0.5},
471 lines: { show: false },
472 points: { show: true },
473 grid: {
474 borderWidth: 1,
475 borderColor: '#ccc',
476 backgroundColor: '#fff',
477 hoverable: true,
478 autoHighlight: false,
482 var linePlotOptions = {
483 yaxis: { show: false },
484 xaxis: { show: false },
485 lines: { show: true },
486 grid: { borderWidth: 1, borderColor: '#ccc' },
487 colors: [ plotColor ]
490 var largeLinePlotOptions = {
491 xaxis: {
492 show: true,
493 tickDecimals: 0,
495 lines: { show: true },
496 grid: { borderWidth: 1, borderColor: '#ccc' },
497 colors: [ plotColor ]
500 var histogramPlotOptions = {
501 bars: {show: true, fill: 1}
504 function createPlot(container, test, useLargeLinePlots) {
505 if (test.results()[0].histogramValues) {
506 var section = $('<section><div class="histogram-plots"></div>'
507 + '<div class="histogram-plot-labels"></div>'
508 + '<span class="tooltip"></span></section>');
509 $(container).append(section);
510 attachHistogramPlots(test, section.children('.histogram-plots'));
512 else if (useLargeLinePlots) {
513 var section = $('<section><div class="large-line-plots"></div>'
514 + '<div class="large-line-plot-labels"></div>'
515 + '<span class="tooltip"></span></section>');
516 $(container).append(section);
517 attachLinePlots(test, section.children('.large-line-plots'), useLargeLinePlots);
518 attachLinePlotLabels(test, section.children('.large-line-plot-labels'));
519 } else {
520 var section = $('<section><div class="plot"></div><div class="line-plots"></div>'
521 + '<span class="tooltip"></span></section>');
522 section.children('.plot').css({'width': (100 * test.results().length + 25) + 'px', 'height': '300px'});
523 $(container).append(section);
525 var plotContainer = section.children('.plot');
526 var minIsZero = true;
527 attachPlot(test, plotContainer, minIsZero);
529 attachLinePlots(test, section.children('.line-plots'), useLargeLinePlots);
531 var tooltip = section.children('.tooltip');
532 plotContainer.bind('plothover', function (event, position, item) {
533 if (item) {
534 var postfix = item.series.id ? ' (' + item.series.id + ')' : '';
535 tooltip.html(item.datapoint[1].toPrecision(4) + postfix);
536 var sectionOffset = $(section).offset();
537 tooltip.css({left: item.pageX - sectionOffset.left - tooltip.outerWidth() / 2, top: item.pageY - sectionOffset.top + 10});
538 tooltip.fadeIn(200);
539 } else
540 tooltip.hide();
542 plotContainer.mouseout(function () {
543 tooltip.hide();
545 plotContainer.click(function (event) {
546 event.preventDefault();
547 minIsZero = !minIsZero;
548 attachPlot(test, plotContainer, minIsZero);
551 return section;
554 function attachLinePlots(test, container, useLargeLinePlots) {
555 var results = test.results();
556 var attachedPlot = false;
558 if (useLargeLinePlots) {
559 var maximum = 0;
560 for (var i = 0; i < results.length; i++) {
561 var values = results[i].values();
562 if (!values)
563 continue;
564 var local_max = Math.max.apply(Math, values);
565 if (local_max > maximum)
566 maximum = local_max;
570 for (var i = 0; i < results.length; i++) {
571 container.append('<div></div>');
572 var values = results[i].values();
573 if (!values)
574 continue;
575 attachedPlot = true;
577 if (useLargeLinePlots) {
578 var options = $.extend(true, {}, largeLinePlotOptions,
579 {yaxis: {min: 0.0, max: maximum},
580 xaxis: {min: 0.0, max: values.length - 1},
581 points: {show: (values.length < 2) ? true : false}});
582 } else {
583 var options = $.extend(true, {}, linePlotOptions,
584 {yaxis: {min: Math.min.apply(Math, values) * 0.9, max: Math.max.apply(Math, values) * 1.1},
585 xaxis: {min: -0.5, max: values.length - 0.5},
586 points: {show: (values.length < 2) ? true : false}});
588 $.plot(container.children().last(), [values.map(function (value, index) { return [index, value]; })], options);
590 if (!attachedPlot)
591 container.children().remove();
594 function attachHistogramPlots(test, container) {
595 var results = test.results();
596 var attachedPlot = false;
598 for (var i = 0; i < results.length; i++) {
599 container.append('<div></div>');
600 var histogram = results[i].histogramValues
601 if (!histogram)
602 continue;
603 attachedPlot = true;
605 var buckets = histogram.buckets
606 var bucket;
607 var max_count = 0;
608 for (var j = 0; j < buckets.length; j++) {
609 bucket = buckets[j];
610 max_count = Math.max(max_count, bucket.count);
612 var xmax = bucket.high * 1.1;
613 var ymax = max_count * 1.1;
615 var options = $.extend(true, {}, histogramPlotOptions,
616 {yaxis: {min: 0.0, max: ymax},
617 xaxis: {min: histogram.params.min, max: xmax}});
618 var plot = $.plot(container.children().last(), [[]], options);
619 // Flot only supports fixed with bars and our histogram's buckets are
620 // variable width, so we need to do our own bar drawing.
621 var ctx = plot.getCanvas().getContext("2d");
622 ctx.lineWidth="1";
623 ctx.fillStyle = "rgba(255, 0, 0, 0.2)";
624 ctx.strokeStyle="red";
625 for (var j = 0; j < buckets.length; j++) {
626 bucket = buckets[j];
627 var bl = plot.pointOffset({ x: bucket.low, y: 0});
628 var tr = plot.pointOffset({ x: bucket.high, y: bucket.count});
629 ctx.fillRect(bl.left, bl.top, tr.left - bl.left, tr.top - bl.top);
630 ctx.strokeRect(bl.left, bl.top, tr.left - bl.left, tr.top - bl.top);
633 if (!attachedPlot)
634 container.children().remove();
637 function attachLinePlotLabels(test, container) {
638 var results = test.results();
639 var attachedPlot = false;
640 for (var i = 0; i < results.length; i++) {
641 container.append('<div>' + results[i].run().label() + '</div>');
645 function attachPlot(test, plotContainer, minIsZero) {
646 var results = test.results();
648 var values = results.reduce(function (values, result, index) {
649 var newValues = result.values();
650 return newValues ? values.concat(newValues.map(function (value) { return [index, value]; })) : values;
651 }, []);
653 var plotData = [$.extend(true, {}, subpointsPlotOptions, {data: values})];
654 plotData.push({id: '&mu;', data: results.map(function (result, index) { return [index, result.mean()]; }), color: plotColor});
656 var overallMax = Statistics.max(results.map(function (result, index) { return result.max(); }));
657 var overallMin = Statistics.min(results.map(function (result, index) { return result.min(); }));
658 var margin = (overallMax - overallMin) * 0.1;
659 var currentPlotOptions = $.extend(true, {}, mainPlotOptions, {yaxis: {
660 min: minIsZero ? 0 : overallMin - margin,
661 max: minIsZero ? overallMax * 1.1 : overallMax + margin}});
663 currentPlotOptions.xaxis.max = results.length - 0.5;
664 currentPlotOptions.xaxis.ticks = results.map(function (result, index) { return [index, result.run().label()]; });
666 $.plot(plotContainer, plotData, currentPlotOptions);
669 function toFixedWidthPrecision(value) {
670 var decimal = value.toFixed(2);
671 return decimal;
674 function formatPercentage(fraction) {
675 var percentage = fraction * 100;
676 return (fraction * 100).toFixed(2) + '%';
679 function setUpSortClicks(runs)
681 $('#nameColumn').click(sortByName);
683 $('#unitColumn').click(sortByUnit);
685 runs.forEach(function(run) {
686 $('#' + run.id()).click(sortByResult);
687 $('#' + run.id() + COMPARISON_SUFFIX).click(sortByReference);
691 var topLevelRows;
692 var allTableRows;
694 function createTable(tests, runs, shouldIgnoreMemory, referenceIndex, useLargeLinePlots) {
695 var resultHeaders = runs.map(function (run, index) {
696 var header = '<th id="' + run.id() + '" ' +
697 'colspan=2 ' +
698 'title="' + run.description() + '">' +
699 '<span class="label" ' +
700 'title="Edit run label">' +
701 run.label() +
702 '</span>' +
703 '<div class="closeButton" ' +
704 'title="Delete run">' +
705 '&times;' +
706 '</div>' +
707 '</th>';
708 if (index !== referenceIndex) {
709 header += '<th id="' + run.id() + COMPARISON_SUFFIX + '" ' +
710 'title="Sort by better/worse">' +
711 '&Delta;' +
712 '</th>';
714 return header;
717 resultHeaders = resultHeaders.join('');
719 htmlString = '<thead>' +
720 '<tr>' +
721 '<th id="nameColumn">' +
722 '<div class="openAllButton" ' +
723 'title="Open all rows or graphs">' +
724 'Open All' +
725 '</div>' +
726 '<div class="closeAllButton" ' +
727 'title="Close all rows">' +
728 'Close All' +
729 '</div>' +
730 'Test' +
731 '</th>' +
732 '<th id="unitColumn">' +
733 'Unit' +
734 '</th>' +
735 resultHeaders +
736 '</tr>' +
737 '</head>' +
738 '<tbody>' +
739 '</tbody>';
741 $('#container').html(htmlString);
743 var testNames = [];
744 for (testName in tests)
745 testNames.push(testName);
747 allTableRows = [];
748 testNames.forEach(function(testName) {
749 var test = tests[testName];
750 if (test.isMemoryTest() === shouldIgnoreMemory) {
751 return;
753 allTableRows.push(new TableRow(runs, test, referenceIndex, useLargeLinePlots));
756 // Build a list of top level rows with attached children
757 topLevelRows = [];
758 allTableRows.forEach(function(row) {
759 // Add us to top level if we are a top-level row...
760 if (row.hasNoURL) {
761 topLevelRows.push(row);
762 // Add a duplicate child row that holds the graph for the parent
763 var graphHolder = new TableRow(runs, row.test, referenceIndex, useLargeLinePlots);
764 graphHolder.isImportant = true;
765 graphHolder.URL = 'Summary';
766 graphHolder.hideRowData();
767 allTableRows.push(graphHolder);
768 row.addNestedChild(graphHolder);
769 return;
772 // ...or add us to our parent if we have one ...
773 for (var i = 0; i < allTableRows.length; i++) {
774 if (allTableRows[i].isParentOf(row)) {
775 allTableRows[i].addNestedChild(row);
776 return;
780 // ...otherwise this result is orphaned, display it at top level with a graph
781 row.hasGraph = true;
782 topLevelRows.push(row);
785 buildTable(topLevelRows);
787 $('.closeButton').click(function(event) {
788 for (var i = 0; i < runs.length; i++) {
789 if (runs[i].id() == event.target.parentNode.id) {
790 runs[i].hide();
791 undeleteManager.ondelete(runs[i].id());
792 location.reload();
793 break;
796 event.stopPropagation();
799 $('.closeAllButton').click(function(event) {
800 for (var i = 0; i < allTableRows.length; i++) {
801 allTableRows[i].closeRow();
803 event.stopPropagation();
806 $('.openAllButton').click(function(event) {
807 for (var i = 0; i < topLevelRows.length; i++) {
808 topLevelRows[i].openRow();
810 event.stopPropagation();
813 setUpSortClicks(runs);
815 $('.label').click(function(event) {
816 for (var i = 0; i < runs.length; i++) {
817 if (runs[i].id() == event.target.parentNode.id) {
818 $(event.target).replaceWith('<input id="labelEditor" type="text" value="' + runs[i].label() + '">');
819 $('#labelEditor').focusout(function() {
820 runs[i].setLabel(this.value);
821 location.reload();
823 $('#labelEditor').keypress(function(event) {
824 if (event.which == 13) {
825 runs[i].setLabel(this.value);
826 location.reload();
829 $('#labelEditor').click(function (event) {
830 event.stopPropagation();
832 $('#labelEditor').mousedown(function (event) {
833 event.stopPropagation();
835 $('#labelEditor').select();
836 break;
839 event.stopPropagation();
843 function validForSorting(row) {
844 return ($.type(row.sortValue) === 'string') || !isNaN(row.sortValue);
847 var sortDirection = 1;
849 function sortRows(rows) {
850 rows.sort(
851 function(rowA,rowB) {
852 if (validForSorting(rowA) !== validForSorting(rowB)) {
853 // Sort valid values upwards when compared to invalid
854 if (validForSorting(rowA)) {
855 return -1;
857 if (validForSorting(rowB)) {
858 return 1;
862 // Some rows always sort to the top
863 if (rowA.isImportant) {
864 return -1;
866 if (rowB.isImportant) {
867 return 1;
870 if (rowA.sortValue === rowB.sortValue) {
871 // Sort identical values by name to keep the sort stable,
872 // always keep name alphabetical (even if a & b sort values
873 // are invalid)
874 return rowA.test.name() > rowB.test.name() ? 1 : -1;
877 return rowA.sortValue > rowB.sortValue ? sortDirection : -sortDirection;
878 } );
880 // Sort the rows' children
881 rows.forEach(function(row) {
882 sortRows(row.children);
886 function buildTable(rows) {
887 rows.forEach(function(row) {
888 row.removeFromPage();
891 sortRows(rows);
893 rows.forEach(function(row) {
894 row.addToPage();
898 var activeSortHeaderElement = undefined;
899 var columnSortDirection = {};
901 function determineColumnSortDirection(element) {
902 columnDirection = columnSortDirection[element.id];
904 if (columnDirection === undefined) {
905 // First time we've sorted this row, default to down
906 columnSortDirection[element.id] = SORT_DOWN_CLASS;
907 } else if (element === activeSortHeaderElement) {
908 // Clicking on same header again, swap direction
909 columnSortDirection[element.id] = (columnDirection === SORT_UP_CLASS) ? SORT_DOWN_CLASS : SORT_UP_CLASS;
913 function updateSortDirection(element) {
914 // Remove old header's sort arrow
915 if (activeSortHeaderElement !== undefined) {
916 activeSortHeaderElement.classList.remove(columnSortDirection[activeSortHeaderElement.id]);
919 determineColumnSortDirection(element);
921 sortDirection = (columnSortDirection[element.id] === SORT_UP_CLASS) ? 1 : -1;
923 // Add new header's sort arrow
924 element.classList.add(columnSortDirection[element.id]);
925 activeSortHeaderElement = element;
928 function sortByName(event) {
929 updateSortDirection(event.toElement);
931 allTableRows.forEach(function(row) {
932 row.prepareToSortByName();
935 buildTable(topLevelRows);
938 function sortByUnit(event) {
939 updateSortDirection(event.toElement);
941 allTableRows.forEach(function(row) {
942 row.prepareToSortByUnit();
945 buildTable(topLevelRows);
948 function sortByResult(event) {
949 updateSortDirection(event.toElement);
951 var runId = event.target.id;
953 allTableRows.forEach(function(row) {
954 row.prepareToSortByTestResults(runId);
957 buildTable(topLevelRows);
960 function sortByReference(event) {
961 updateSortDirection(event.toElement);
963 // The element ID has _compare appended to allow us to set up a click event
964 // remove the _compare to return a useful Id
965 var runIdWithCompare = event.target.id;
966 var runId = runIdWithCompare.split('_')[0];
968 allTableRows.forEach(function(row) {
969 row.prepareToSortRelativeToReference(runId);
972 buildTable(topLevelRows);
975 function linearRegression(points) {
976 // Implement http://www.easycalculation.com/statistics/learn-correlation.php.
977 // x = magnitude
978 // y = iterations
979 var sumX = 0;
980 var sumY = 0;
981 var sumXSquared = 0;
982 var sumYSquared = 0;
983 var sumXTimesY = 0;
985 for (var i = 0; i < points.length; i++) {
986 var x = i;
987 var y = points[i];
988 sumX += x;
989 sumY += y;
990 sumXSquared += x * x;
991 sumYSquared += y * y;
992 sumXTimesY += x * y;
995 var r = (points.length * sumXTimesY - sumX * sumY) /
996 Math.sqrt((points.length * sumXSquared - sumX * sumX) *
997 (points.length * sumYSquared - sumY * sumY));
999 if (isNaN(r) || r == Math.Infinity)
1000 r = 0;
1002 var slope = (points.length * sumXTimesY - sumX * sumY) / (points.length * sumXSquared - sumX * sumX);
1003 var intercept = sumY / points.length - slope * sumX / points.length;
1004 return {slope: slope, intercept: intercept, rSquared: r * r};
1007 var warningSign = '<svg viewBox="0 0 100 100" style="width: 18px; height: 18px; vertical-align: bottom;" version="1.1">'
1008 + '<polygon fill="red" points="50,10 90,80 10,80 50,10" stroke="red" stroke-width="10" stroke-linejoin="round" />'
1009 + '<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" />'
1010 + '<circle cx="50" cy="73" r="6" fill="white" />'
1011 + '</svg>';
1013 function TableRow(runs, test, referenceIndex, useLargeLinePlots) {
1014 this.runs = runs;
1015 this.test = test;
1016 this.referenceIndex = referenceIndex;
1017 this.useLargeLinePlots = useLargeLinePlots;
1018 this.children = [];
1020 this.tableRow = $('<tr class="highlight">' +
1021 '<td class="test collapsed" >' +
1022 this.test.name() +
1023 '</td>' +
1024 '<td class="unit">' +
1025 this.test.unit() +
1026 '</td>' +
1027 '</tr>');
1029 var runIndex = 0;
1030 var results = this.test.results();
1031 var referenceResult = undefined;
1033 this.resultIndexMap = {};
1034 for (var i = 0; i < results.length; i++) {
1035 while (this.runs[runIndex] !== results[i].run())
1036 runIndex++;
1037 if (runIndex === this.referenceIndex)
1038 referenceResult = results[i];
1039 this.resultIndexMap[runIndex] = i;
1041 for (var i = 0; i < this.runs.length; i++) {
1042 var resultIndex = this.resultIndexMap[i];
1043 if (resultIndex === undefined)
1044 this.tableRow.append(this.markupForMissingRun(i == this.referenceIndex));
1045 else
1046 this.tableRow.append(this.markupForRun(results[resultIndex], referenceResult));
1049 // Use the test name (without URL) to bind parents and their children
1050 var nameAndURL = this.test.name().split('.');
1051 var benchmarkName = nameAndURL.shift();
1052 this.testName = nameAndURL.shift();
1053 this.hasNoURL = (nameAndURL.length === 0);
1055 if (!this.hasNoURL) {
1056 // Re-join the URL
1057 this.URL = nameAndURL.join('.');
1060 this.isImportant = false;
1061 this.hasGraph = false;
1062 this.currentIndentationClass = ''
1063 this.indentLevel = 0;
1064 this.setRowNestedState(COLLAPSED);
1065 this.setVisibility(VISIBLE);
1066 this.prepareToSortByName();
1069 TableRow.prototype.hideRowData = function() {
1070 data = this.tableRow.children('td');
1072 for (index in data) {
1073 if (index > 0) {
1074 // Blank out everything except the test name
1075 data[index].innerHTML = '';
1080 TableRow.prototype.prepareToSortByTestResults = function(runId) {
1081 var testResults = this.test.results();
1082 // Find the column in this row that matches the runId and prepare to
1083 // sort by the mean of that test.
1084 for (index in testResults) {
1085 sourceId = testResults[index].run().id();
1086 if (runId === sourceId) {
1087 this.sortValue = testResults[index].mean();
1088 return;
1091 // This row doesn't have any results for the passed runId
1092 this.sortValue = undefined;
1095 TableRow.prototype.prepareToSortRelativeToReference = function(runId) {
1096 var testResults = this.test.results();
1098 if (this.resultIndexMap[this.referenceIndex] === undefined) {
1099 // This test has no results in the reference run
1100 this.sortValue = undefined;
1101 return;
1104 otherResults = testResults[this.referenceIndex];
1106 // Find the column in this row that matches the runId and prepare to
1107 // sort by the difference from the reference.
1108 for (index in testResults) {
1109 sourceId = testResults[index].run().id();
1110 if (runId === sourceId) {
1111 this.sortValue = testResults[index].percentDifference(otherResults);
1112 if (this.test.biggerIsBetter()) {
1113 // For this test bigger is not better
1114 this.sortValue = -this.sortValue;
1116 return;
1119 // This row doesn't have any results for the passed runId
1120 this.sortValue = undefined;
1123 TableRow.prototype.prepareToSortByUnit = function() {
1124 this.sortValue = this.test.unit().toLowerCase();
1127 TableRow.prototype.prepareToSortByName = function() {
1128 this.sortValue = this.test.name().toLowerCase();
1131 TableRow.prototype.isParentOf = function(row) {
1132 return this.hasNoURL && (this.testName === row.testName);
1135 TableRow.prototype.addNestedChild = function(child) {
1136 this.children.push(child);
1138 // Indent child one step in from parent
1139 child.indentLevel = this.indentLevel + INDENTATION;
1140 child.hasGraph = true;
1141 // Start child off as hidden (i.e. collapsed inside parent)
1142 child.setVisibility(INVISIBLE);
1143 child.updateIndentation();
1144 // Show URL in the title column
1145 child.tableRow.children()[0].innerHTML = child.URL;
1146 // Set up class to change background colour of nested rows
1147 if (child.isImportant) {
1148 child.tableRow.addClass('importantNestedRow');
1149 } else {
1150 child.tableRow.addClass('nestedRow');
1154 TableRow.prototype.setVisibility = function(visibility) {
1155 this.visibility = visibility;
1156 this.tableRow[0].style.display = (visibility === INVISIBLE) ? 'none' : '';
1159 TableRow.prototype.setRowNestedState = function(newState) {
1160 this.rowState = newState;
1161 this.updateIndentation();
1164 TableRow.prototype.updateIndentation = function() {
1165 var element = this.tableRow.children('td').first();
1167 element.removeClass(this.currentIndentationClass);
1169 this.currentIndentationClass = (this.rowState === COLLAPSED) ? 'collapsed' : 'expanded';
1171 element[0].style.marginLeft = this.indentLevel.toString() + 'px';
1172 element[0].style.float = 'left';
1174 element.addClass(this.currentIndentationClass);
1177 TableRow.prototype.addToPage = function() {
1178 $('#container').children('tbody').last().append(this.tableRow);
1180 // Set up click callback
1181 var owningObject = this;
1182 this.tableRow.click(function(event) {
1183 event.preventDefault();
1184 owningObject.toggle();
1187 // Add children to the page too
1188 this.children.forEach(function(child) {
1189 child.addToPage();
1193 TableRow.prototype.removeFromPage = function() {
1194 // Remove children
1195 this.children.forEach(function(child) {
1196 child.removeFromPage();
1198 // Remove us
1199 this.tableRow.remove();
1203 TableRow.prototype.markupForRun = function(result, referenceResult) {
1204 var comparisonCell = '';
1205 var shouldCompare = result !== referenceResult;
1206 if (shouldCompare) {
1207 var comparisonText = '';
1208 var className = '';
1210 if (referenceResult) {
1211 var percentDifference = referenceResult.percentDifference(result);
1212 if (isNaN(percentDifference)) {
1213 comparisonText = 'Unknown';
1214 className = UNKNOWN_CLASS;
1215 } else if (Math.abs(percentDifference) < SMALLEST_PERCENT_DISPLAYED) {
1216 comparisonText = 'Equal';
1217 // Show equal values in green
1218 className = BETTER_CLASS;
1219 } else {
1220 var better = this.test.biggerIsBetter() ? percentDifference > 0 : percentDifference < 0;
1221 comparisonText = formatPercentage(Math.abs(percentDifference)) + (better ? ' Better' : ' Worse');
1222 className = better ? BETTER_CLASS : WORSE_CLASS;
1225 if (!referenceResult.isStatisticallySignificant(result)) {
1226 // Put result in brackets and fade if not statistically significant
1227 className += ' fadeOut';
1228 comparisonText = '(' + comparisonText + ')';
1231 comparisonCell = '<td class="comparison ' + className + '">' + comparisonText + '</td>';
1234 var values = result.values();
1235 var warning = '';
1236 var regressionAnalysis = '';
1237 if (result.histogramValues) {
1238 // Don't calculate regression result for histograms.
1239 } else if (values && values.length > 3) {
1240 regressionResult = linearRegression(values);
1241 regressionAnalysis = 'slope=' + toFixedWidthPrecision(regressionResult.slope)
1242 + ', R^2=' + toFixedWidthPrecision(regressionResult.rSquared);
1243 if (regressionResult.rSquared > 0.6 && Math.abs(regressionResult.slope) > 0.01) {
1244 warning = ' <span class="regression-warning" title="Detected a time dependency with ' + regressionAnalysis + '">' + warningSign + ' </span>';
1248 var referenceClass = shouldCompare ? '' : 'reference';
1250 var statistics = '&sigma;=' + toFixedWidthPrecision(result.confidenceIntervalDelta()) + ', min=' + toFixedWidthPrecision(result.min())
1251 + ', max=' + toFixedWidthPrecision(result.max()) + '\n' + regressionAnalysis;
1253 var confidence;
1254 if (isNaN(result.confidenceIntervalDeltaRatio())) {
1255 // Don't bother showing +- Nan as it is meaningless
1256 confidence = '';
1257 } else {
1258 confidence = '&plusmn; ' + formatPercentage(result.confidenceIntervalDeltaRatio());
1261 return '<td class="result ' + referenceClass + '" title="' + statistics + '">' + toFixedWidthPrecision(result.mean())
1262 + '</td><td class="confidenceIntervalDelta ' + referenceClass + '" title="' + statistics + '">' + confidence + warning + '</td>' + comparisonCell;
1265 TableRow.prototype.markupForMissingRun = function(isReference) {
1266 if (isReference) {
1267 return '<td colspan=2 class="missingReference">Missing</td>';
1269 return '<td colspan=3 class="missing">Missing</td>';
1272 TableRow.prototype.openRow = function() {
1273 if (this.rowState === EXPANDED) {
1274 // If we're already expanded, open our children instead
1275 this.children.forEach(function(child) {
1276 child.openRow();
1278 return;
1281 this.setRowNestedState(EXPANDED);
1283 if (this.hasGraph) {
1284 var firstCell = this.tableRow.children('td').first();
1285 var plot = createPlot(firstCell, this.test, this.useLargeLinePlots);
1286 plot.css({'position': 'absolute', 'z-index': 2});
1287 var offset = this.tableRow.offset();
1288 offset.left += GRAPH_INDENT;
1289 offset.top += this.tableRow.outerHeight();
1290 plot.offset(offset);
1291 this.tableRow.children('td').css({'padding-bottom': plot.outerHeight() + PADDING_UNDER_GRAPH});
1294 this.children.forEach(function(child) {
1295 child.setVisibility(VISIBLE);
1298 if (this.children.length === 1) {
1299 // If we only have a single child...
1300 var child = this.children[0];
1301 if (child.isImportant) {
1302 // ... and it is important (i.e. the summary row) just open it when
1303 // parent is opened to save needless clicking
1304 child.openRow();
1309 TableRow.prototype.closeRow = function() {
1310 if (this.rowState === COLLAPSED) {
1311 return;
1314 this.setRowNestedState(COLLAPSED);
1316 if (this.hasGraph) {
1317 var firstCell = this.tableRow.children('td').first();
1318 firstCell.children('section').remove();
1319 this.tableRow.children('td').css({'padding-bottom': ''});
1322 this.children.forEach(function(child) {
1323 // Make children invisible, but leave their collapsed status alone
1324 child.setVisibility(INVISIBLE);
1328 TableRow.prototype.toggle = function() {
1329 if (this.rowState === EXPANDED) {
1330 this.closeRow();
1331 } else {
1332 this.openRow();
1334 return false;
1337 function init() {
1338 var runs = [];
1339 var metrics = {};
1340 var deletedRunsById = {};
1341 $.each(JSON.parse(document.getElementById('results-json').textContent), function (index, entry) {
1342 var run = new TestRun(entry);
1343 if (run.isHidden()) {
1344 deletedRunsById[run.id()] = run;
1345 return;
1348 runs.push(run);
1350 function addTests(tests) {
1351 for (var testName in tests) {
1352 var rawMetrics = tests[testName].metrics;
1354 for (var metricName in rawMetrics) {
1355 var fullMetricName = testName + ':' + metricName;
1356 var metric = metrics[fullMetricName];
1357 if (!metric) {
1358 metric = new PerfTestMetric(testName, metricName, rawMetrics[metricName].units, rawMetrics[metricName].important);
1359 metrics[fullMetricName] = metric;
1361 metric.addResult(new TestResult(metric, rawMetrics[metricName].current, run));
1366 addTests(entry.tests);
1369 var useLargeLinePlots = false;
1370 var shouldIgnoreMemory= true;
1371 var referenceIndex = 0;
1373 createTable(metrics, runs, shouldIgnoreMemory, referenceIndex, useLargeLinePlots);
1375 $('#time-memory').bind('change', function (event, checkedElement) {
1376 shouldIgnoreMemory = checkedElement.textContent == 'Time';
1377 createTable(metrics, runs, shouldIgnoreMemory, referenceIndex, useLargeLinePlots);
1380 $('#scatter-line').bind('change', function (event, checkedElement) {
1381 useLargeLinePlots = checkedElement.textContent == 'Line';
1382 createTable(metrics, runs, shouldIgnoreMemory, referenceIndex, useLargeLinePlots);
1385 runs.map(function (run, index) {
1386 $('#reference').append('<span value="' + index + '"' + (index == referenceIndex ? ' class="checked"' : '') + ' title="' + run.description() + '">' + run.label() + '</span>');
1389 $('#reference').bind('change', function (event, checkedElement) {
1390 referenceIndex = parseInt(checkedElement.getAttribute('value'));
1391 createTable(metrics, runs, shouldIgnoreMemory, referenceIndex, useLargeLinePlots);
1394 $('.checkbox').each(function (index, checkbox) {
1395 $(checkbox).children('span').click(function (event) {
1396 if ($(this).hasClass('checked'))
1397 return;
1398 $(checkbox).children('span').removeClass('checked');
1399 $(this).addClass('checked');
1400 $(checkbox).trigger('change', $(this));
1404 runToUndelete = deletedRunsById[undeleteManager.mostRecentlyDeletedId()];
1406 if (runToUndelete) {
1407 $('#undelete').html('Undelete ' + runToUndelete.label());
1408 $('#undelete').attr('title', runToUndelete.description());
1409 $('#undelete').click(function (event) {
1410 runToUndelete.show();
1411 undeleteManager.undeleteMostRecent();
1412 location.reload();
1414 } else {
1415 $('#undelete').hide();
1419 </script>
1420 <script id="results-json" type="application/json">%json_results%</script>
1421 <script id="units-json" type="application/json">%json_units%</script>
1422 </body>
1423 </html>