<!DOCTYPE html>
<html>
<head>
<title>Telemetry Performance Test Results</title>
<style type="text/css">
section {
background: white;
padding: 10px;
position: relative;
}
.collapsed:before {
color: #ccc;
content: '\25B8\00A0';
}
.expanded:before {
color: #eee;
content: '\25BE\00A0';
}
.line-plots {
padding-left: 25px;
}
.line-plots > div {
display: inline-block;
width: 90px;
height: 40px;
margin-right: 10px;
}
.lage-line-plots {
padding-left: 25px;
}
.large-line-plots > div, .histogram-plots > div {
display: inline-block;
width: 400px;
height: 200px;
margin-right: 10px;
}
.large-line-plot-labels > div, .histogram-plot-labels > div {
display: inline-block;
width: 400px;
height: 11px;
margin-right: 10px;
color: #545454;
text-align: center;
font-size: 11px;
}
.closeButton {
display: inline-block;
background: #eee;
background: linear-gradient(rgb(220, 220, 220), rgb(255, 255, 255));
border: inset 1px #ddd;
border-radius: 4px;
float: right;
font-size: small;
-webkit-user-select: none;
font-weight: bold;
padding: 1px 4px;
}
.closeButton:hover {
background: #F09C9C;
}
.label {
cursor: text;
}
.label:hover {
background: #ffcc66;
}
section h1 {
text-align: center;
font-size: 1em;
}
section .tooltip {
position: absolute;
text-align: center;
background: #ffcc66;
border-radius: 5px;
padding: 0px 5px;
}
body {
padding: 0px;
margin: 0px;
font-family: sans-serif;
}
table {
background: white;
width: 100%;
}
table, td, th {
border-collapse: collapse;
padding: 5px;
white-space: nowrap;
}
.highlight:hover {
color: #202020;
background: #e0e0e0;
}
.nestedRow {
background: #f8f8f8;
}
.importantNestedRow {
background: #e0e0e0;
font-weight: bold;
}
table td {
position: relative;
}
th, td {
cursor: pointer;
cursor: hand;
}
th {
background: #e6eeee;
background: linear-gradient(rgb(244, 244, 244), rgb(217, 217, 217));
border: 1px solid #ccc;
}
th.sortUp:after {
content: ' \25BE';
}
th.sortDown:after {
content: ' \25B4';
}
td.comparison, td.result {
text-align: right;
}
td.better {
color: #6c6;
}
td.fadeOut {
opacity: 0.5;
}
td.unknown {
color: #ccc;
}
td.worse {
color: #c66;
}
td.reference {
font-style: italic;
font-weight: bold;
color: #444;
}
td.missing {
color: #ccc;
text-align: center;
}
td.missingReference {
color: #ccc;
text-align: center;
font-style: italic;
}
.checkbox {
display: inline-block;
background: #eee;
background: linear-gradient(rgb(220, 220, 220), rgb(200, 200, 200));
border: inset 1px #ddd;
border-radius: 5px;
margin: 10px;
font-size: small;
cursor: pointer;
cursor: hand;
-webkit-user-select: none;
font-weight: bold;
}
.checkbox span {
display: inline-block;
line-height: 100%;
padding: 5px 8px;
border: outset 1px transparent;
}
.checkbox .checked {
background: #e6eeee;
background: linear-gradient(rgb(255, 255, 255), rgb(235, 235, 235));
border: outset 1px #eee;
border-radius: 5px;
}
.openAllButton {
display: inline-block;
colour: #6c6
background: #eee;
background: linear-gradient(rgb(220, 220, 220), rgb(255, 255, 255));
border: inset 1px #ddd;
border-radius: 5px;
float: left;
font-size: small;
-webkit-user-select: none;
font-weight: bold;
padding: 1px 4px;
}
.openAllButton:hover {
background: #60f060;
}
.closeAllButton {
display: inline-block;
colour: #c66
background: #eee;
background: linear-gradient(rgb(220, 220, 220),rgb(255, 255, 255));
border: inset 1px #ddd;
border-radius: 5px;
float: left;
font-size: small;
-webkit-user-select: none;
font-weight: bold;
padding: 1px 4px;
}
.closeAllButton:hover {
background: #f04040;
}
</style>
</head>
<body onload="init()">
<div style="padding: 0 10px; white-space: nowrap;">
Result <span id="time-memory" class="checkbox"></span>
Reference <span id="reference" class="checkbox"></span>
Style <span id="scatter-line" class="checkbox"><span class="checked">Scatter</span><span>Line</span></span>
<span class="checkbox"><span class="checked" id="undelete">Undelete</span></span><br>
Run your test with --reset-results to clear all runs
</div>
<table id="container"></table>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.4/jquery.min.js"></script>
<script>
%plugins%
</script>
<script>
var EXPANDED = true;
var COLLAPSED = false;
var SMALLEST_PERCENT_DISPLAYED = 0.01;
var INVISIBLE = false;
var VISIBLE = true;
var COMPARISON_SUFFIX = '_compare';
var SORT_DOWN_CLASS = 'sortDown';
var SORT_UP_CLASS = 'sortUp';
var BETTER_CLASS = 'better';
var WORSE_CLASS = 'worse';
var UNKNOWN_CLASS = 'unknown'
// px Indentation for graphs
var GRAPH_INDENT = 64;
var PADDING_UNDER_GRAPH = 5;
// px Indentation for nested children left-margins
var INDENTATION = 40;
function TestResult(metric, values, associatedRun, std, degreesOfFreedom) {
if (values) {
if (values[0] instanceof Array) {
var flattenedValues = [];
for (var i = 0; i < values.length; i++)
flattenedValues = flattenedValues.concat(values[i]);
values = flattenedValues;
}
if (jQuery.type(values[0]) === 'string') {
try {
var current = JSON.parse(values[0]);
if (current.params.type === 'HISTOGRAM') {
this.histogramValues = current;
// Histogram results have no values (per se). Instead we calculate
// the values from the histogram bins.
var values = [];
var buckets = current.buckets
for (var i = 0; i < buckets.length; i++) {
var bucket = buckets[i];
var bucket_mean = (bucket.high + bucket.low) / 2;
for (var b = 0; b < bucket.count; b++) {
values.push(bucket_mean);
}
}
}
}
catch (e) {
console.error(e, e.stack);
}
}
} else {
values = [];
}
this.test = function() { return metric; }
this.values = function() { return values.map(function(value) { return metric.scalingFactor() * value; }); }
this.unscaledMean = function() { return Statistics.sum(values) / values.length; }
this.mean = function() { return metric.scalingFactor() * this.unscaledMean(); }
this.min = function() { return metric.scalingFactor() * Statistics.min(values); }
this.max = function() { return metric.scalingFactor() * Statistics.max(values); }
this.confidenceIntervalDelta = function() {
if (std !== undefined) {
return metric.scalingFactor() * Statistics.confidenceIntervalDeltaFromStd(0.95, values.length,
std, degreesOfFreedom);
}
return metric.scalingFactor() * Statistics.confidenceIntervalDelta(0.95, values.length,
Statistics.sum(values), Statistics.squareSum(values));
}
this.confidenceIntervalDeltaRatio = function() { return this.confidenceIntervalDelta() / this.mean(); }
this.percentDifference = function(other) {
if (other === undefined) {
return undefined;
}
return (other.unscaledMean() - this.unscaledMean()) / this.unscaledMean();
}
this.isStatisticallySignificant = function(other) {
if (other === undefined) {
return false;
}
var diff = Math.abs(other.mean() - this.mean());
return diff > this.confidenceIntervalDelta() && diff > other.confidenceIntervalDelta();
}
this.run = function() { return associatedRun; }
}
function TestRun(entry) {
this.id = function() { return entry['buildTime'].replace(/[:.-]/g,''); }
this.label = function() {
if (labelKey in localStorage)
return localStorage[labelKey];
return entry['label'];
}
this.setLabel = function(label) { localStorage[labelKey] = label; }
this.isHidden = function() { return localStorage[hiddenKey]; }
this.hide = function() { localStorage[hiddenKey] = true; }
this.show = function() { localStorage.removeItem(hiddenKey); }
this.description = function() {
return new Date(entry['buildTime']).toLocaleString() + '\n' + entry['platform'] + ' ' + this.label();
}
var labelKey = 'telemetry_label_' + this.id();
var hiddenKey = 'telemetry_hide_' + this.id();
}
function PerfTestMetric(name, metric, unit, isImportant) {
var testResults = [];
var cachedUnit = null;
var cachedScalingFactor = null;
// We can't do this in TestResult because all results for each test need to share the same unit and the same scaling factor.
function computeScalingFactorIfNeeded() {
// FIXME: We shouldn't be adjusting units on every test result.
// We can only do this on the first test.
if (!testResults.length || cachedUnit)
return;
var mean = testResults[0].unscaledMean(); // FIXME: We should look at all values.
var kilo = unit == 'bytes' ? 1024 : 1000;
if (mean > 10 * kilo * kilo && unit != 'ms') {
cachedScalingFactor = 1 / kilo / kilo;
cachedUnit = 'M ' + unit;
} else if (mean > 10 * kilo) {
cachedScalingFactor = 1 / kilo;
cachedUnit = unit == 'ms' ? 's' : ('K ' + unit);
} else {
cachedScalingFactor = 1;
cachedUnit = unit;
}
}
this.name = function() { return name + ':' + metric; }
this.isImportant = isImportant;
this.isMemoryTest = function() {
return (unit == 'kb' ||
unit == 'KB' ||
unit == 'MB' ||
unit == 'bytes' ||
unit == 'count' ||
!metric.indexOf('V8.'));
}
this.addResult = function(newResult) {
testResults.push(newResult);
cachedUnit = null;
cachedScalingFactor = null;
}
this.results = function() { return testResults; }
this.scalingFactor = function() {
computeScalingFactorIfNeeded();
return cachedScalingFactor;
}
this.unit = function() {
computeScalingFactorIfNeeded();
return cachedUnit;
}
this.biggerIsBetter = function() {
if (window.unitToBiggerIsBetter == undefined) {
window.unitToBiggerIsBetter = {};
var units = JSON.parse(document.getElementById('units-json').textContent);
for (var u in units) {
if (units[u].improvement_direction == 'up') {
window.unitToBiggerIsBetter[u] = true;
}
}
}
return window.unitToBiggerIsBetter[unit];
}
}
function UndeleteManager() {
var key = 'telemetry_undeleteIds'
var undeleteIds = localStorage[key];
if (undeleteIds) {
undeleteIds = JSON.parse(undeleteIds);
} else {
undeleteIds = [];
}
this.ondelete = function(id) {
undeleteIds.push(id);
localStorage[key] = JSON.stringify(undeleteIds);
}
this.undeleteMostRecent = function() {
if (!this.mostRecentlyDeletedId())
return;
undeleteIds.pop();
localStorage[key] = JSON.stringify(undeleteIds);
}
this.mostRecentlyDeletedId = function() {
if (!undeleteIds.length)
return undefined;
return undeleteIds[undeleteIds.length-1];
}
}
var undeleteManager = new UndeleteManager();
var plotColor = 'rgb(230,50,50)';
var subpointsPlotOptions = {
lines: {show:true, lineWidth: 0},
color: plotColor,
points: {show: true, radius: 1},
bars: {show: false}};
var mainPlotOptions = {
xaxis: {
min: -0.5,
tickSize: 1,
},
crosshair: { mode: 'y' },
series: { shadowSize: 0 },
bars: {show: true, align: 'center', barWidth: 0.5},
lines: { show: false },
points: { show: true },
grid: {
borderWidth: 1,
borderColor: '#ccc',
backgroundColor: '#fff',
hoverable: true,
autoHighlight: false,
}
};
var linePlotOptions = {
yaxis: { show: false },
xaxis: { show: false },
lines: { show: true },
grid: { borderWidth: 1, borderColor: '#ccc' },
colors: [ plotColor ]
};
var largeLinePlotOptions = {
xaxis: {
show: true,
tickDecimals: 0,
},
lines: { show: true },
grid: { borderWidth: 1, borderColor: '#ccc' },
colors: [ plotColor ]
};
var histogramPlotOptions = {
bars: {show: true, fill: 1}
};
function createPlot(container, test, useLargeLinePlots) {
if (test.results()[0].histogramValues) {
var section = $('<section><div class="histogram-plots"></div>'
+ '<div class="histogram-plot-labels"></div>'
+ '<span class="tooltip"></span></section>');
$(container).append(section);
attachHistogramPlots(test, section.children('.histogram-plots'));
}
else if (useLargeLinePlots) {
var section = $('<section><div class="large-line-plots"></div>'
+ '<div class="large-line-plot-labels"></div>'
+ '<span class="tooltip"></span></section>');
$(container).append(section);
attachLinePlots(test, section.children('.large-line-plots'), useLargeLinePlots);
attachLinePlotLabels(test, section.children('.large-line-plot-labels'));
} else {
var section = $('<section><div class="plot"></div><div class="line-plots"></div>'
+ '<span class="tooltip"></span></section>');
section.children('.plot').css({'width': (100 * test.results().length + 25) + 'px', 'height': '300px'});
$(container).append(section);
var plotContainer = section.children('.plot');
var minIsZero = true;
attachPlot(test, plotContainer, minIsZero);
attachLinePlots(test, section.children('.line-plots'), useLargeLinePlots);
var tooltip = section.children('.tooltip');
plotContainer.bind('plothover', function(event, position, item) {
if (item) {
var postfix = item.series.id ? ' (' + item.series.id + ')' : '';
tooltip.html(item.datapoint[1].toPrecision(4) + postfix);
var sectionOffset = $(section).offset();
tooltip.css({left: item.pageX - sectionOffset.left - tooltip.outerWidth() / 2, top: item.pageY - sectionOffset.top + 10});
tooltip.fadeIn(200);
} else
tooltip.hide();
});
plotContainer.mouseout(function() {
tooltip.hide();
});
plotContainer.click(function(event) {
event.preventDefault();
minIsZero = !minIsZero;
attachPlot(test, plotContainer, minIsZero);
});
}
return section;
}
function attachLinePlots(test, container, useLargeLinePlots) {
var results = test.results();
var attachedPlot = false;
if (useLargeLinePlots) {
var maximum = 0;
for (var i = 0; i < results.length; i++) {
var values = results[i].values();
if (!values)
continue;
var local_max = Math.max.apply(Math, values);
if (local_max > maximum)
maximum = local_max;
}
}
for (var i = 0; i < results.length; i++) {
container.append('<div></div>');
var values = results[i].values();
if (!values)
continue;
attachedPlot = true;
if (useLargeLinePlots) {
var options = $.extend(true, {}, largeLinePlotOptions,
{yaxis: {min: 0.0, max: maximum},
xaxis: {min: 0.0, max: values.length - 1},
points: {show: (values.length < 2) ? true : false}});
} else {
var options = $.extend(true, {}, linePlotOptions,
{yaxis: {min: Math.min.apply(Math, values) * 0.9, max: Math.max.apply(Math, values) * 1.1},
xaxis: {min: -0.5, max: values.length - 0.5},
points: {show: (values.length < 2) ? true : false}});
}
$.plot(container.children().last(), [values.map(function(value, index) { return [index, value]; })], options);
}
if (!attachedPlot)
container.children().remove();
}
function attachHistogramPlots(test, container) {
var results = test.results();
var attachedPlot = false;
for (var i = 0; i < results.length; i++) {
container.append('<div></div>');
var histogram = results[i].histogramValues
if (!histogram)
continue;
attachedPlot = true;
var buckets = histogram.buckets
var bucket;
var max_count = 0;
for (var j = 0; j < buckets.length; j++) {
bucket = buckets[j];
max_count = Math.max(max_count, bucket.count);
}
var xmax = bucket.high * 1.1;
var ymax = max_count * 1.1;
var options = $.extend(true, {}, histogramPlotOptions,
{yaxis: {min: 0.0, max: ymax},
xaxis: {min: histogram.params.min, max: xmax}});
var plot = $.plot(container.children().last(), [[]], options);
// Flot only supports fixed with bars and our histogram's buckets are
// variable width, so we need to do our own bar drawing.
var ctx = plot.getCanvas().getContext("2d");
ctx.lineWidth="1";
ctx.fillStyle = "rgba(255, 0, 0, 0.2)";
ctx.strokeStyle="red";
for (var j = 0; j < buckets.length; j++) {
bucket = buckets[j];
var bl = plot.pointOffset({ x: bucket.low, y: 0});
var tr = plot.pointOffset({ x: bucket.high, y: bucket.count});
ctx.fillRect(bl.left, bl.top, tr.left - bl.left, tr.top - bl.top);
ctx.strokeRect(bl.left, bl.top, tr.left - bl.left, tr.top - bl.top);
}
}
if (!attachedPlot)
container.children().remove();
}
function attachLinePlotLabels(test, container) {
var results = test.results();
var attachedPlot = false;
for (var i = 0; i < results.length; i++) {
container.append('<div>' + results[i].run().label() + '</div>');
}
}
function attachPlot(test, plotContainer, minIsZero) {
var results = test.results();
var values = results.reduce(function(values, result, index) {
var newValues = result.values();
return newValues ? values.concat(newValues.map(function(value) { return [index, value]; })) : values;
}, []);
var plotData = [$.extend(true, {}, subpointsPlotOptions, {data: values})];
plotData.push({id: 'μ', data: results.map(function(result, index) { return [index, result.mean()]; }), color: plotColor});
var overallMax = Statistics.max(results.map(function(result, index) { return result.max(); }));
var overallMin = Statistics.min(results.map(function(result, index) { return result.min(); }));
var margin = (overallMax - overallMin) * 0.1;
var currentPlotOptions = $.extend(true, {}, mainPlotOptions, {yaxis: {
min: minIsZero ? 0 : overallMin - margin,
max: minIsZero ? overallMax * 1.1 : overallMax + margin}});
currentPlotOptions.xaxis.max = results.length - 0.5;
currentPlotOptions.xaxis.ticks = results.map(function(result, index) { return [index, result.run().label()]; });
$.plot(plotContainer, plotData, currentPlotOptions);
}
function toFixedWidthPrecision(value) {
var decimal = value.toFixed(2);
return decimal;
}
function formatPercentage(fraction) {
var percentage = fraction * 100;
return (fraction * 100).toFixed(2) + '%';
}
function setUpSortClicks(runs)
{
$('#nameColumn').click(sortByName);
$('#unitColumn').click(sortByUnit);
runs.forEach(function(run) {
$('#' + run.id()).click(sortByResult);
$('#' + run.id() + COMPARISON_SUFFIX).click(sortByReference);
});
}
function TestTypeSelector(tests) {
this.recognizers = {
'Time': function(test) { return !test.isMemoryTest(); },
'Memory': function(test) { return test.isMemoryTest(); }
};
this.testTypeNames = this.generateUsedTestTypeNames(tests);
// Default to selecting the first test-type name in the list.
this.testTypeName = this.testTypeNames[0];
}
TestTypeSelector.prototype = {
set testTypeName(testTypeName) {
this._testTypeName = testTypeName;
this.shouldShowTest = this.recognizers[testTypeName];
},
generateUsedTestTypeNames: function(allTests) {
var testTypeNames = [];
for (var recognizedTestName in this.recognizers) {
var recognizes = this.recognizers[recognizedTestName];
for (var testName in allTests) {
var test = allTests[testName];
if (recognizes(test)) {
testTypeNames.push(recognizedTestName);
break;
}
}
}
if (testTypeNames.length === 0) {
// No test types we recognize, add 'No Results' with a dummy recognizer.
var noResults = 'No Results';
this.recognizers[noResults] = function() { return false; };
testTypeNames.push(noResults);
} else if (testTypeNames.length > 1) {
// We have more than one test type, so add 'All' with a recognizer that always succeeds.
var allResults = 'All';
this.recognizers[allResults] = function() { return true; };
testTypeNames.push(allResults);
}
return testTypeNames;
},
buildButtonHTMLForUsedTestTypes: function() {
var selectedTestTypeName = this._testTypeName;
// Build spans for all recognised test names with the selected test highlighted.
return this.testTypeNames.map(function(testTypeName) {
var classAttribute = testTypeName === selectedTestTypeName ? ' class=checked' : '';
return '<span' + classAttribute + '>' + testTypeName + '</span>';
}).join('');
}
};
var topLevelRows;
var allTableRows;
function displayTable(tests, runs, testTypeSelector, referenceIndex, useLargeLinePlots) {
var resultHeaders = runs.map(function(run, index) {
var header = '<th id="' + run.id() + '" ' +
'colspan=2 ' +
'title="' + run.description() + '">' +
'<span class="label" ' +
'title="Edit run label">' +
run.label() +
'</span>' +
'<div class="closeButton" ' +
'title="Delete run">' +
'×' +
'</div>' +
'</th>';
if (index !== referenceIndex) {
header += '<th id="' + run.id() + COMPARISON_SUFFIX + '" ' +
'title="Sort by better/worse">' +
'Δ' +
'</th>';
}
return header;
});
resultHeaders = resultHeaders.join('');
htmlString = '<thead>' +
'<tr>' +
'<th id="nameColumn">' +
'<div class="openAllButton" ' +
'title="Open all rows or graphs">' +
'Open All' +
'</div>' +
'<div class="closeAllButton" ' +
'title="Close all rows">' +
'Close All' +
'</div>' +
'Test' +
'</th>' +
'<th id="unitColumn">' +
'Unit' +
'</th>' +
resultHeaders +
'</tr>' +
'</head>' +
'<tbody>' +
'</tbody>';
$('#container').html(htmlString);
var testNames = [];
for (testName in tests)
testNames.push(testName);
allTableRows = [];
testNames.forEach(function(testName) {
var test = tests[testName];
if (testTypeSelector.shouldShowTest(test)) {
allTableRows.push(new TableRow(runs, test, referenceIndex, useLargeLinePlots));
}
});
// Build a list of top level rows with attached children
topLevelRows = [];
allTableRows.forEach(function(row) {
// Add us to top level if we are a top-level row...
if (row.hasNoURL) {
topLevelRows.push(row);
// Add a duplicate child row that holds the graph for the parent
var graphHolder = new TableRow(runs, row.test, referenceIndex, useLargeLinePlots);
graphHolder.isImportant = true;
graphHolder.URL = 'Summary';
graphHolder.hideRowData();
allTableRows.push(graphHolder);
row.addNestedChild(graphHolder);
return;
}
// ...or add us to our parent if we have one ...
for (var i = 0; i < allTableRows.length; i++) {
if (allTableRows[i].isParentOf(row)) {
allTableRows[i].addNestedChild(row);
return;
}
}
// ...otherwise this result is orphaned, display it at top level with a graph
row.hasGraph = true;
topLevelRows.push(row);
});
buildTable(topLevelRows);
$('.closeButton').click(function(event) {
for (var i = 0; i < runs.length; i++) {
if (runs[i].id() == event.target.parentNode.id) {
runs[i].hide();
undeleteManager.ondelete(runs[i].id());
location.reload();
break;
}
}
event.stopPropagation();
});
$('.closeAllButton').click(function(event) {
for (var i = 0; i < allTableRows.length; i++) {
allTableRows[i].closeRow();
}
event.stopPropagation();
});
$('.openAllButton').click(function(event) {
for (var i = 0; i < topLevelRows.length; i++) {
topLevelRows[i].openRow();
}
event.stopPropagation();
});
setUpSortClicks(runs);
$('.label').click(function(event) {
for (var i = 0; i < runs.length; i++) {
if (runs[i].id() == event.target.parentNode.id) {
$(event.target).replaceWith('<input id="labelEditor" type="text" value="' + runs[i].label() + '">');
$('#labelEditor').focusout(function() {
runs[i].setLabel(this.value);
location.reload();
});
$('#labelEditor').keypress(function(event) {
if (event.which == 13) {
runs[i].setLabel(this.value);
location.reload();
}
});
$('#labelEditor').click(function(event) {
event.stopPropagation();
});
$('#labelEditor').mousedown(function(event) {
event.stopPropagation();
});
$('#labelEditor').select();
break;
}
}
event.stopPropagation();
});
}
function validForSorting(row) {
return ($.type(row.sortValue) === 'string') || !isNaN(row.sortValue);
}
var sortDirection = 1;
function sortRows(rows) {
rows.sort(
function(rowA,rowB) {
if (validForSorting(rowA) !== validForSorting(rowB)) {
// Sort valid values upwards when compared to invalid
if (validForSorting(rowA)) {
return -1;
}
if (validForSorting(rowB)) {
return 1;
}
}
// Some rows always sort to the top
if (rowA.isImportant) {
return -1;
}
if (rowB.isImportant) {
return 1;
}
if (rowA.sortValue === rowB.sortValue) {
// Sort identical values by name to keep the sort stable,
// always keep name alphabetical (even if a & b sort values
// are invalid)
return rowA.test.name() > rowB.test.name() ? 1 : -1;
}
return rowA.sortValue > rowB.sortValue ? sortDirection : -sortDirection;
} );
// Sort the rows' children
rows.forEach(function(row) {
sortRows(row.children);
});
}
function buildTable(rows) {
rows.forEach(function(row) {
row.removeFromPage();
});
sortRows(rows);
rows.forEach(function(row) {
row.addToPage();
});
}
var activeSortHeaderElement = undefined;
var columnSortDirection = {};
function determineColumnSortDirection(element) {
columnDirection = columnSortDirection[element.id];
if (columnDirection === undefined) {
// First time we've sorted this row, default to down
columnSortDirection[element.id] = SORT_DOWN_CLASS;
} else if (element === activeSortHeaderElement) {
// Clicking on same header again, swap direction
columnSortDirection[element.id] = (columnDirection === SORT_UP_CLASS) ? SORT_DOWN_CLASS : SORT_UP_CLASS;
}
}
function updateSortDirection(element) {
// Remove old header's sort arrow
if (activeSortHeaderElement !== undefined) {
activeSortHeaderElement.classList.remove(columnSortDirection[activeSortHeaderElement.id]);
}
determineColumnSortDirection(element);
sortDirection = (columnSortDirection[element.id] === SORT_UP_CLASS) ? 1 : -1;
// Add new header's sort arrow
element.classList.add(columnSortDirection[element.id]);
activeSortHeaderElement = element;
}
function sortByName(event) {
updateSortDirection(event.toElement);
allTableRows.forEach(function(row) {
row.prepareToSortByName();
});
buildTable(topLevelRows);
}
function sortByUnit(event) {
updateSortDirection(event.toElement);
allTableRows.forEach(function(row) {
row.prepareToSortByUnit();
});
buildTable(topLevelRows);
}
function sortByResult(event) {
updateSortDirection(event.toElement);
var runId = event.target.id;
allTableRows.forEach(function(row) {
row.prepareToSortByTestResults(runId);
});
buildTable(topLevelRows);
}
function sortByReference(event) {
updateSortDirection(event.toElement);
// The element ID has _compare appended to allow us to set up a click event
// remove the _compare to return a useful Id
var runIdWithCompare = event.target.id;
var runId = runIdWithCompare.split('_')[0];
allTableRows.forEach(function(row) {
row.prepareToSortRelativeToReference(runId);
});
buildTable(topLevelRows);
}
function linearRegression(points) {
// Implement http://www.easycalculation.com/statistics/learn-correlation.php.
// x = magnitude
// y = iterations
var sumX = 0;
var sumY = 0;
var sumXSquared = 0;
var sumYSquared = 0;
var sumXTimesY = 0;
for (var i = 0; i < points.length; i++) {
var x = i;
var y = points[i];
sumX += x;
sumY += y;
sumXSquared += x * x;
sumYSquared += y * y;
sumXTimesY += x * y;
}
var r = (points.length * sumXTimesY - sumX * sumY) /
Math.sqrt((points.length * sumXSquared - sumX * sumX) *
(points.length * sumYSquared - sumY * sumY));
if (isNaN(r) || r == Math.Infinity)
r = 0;
var slope = (points.length * sumXTimesY - sumX * sumY) / (points.length * sumXSquared - sumX * sumX);
var intercept = sumY / points.length - slope * sumX / points.length;
return {slope: slope, intercept: intercept, rSquared: r * r};
}
var warningSign = '<svg viewBox="0 0 100 100" style="width: 18px; height: 18px; vertical-align: bottom;" version="1.1">'
+ '<polygon fill="red" points="50,10 90,80 10,80 50,10" stroke="red" stroke-width="10" stroke-linejoin="round" />'
+ '<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" />'
+ '<circle cx="50" cy="73" r="6" fill="white" />'
+ '</svg>';
function TableRow(runs, test, referenceIndex, useLargeLinePlots) {
this.runs = runs;
this.test = test;
this.referenceIndex = referenceIndex;
this.useLargeLinePlots = useLargeLinePlots;
this.children = [];
this.tableRow = $('<tr class="highlight">' +
'<td class="test collapsed" >' +
this.test.name() +
'</td>' +
'<td class="unit">' +
this.test.unit() +
'</td>' +
'</tr>');
var runIndex = 0;
var results = this.test.results();
var referenceResult = undefined;
this.resultIndexMap = {};
for (var i = 0; i < results.length; i++) {
while (this.runs[runIndex] !== results[i].run())
runIndex++;
if (runIndex === this.referenceIndex)
referenceResult = results[i];
this.resultIndexMap[runIndex] = i;
}
for (var i = 0; i < this.runs.length; i++) {
var resultIndex = this.resultIndexMap[i];
if (resultIndex === undefined)
this.tableRow.append(this.markupForMissingRun(i == this.referenceIndex));
else
this.tableRow.append(this.markupForRun(results[resultIndex], referenceResult));
}
// Use the test name (without URL) to bind parents and their children
var nameAndURL = this.test.name().split('.');
var benchmarkName = nameAndURL.shift();
this.testName = nameAndURL.shift();
this.hasNoURL = (nameAndURL.length === 0);
if (!this.hasNoURL) {
// Re-join the URL
this.URL = nameAndURL.join('.');
}
this.isImportant = false;
this.hasGraph = false;
this.currentIndentationClass = ''
this.indentLevel = 0;
this.setRowNestedState(COLLAPSED);
this.setVisibility(VISIBLE);
this.prepareToSortByName();
}
TableRow.prototype.hideRowData = function() {
data = this.tableRow.children('td');
for (index in data) {
if (index > 0) {
// Blank out everything except the test name
data[index].innerHTML = '';
}
}
}
TableRow.prototype.prepareToSortByTestResults = function(runId) {
var testResults = this.test.results();
// Find the column in this row that matches the runId and prepare to
// sort by the mean of that test.
for (index in testResults) {
sourceId = testResults[index].run().id();
if (runId === sourceId) {
this.sortValue = testResults[index].mean();
return;
}
}
// This row doesn't have any results for the passed runId
this.sortValue = undefined;
}
TableRow.prototype.prepareToSortRelativeToReference = function(runId) {
var testResults = this.test.results();
// Get index of test results that correspond to the reference column.
var remappedReferenceIndex = this.resultIndexMap[this.referenceIndex];
if (remappedReferenceIndex === undefined) {
// This test has no results in the reference run.
this.sortValue = undefined;
return;
}
otherResults = testResults[remappedReferenceIndex];
// Find the column in this row that matches the runId and prepare to
// sort by the difference from the reference.
for (index in testResults) {
sourceId = testResults[index].run().id();
if (runId === sourceId) {
this.sortValue = testResults[index].percentDifference(otherResults);
if (this.test.biggerIsBetter()) {
// For this test bigger is not better
this.sortValue = -this.sortValue;
}
return;
}
}
// This row doesn't have any results for the passed runId
this.sortValue = undefined;
}
TableRow.prototype.prepareToSortByUnit = function() {
this.sortValue = this.test.unit().toLowerCase();
}
TableRow.prototype.prepareToSortByName = function() {
this.sortValue = this.test.name().toLowerCase();
}
TableRow.prototype.isParentOf = function(row) {
return this.hasNoURL && (this.testName === row.testName);
}
TableRow.prototype.addNestedChild = function(child) {
this.children.push(child);
// Indent child one step in from parent
child.indentLevel = this.indentLevel + INDENTATION;
child.hasGraph = true;
// Start child off as hidden (i.e. collapsed inside parent)
child.setVisibility(INVISIBLE);
child.updateIndentation();
// Show URL in the title column
child.tableRow.children()[0].innerHTML = child.URL;
// Set up class to change background colour of nested rows
if (child.isImportant) {
child.tableRow.addClass('importantNestedRow');
} else {
child.tableRow.addClass('nestedRow');
}
}
TableRow.prototype.setVisibility = function(visibility) {
this.visibility = visibility;
this.tableRow[0].style.display = (visibility === INVISIBLE) ? 'none' : '';
}
TableRow.prototype.setRowNestedState = function(newState) {
this.rowState = newState;
this.updateIndentation();
}
TableRow.prototype.updateIndentation = function() {
var element = this.tableRow.children('td').first();
element.removeClass(this.currentIndentationClass);
this.currentIndentationClass = (this.rowState === COLLAPSED) ? 'collapsed' : 'expanded';
element[0].style.marginLeft = this.indentLevel.toString() + 'px';
element[0].style.float = 'left';
element.addClass(this.currentIndentationClass);
}
TableRow.prototype.addToPage = function() {
$('#container').children('tbody').last().append(this.tableRow);
// Set up click callback
var owningObject = this;
this.tableRow.click(function(event) {
event.preventDefault();
owningObject.toggle();
});
// Add children to the page too
this.children.forEach(function(child) {
child.addToPage();
});
}
TableRow.prototype.removeFromPage = function() {
// Remove children
this.children.forEach(function(child) {
child.removeFromPage();
});
// Remove us
this.tableRow.remove();
}
TableRow.prototype.markupForRun = function(result, referenceResult) {
var comparisonCell = '';
var shouldCompare = result !== referenceResult;
if (shouldCompare) {
var comparisonText = '';
var className = '';
if (referenceResult) {
var percentDifference = referenceResult.percentDifference(result);
if (isNaN(percentDifference)) {
comparisonText = 'Unknown';
className = UNKNOWN_CLASS;
} else if (Math.abs(percentDifference) < SMALLEST_PERCENT_DISPLAYED) {
comparisonText = 'Equal';
// Show equal values in green
className = BETTER_CLASS;
} else {
var better = this.test.biggerIsBetter() ? percentDifference > 0 : percentDifference < 0;
comparisonText = formatPercentage(Math.abs(percentDifference)) + (better ? ' Better' : ' Worse');
className = better ? BETTER_CLASS : WORSE_CLASS;
}
if (!referenceResult.isStatisticallySignificant(result)) {
// Put result in brackets and fade if not statistically significant
className += ' fadeOut';
comparisonText = '(' + comparisonText + ')';
}
}
comparisonCell = '<td class="comparison ' + className + '">' + comparisonText + '</td>';
}
var values = result.values();
var warning = '';
var regressionAnalysis = '';
if (result.histogramValues) {
// Don't calculate regression result for histograms.
} else if (values && values.length > 3) {
regressionResult = linearRegression(values);
regressionAnalysis = 'slope=' + toFixedWidthPrecision(regressionResult.slope)
+ ', R^2=' + toFixedWidthPrecision(regressionResult.rSquared);
if (regressionResult.rSquared > 0.6 && Math.abs(regressionResult.slope) > 0.01) {
warning = ' <span class="regression-warning" title="Detected a time dependency with ' + regressionAnalysis + '">' + warningSign + ' </span>';
}
}
var referenceClass = shouldCompare ? '' : 'reference';
var statistics = 'σ=' + toFixedWidthPrecision(result.confidenceIntervalDelta()) + ', min=' + toFixedWidthPrecision(result.min())
+ ', max=' + toFixedWidthPrecision(result.max()) + '\n' + regressionAnalysis;
var confidence;
if (isNaN(result.confidenceIntervalDeltaRatio())) {
// Don't bother showing +- Nan as it is meaningless
confidence = '';
} else {
confidence = '± ' + formatPercentage(result.confidenceIntervalDeltaRatio());
}
return '<td class="result ' + referenceClass + '" title="' + statistics + '">' + toFixedWidthPrecision(result.mean())
+ '</td><td class="confidenceIntervalDelta ' + referenceClass + '" title="' + statistics + '">' + confidence + warning + '</td>' + comparisonCell;
}
TableRow.prototype.markupForMissingRun = function(isReference) {
if (isReference) {
return '<td colspan=2 class="missingReference">Missing</td>';
}
return '<td colspan=3 class="missing">Missing</td>';
}
TableRow.prototype.openRow = function() {
if (this.rowState === EXPANDED) {
// If we're already expanded, open our children instead
this.children.forEach(function(child) {
child.openRow();
});
return;
}
this.setRowNestedState(EXPANDED);
if (this.hasGraph) {
var firstCell = this.tableRow.children('td').first();
var plot = createPlot(firstCell, this.test, this.useLargeLinePlots);
plot.css({'position': 'absolute', 'z-index': 2});
var offset = this.tableRow.offset();
offset.left += GRAPH_INDENT;
offset.top += this.tableRow.outerHeight();
plot.offset(offset);
this.tableRow.children('td').css({'padding-bottom': plot.outerHeight() + PADDING_UNDER_GRAPH});
}
this.children.forEach(function(child) {
child.setVisibility(VISIBLE);
});
if (this.children.length === 1) {
// If we only have a single child...
var child = this.children[0];
if (child.isImportant) {
// ... and it is important (i.e. the summary row) just open it when
// parent is opened to save needless clicking
child.openRow();
}
}
}
TableRow.prototype.closeRow = function() {
if (this.rowState === COLLAPSED) {
return;
}
this.setRowNestedState(COLLAPSED);
if (this.hasGraph) {
var firstCell = this.tableRow.children('td').first();
firstCell.children('section').remove();
this.tableRow.children('td').css({'padding-bottom': ''});
}
this.children.forEach(function(child) {
// Make children invisible, but leave their collapsed status alone
child.setVisibility(INVISIBLE);
});
}
TableRow.prototype.toggle = function() {
if (this.rowState === EXPANDED) {
this.closeRow();
} else {
this.openRow();
}
return false;
}
function init() {
var runs = [];
var metrics = {};
var deletedRunsById = {};
$.each(JSON.parse(document.getElementById('results-json').textContent), function(index, entry) {
var run = new TestRun(entry);
if (run.isHidden()) {
deletedRunsById[run.id()] = run;
return;
}
runs.push(run);
function addTests(tests) {
for (var testName in tests) {
var rawMetrics = tests[testName].metrics;
for (var metricName in rawMetrics) {
var fullMetricName = testName + ':' + metricName;
var metric = metrics[fullMetricName];
if (!metric) {
metric = new PerfTestMetric(testName, metricName, rawMetrics[metricName].units, rawMetrics[metricName].important);
metrics[fullMetricName] = metric;
}
// std & degrees_of_freedom could be undefined
metric.addResult(
new TestResult(metric, rawMetrics[metricName].current,
run, rawMetrics[metricName]['std'], rawMetrics[metricName]['degrees_of_freedom']));
}
}
}
addTests(entry.tests);
});
var useLargeLinePlots = false;
var referenceIndex = 0;
var testTypeSelector = new TestTypeSelector(metrics);
var buttonHTML = testTypeSelector.buildButtonHTMLForUsedTestTypes();
$('#time-memory').append(buttonHTML);
$('#scatter-line').bind('change', function(event, checkedElement) {
useLargeLinePlots = checkedElement.textContent == 'Line';
displayTable(metrics, runs, testTypeSelector, referenceIndex, useLargeLinePlots);
});
runs.map(function(run, index) {
$('#reference').append('<span value="' + index + '"' + (index == referenceIndex ? ' class="checked"' : '') + ' title="' + run.description() + '">' + run.label() + '</span>');
})
$('#time-memory').bind('change', function(event, checkedElement) {
testTypeSelector.testTypeName = checkedElement.textContent;
displayTable(metrics, runs, testTypeSelector, referenceIndex, useLargeLinePlots);
});
$('#reference').bind('change', function(event, checkedElement) {
referenceIndex = parseInt(checkedElement.getAttribute('value'));
displayTable(metrics, runs, testTypeSelector, referenceIndex, useLargeLinePlots);
});
displayTable(metrics, runs, testTypeSelector, referenceIndex, useLargeLinePlots);
$('.checkbox').each(function(index, checkbox) {
$(checkbox).children('span').click(function(event) {
if ($(this).hasClass('checked'))
return;
$(checkbox).children('span').removeClass('checked');
$(this).addClass('checked');
$(checkbox).trigger('change', $(this));
});
});
runToUndelete = deletedRunsById[undeleteManager.mostRecentlyDeletedId()];
if (runToUndelete) {
$('#undelete').html('Undelete ' + runToUndelete.label());
$('#undelete').attr('title', runToUndelete.description());
$('#undelete').click(function(event) {
runToUndelete.show();
undeleteManager.undeleteMostRecent();
location.reload();
});
} else {
$('#undelete').hide();
}
}
</script>
<script id="results-json" type="application/json">%json_results%</script>
<script id="units-json" type="application/json">%json_units%</script>
</body>
</html>