4 <title>Telemetry Performance Test Results
</title>
5 <style type=
"text/css">
15 content: '\25B8\00A0';
20 content: '\25BE\00A0';
28 display: inline-block
;
38 .large-line-plots > div, .histogram-plots > div {
39 display: inline-block
;
45 .large-line-plot-labels > div, .histogram-plot-labels > div {
46 display: inline-block
;
56 display: inline-block
;
58 background: linear-gradient
(rgb
(220, 220, 220), rgb
(255, 255, 255));
59 border: inset
1px #ddd;
63 -webkit-user-select: none
;
96 font-family: sans-serif
;
105 border-collapse: collapse
;
119 .importantNestedRow {
135 background: linear-gradient
(rgb
(244, 244, 244), rgb
(217, 217, 217));
136 border: 1px solid
#ccc;
147 td
.comparison
, td
.result
{
178 td
.missingReference
{
185 display: inline-block
;
187 background: linear-gradient
(rgb
(220, 220, 220), rgb
(200, 200, 200));
188 border: inset
1px #ddd;
194 -webkit-user-select: none
;
199 display: inline-block
;
202 border: outset
1px transparent
;
207 background: linear-gradient
(rgb
(255, 255, 255), rgb
(235, 235, 235));
208 border: outset
1px #eee;
213 display: inline-block
;
216 background: linear-gradient
(rgb
(220, 220, 220), rgb
(255, 255, 255));
217 border: inset
1px #ddd;
221 -webkit-user-select: none
;
226 .openAllButton:hover {
231 display: inline-block
;
234 background: linear-gradient
(rgb
(220, 220, 220),rgb
(255, 255, 255));
235 border: inset
1px #ddd;
239 -webkit-user-select: none
;
244 .closeAllButton:hover {
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
258 <table id=
"container"></table>
259 <script src=
"https://ajax.googleapis.com/ajax/libs/jquery/1.6.4/jquery.min.js"></script>
266 var COLLAPSED
= false;
267 var SMALLEST_PERCENT_DISPLAYED
= 0.01;
268 var INVISIBLE
= false;
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') {
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.
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) {
326 return (other
.unscaledMean() - this.unscaledMean()) / this.unscaledMean();
328 this.isStatisticallySignificant = function (other
) {
329 if (other
=== undefined) {
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
];
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())
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
)
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
);
386 cachedScalingFactor
= 1;
391 this.name = function () { return name
+ ':' + metric
; }
392 this.isImportant
= isImportant
;
393 this.isMemoryTest = function () {
394 return (unit
== 'kb' ||
399 !metric
.indexOf('V8.'));
401 this.addResult = function (newResult
) {
402 testResults
.push(newResult
);
404 cachedScalingFactor
= null;
406 this.results = function () { return testResults
; }
407 this.scalingFactor = function() {
408 computeScalingFactorIfNeeded();
409 return cachedScalingFactor
;
411 this.unit = function () {
412 computeScalingFactorIfNeeded();
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
];
433 undeleteIds
= JSON
.parse(undeleteIds
);
438 this.ondelete = function(id
) {
439 undeleteIds
.push(id
);
440 localStorage
[key
] = JSON
.stringify(undeleteIds
);
442 this.undeleteMostRecent = function() {
443 if (!this.mostRecentlyDeletedId())
446 localStorage
[key
] = JSON
.stringify(undeleteIds
);
448 this.mostRecentlyDeletedId = function() {
449 if (!undeleteIds
.length
)
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},
460 points
: {show
: true, radius
: 1},
461 bars
: {show
: false}};
463 var mainPlotOptions
= {
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 },
476 backgroundColor
: '#fff',
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
= {
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'));
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
) {
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});
542 plotContainer
.mouseout(function () {
545 plotContainer
.click(function (event
) {
546 event
.preventDefault();
547 minIsZero
= !minIsZero
;
548 attachPlot(test
, plotContainer
, minIsZero
);
554 function attachLinePlots(test
, container
, useLargeLinePlots
) {
555 var results
= test
.results();
556 var attachedPlot
= false;
558 if (useLargeLinePlots
) {
560 for (var i
= 0; i
< results
.length
; i
++) {
561 var values
= results
[i
].values();
564 var local_max
= Math
.max
.apply(Math
, values
);
565 if (local_max
> maximum
)
570 for (var i
= 0; i
< results
.length
; i
++) {
571 container
.append('<div></div>');
572 var values
= results
[i
].values();
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}});
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
);
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
605 var buckets
= histogram
.buckets
608 for (var j
= 0; j
< buckets
.length
; 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");
623 ctx
.fillStyle
= "rgba(255, 0, 0, 0.2)";
624 ctx
.strokeStyle
="red";
625 for (var j
= 0; j
< buckets
.length
; 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
);
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
;
653 var plotData
= [$.extend(true, {}, subpointsPlotOptions
, {data
: values
})];
654 plotData
.push({id
: 'μ', 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);
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
);
694 function createTable(tests
, runs
, shouldIgnoreMemory
, referenceIndex
, useLargeLinePlots
) {
695 var resultHeaders
= runs
.map(function (run
, index
) {
696 var header
= '<th id="' + run
.id() + '" ' +
698 'title="' + run
.description() + '">' +
699 '<span class="label" ' +
700 'title="Edit run label">' +
703 '<div class="closeButton" ' +
704 'title="Delete run">' +
708 if (index
!== referenceIndex
) {
709 header
+= '<th id="' + run
.id() + COMPARISON_SUFFIX
+ '" ' +
710 'title="Sort by better/worse">' +
717 resultHeaders
= resultHeaders
.join('');
719 htmlString
= '<thead>' +
721 '<th id="nameColumn">' +
722 '<div class="openAllButton" ' +
723 'title="Open all rows or graphs">' +
726 '<div class="closeAllButton" ' +
727 'title="Close all rows">' +
732 '<th id="unitColumn">' +
741 $('#container').html(htmlString
);
744 for (testName
in tests
)
745 testNames
.push(testName
);
748 testNames
.forEach(function(testName
) {
749 var test
= tests
[testName
];
750 if (test
.isMemoryTest() === shouldIgnoreMemory
) {
753 allTableRows
.push(new TableRow(runs
, test
, referenceIndex
, useLargeLinePlots
));
756 // Build a list of top level rows with attached children
758 allTableRows
.forEach(function(row
) {
759 // Add us to top level if we are a top-level row...
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
);
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
);
780 // ...otherwise this result is orphaned, display it at top level with a graph
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
) {
791 undeleteManager
.ondelete(runs
[i
].id());
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
);
823 $('#labelEditor').keypress(function(event
) {
824 if (event
.which
== 13) {
825 runs
[i
].setLabel(this.value
);
829 $('#labelEditor').click(function (event
) {
830 event
.stopPropagation();
832 $('#labelEditor').mousedown(function (event
) {
833 event
.stopPropagation();
835 $('#labelEditor').select();
839 event
.stopPropagation();
843 function validForSorting(row
) {
844 return ($.type(row
.sortValue
) === 'string') || !isNaN(row
.sortValue
);
847 var sortDirection
= 1;
849 function sortRows(rows
) {
851 function(rowA
,rowB
) {
852 if (validForSorting(rowA
) !== validForSorting(rowB
)) {
853 // Sort valid values upwards when compared to invalid
854 if (validForSorting(rowA
)) {
857 if (validForSorting(rowB
)) {
862 // Some rows always sort to the top
863 if (rowA
.isImportant
) {
866 if (rowB
.isImportant
) {
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
874 return rowA
.test
.name() > rowB
.test
.name() ? 1 : -1;
877 return rowA
.sortValue
> rowB
.sortValue
? sortDirection
: -sortDirection
;
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();
893 rows
.forEach(function(row
) {
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.
985 for (var i
= 0; i
< points
.length
; i
++) {
990 sumXSquared
+= x
* x
;
991 sumYSquared
+= y
* 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
)
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" />'
1013 function TableRow(runs
, test
, referenceIndex
, useLargeLinePlots
) {
1016 this.referenceIndex
= referenceIndex
;
1017 this.useLargeLinePlots
= useLargeLinePlots
;
1020 this.tableRow
= $('<tr class="highlight">' +
1021 '<td class="test collapsed" >' +
1024 '<td class="unit">' +
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())
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
));
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
) {
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
) {
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();
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;
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
;
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');
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
) {
1193 TableRow
.prototype.removeFromPage = function() {
1195 this.children
.forEach(function(child
) {
1196 child
.removeFromPage();
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
= '';
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
;
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();
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
= 'σ=' + toFixedWidthPrecision(result
.confidenceIntervalDelta()) + ', min=' + toFixedWidthPrecision(result
.min())
1251 + ', max=' + toFixedWidthPrecision(result
.max()) + '\n' + regressionAnalysis
;
1254 if (isNaN(result
.confidenceIntervalDeltaRatio())) {
1255 // Don't bother showing +- Nan as it is meaningless
1258 confidence
= '± ' + 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
) {
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
) {
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
1309 TableRow
.prototype.closeRow = function() {
1310 if (this.rowState
=== COLLAPSED
) {
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
) {
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
;
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
];
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'))
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();
1415 $('#undelete').hide();
1420 <script id=
"results-json" type=
"application/json">%json_results%
</script>
1421 <script id=
"units-json" type=
"application/json">%json_units%
</script>