<!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>