diff options
author | tonyg@chromium.org <tonyg@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-08-04 21:33:29 +0000 |
---|---|---|
committer | tonyg@chromium.org <tonyg@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-08-04 21:33:29 +0000 |
commit | 9694696cd263109e42565a0598adddee1bbf4bdc (patch) | |
tree | eaf6585deea4065a1f623a41aab74cff8b1b71b3 /tools/telemetry/support | |
parent | 872e8e1683de933fe09e948ce42c2dfc6bcdf5b7 (diff) | |
download | chromium_src-9694696cd263109e42565a0598adddee1bbf4bdc.zip chromium_src-9694696cd263109e42565a0598adddee1bbf4bdc.tar.gz chromium_src-9694696cd263109e42565a0598adddee1bbf4bdc.tar.bz2 |
[Telemetry] Add HTML output and make it the default.
This is based on Blink's run-perf-tests HTML output.
BUG=264695
Review URL: https://chromiumcodereview.appspot.com/20565003
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@215542 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'tools/telemetry/support')
-rw-r--r-- | tools/telemetry/support/html_output/results-template.html | 596 |
1 files changed, 596 insertions, 0 deletions
diff --git a/tools/telemetry/support/html_output/results-template.html b/tools/telemetry/support/html_output/results-template.html new file mode 100644 index 0000000..b0b84c7 --- /dev/null +++ b/tools/telemetry/support/html_output/results-template.html @@ -0,0 +1,596 @@ +<!DOCTYPE html> +<html> +<head> +<title>Telemetry Performance Test Results</title> +<style type="text/css"> + +section { + background: white; + padding: 10px; + position: relative; +} + +.time-plots { + padding-left: 25px; +} + +.time-plots > div { + display: inline-block; + width: 90px; + height: 40px; + margin-right: 10px; +} + +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; +} + +tr.even { + background: #f6f6f6; +} + +table td { + position: relative; + font-family: monospace; +} + +th, td { + cursor: pointer; + cursor: hand; +} + +th { + background: #e6eeee; + background: -webkit-gradient(linear, left top, left bottom, from(rgb(244, 244, 244)), to(rgb(217, 217, 217))); + border: 1px solid #ccc; +} + +th:after { + content: ' \25B8'; +} + +th.headerSortUp:after { + content: ' \25BE'; +} + +th.headerSortDown:after { + content: ' \25B4'; +} + +td.comparison, td.result { + text-align: right; +} + +td.better { + color: #6c6; +} + +td.worse { + color: #c66; +} + +td.missing { + text-align: center; +} + +.checkbox { + display: inline-block; + background: #eee; + background: -webkit-gradient(linear, left bottom, left top, from(rgb(220, 220, 220)), to(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: -webkit-gradient(linear, left top, left bottom, from(rgb(255, 255, 255)), to(rgb(235, 235, 235))); + border: outset 1px #eee; + border-radius: 5px; +} + +</style> +</head> +<body onload="init()"> +<div style="padding: 0 10px; white-space: nowrap;"> +Result <span id="time-memory" class="checkbox"><span class="checked">Time</span><span>Memory</span></span> +Reference <span id="reference" class="checkbox"></span> +Run Telemetry with --reset-html-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> +function TestResult(metric, values, associatedRun) { + if (values[0] instanceof Array) { + var flattenedValues = []; + for (var i = 0; i < values.length; i++) + flattenedValues = flattenedValues.concat(values[i]); + values = flattenedValues; + } + + 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 () { + 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) { return (other.unscaledMean() - this.unscaledMean()) / this.unscaledMean(); } + this.isStatisticallySignificant = function (other) { + var diff = Math.abs(other.mean() - this.mean()); + return diff > this.confidenceIntervalDelta() && diff > other.confidenceIntervalDelta(); + } + this.run = function () { return associatedRun; } +} + +function TestRun(entry) { + this.description = function () { return entry['description']; } + this.revision = function () { return entry['revision']; } + this.label = function () { + var label = 'r' + this.revision(); + if (this.description()) + label += ' ‐ ' + this.description(); + return label; + } +} + +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'); + } + 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 unit in units) { + if (units[unit].improvement_direction == 'up') { + window.unitToBiggerIsBetter[unit] = true; + } + } + } + return window.unitToBiggerIsBetter[unit]; + } +} + +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 timePlotOptions = { + yaxis: { show: false }, + xaxis: { show: false }, + lines: { show: true }, + grid: { borderWidth: 1, borderColor: '#ccc' }, + colors: [ plotColor ] +}; + +function createPlot(container, test) { + var section = $('<section><div class="plot"></div><div class="time-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); + + attachTimePlots(test, section.children('.time-plots')); + + 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 attachTimePlots(test, container) { + var results = test.results(); + var attachedPlot = false; + for (var i = 0; i < results.length; i++) { + container.append('<div></div>'); + var values = results[i].values(); + if (!values) + continue; + attachedPlot = true; + + $.plot(container.children().last(), [values.map(function (value, index) { return [index, value]; })], + $.extend(true, {}, timePlotOptions, {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}})); + } + if (!attachedPlot) + container.children().remove(); +} + +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 createTable(tests, runs, shouldIgnoreMemory, referenceIndex) { + $('#container').html('<thead><tr><th>Test</th><th>Unit</th>' + runs.map(function (run, index) { + return '<th colspan="' + (index == referenceIndex ? 2 : 3) + '" class="{sorter: \'comparison\'}">' + run.label() + '</th>'; + }).reduce(function (markup, cell) { return markup + cell; }, '') + '</tr></head><tbody></tbody>'); + + var testNames = []; + for (testName in tests) + testNames.push(testName); + + testNames.sort().map(function (testName) { + var test = tests[testName]; + if (test.isMemoryTest() != shouldIgnoreMemory) + createTableRow(runs, test, referenceIndex); + }); + + $('#container').tablesorter({widgets: ['zebra']}); +} + +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 createTableRow(runs, test, referenceIndex) { + var tableRow = $('<tr><td class="test"' + (test.isImportant ? ' style="font-weight:bold"' : '') + '>' + test.name() + '</td><td class="unit">' + test.unit() + '</td></tr>'); + + function markupForRun(result, referenceResult) { + var comparisonCell = ''; + var hiddenValue = ''; + var shouldCompare = result !== referenceResult; + if (shouldCompare && referenceResult) { + var percentDifference = referenceResult.percentDifference(result); + var better = test.biggerIsBetter() ? percentDifference > 0 : percentDifference < 0; + var comparison = ''; + var className = 'comparison'; + if (referenceResult.isStatisticallySignificant(result)) { + comparison = formatPercentage(Math.abs(percentDifference)) + (better ? ' Better' : ' Worse '); + className += better ? ' better' : ' worse'; + } + hiddenValue = '<span style="display: none">|' + comparison + '</span>'; + comparisonCell = '<td class="' + className + '">' + comparison + '</td>'; + } else if (shouldCompare) + comparisonCell = '<td class="comparison"></td>'; + + var values = result.values(); + var warning = ''; + var regressionAnalysis = ''; + 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 statistics = 'σ=' + toFixedWidthPrecision(result.confidenceIntervalDelta()) + ', min=' + toFixedWidthPrecision(result.min()) + + ', max=' + toFixedWidthPrecision(result.max()) + '\n' + regressionAnalysis; + + // Tablesorter doesn't know about the second cell so put the comparison in the invisible element. + return '<td class="result" title="' + statistics + '">' + toFixedWidthPrecision(result.mean()) + hiddenValue + + '</td><td class="confidenceIntervalDelta" title="' + statistics + '">± ' + + formatPercentage(result.confidenceIntervalDeltaRatio()) + warning + '</td>' + comparisonCell; + } + + function markupForMissingRun(isReference) { + return '<td colspan="' + (isReference ? 2 : 3) + '" class="missing">Missing</td>'; + } + + var runIndex = 0; + var results = test.results(); + var referenceResult = undefined; + var resultIndexMap = {}; + for (var i = 0; i < results.length; i++) { + while (runs[runIndex] !== results[i].run()) + runIndex++; + if (runIndex == referenceIndex) + referenceResult = results[i]; + resultIndexMap[runIndex] = i; + } + for (var i = 0; i < runs.length; i++) { + var resultIndex = resultIndexMap[i]; + if (resultIndex == undefined) + tableRow.append(markupForMissingRun(i == referenceIndex)); + else + tableRow.append(markupForRun(results[resultIndex], referenceResult)); + } + + $('#container').children('tbody').last().append(tableRow); + + function toggle() { + var firstCell = tableRow.children('td').first(); + if (firstCell.children('section').length) { + firstCell.children('section').remove(); + tableRow.children('td').css({'padding-bottom': ''}); + } else { + var plot = createPlot(firstCell, test); + plot.css({'position': 'absolute', 'z-index': 2}); + var offset = tableRow.offset(); + offset.left += 1; + offset.top += tableRow.outerHeight(); + plot.offset(offset); + tableRow.children('td').css({'padding-bottom': plot.outerHeight() + 5}); + } + + return false; + }; + + tableRow.click(function(event) { + if (event.target != tableRow[0] && event.target.parentNode != tableRow[0]) + return; + + event.preventDefault(); + + toggle(); + }); + + if (test.isImportant) { + toggle(); + } +} + +function init() { + $.tablesorter.addParser({ + id: 'comparison', + is: function(s) { + return s.indexOf('|') >= 0; + }, + format: function(s) { + var parsed = parseFloat(s.substring(s.indexOf('|') + 1)); + return isNaN(parsed) ? 0 : parsed; + }, + type: 'numeric', + }); + + var runs = []; + var metrics = {}; + $.each(JSON.parse(document.getElementById('results-json').textContent), function (index, entry) { + var run = new TestRun(entry); + runs.push(run); + + function addTests(tests, parentFullName) { + for (var testName in tests) { + var fullTestName = parentFullName + '/' + testName; + var rawMetrics = tests[testName].metrics; + + for (var metricName in rawMetrics) { + var fullMetricName = fullTestName + ':' + metricName; + var metric = metrics[fullMetricName]; + if (!metric) { + metric = new PerfTestMetric(fullTestName, metricName, rawMetrics[metricName].units, rawMetrics[metricName].important); + metrics[fullMetricName] = metric; + } + metric.addResult(new TestResult(metric, rawMetrics[metricName].current, run)); + } + + if (tests[testName].tests) + addTests(tests[testName].tests, fullTestName); + } + } + + addTests(entry.tests, ''); + }); + + var shouldIgnoreMemory= true; + var referenceIndex = 0; + + createTable(metrics, runs, shouldIgnoreMemory, referenceIndex); + + $('#time-memory').bind('change', function (event, checkedElement) { + shouldIgnoreMemory = checkedElement.textContent == 'Time'; + createTable(metrics, runs, shouldIgnoreMemory, referenceIndex); + }); + + runs.map(function (run, index) { + $('#reference').append('<span value="' + index + '"' + (index == referenceIndex ? ' class="checked"' : '') + '>' + run.label() + '</span>'); + }) + + $('#reference').bind('change', function (event, checkedElement) { + referenceIndex = parseInt(checkedElement.getAttribute('value')); + createTable(metrics, runs, shouldIgnoreMemory, referenceIndex); + }); + + $('.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)); + }); + }); +} + +</script> +<script id="results-json" type="application/json">%json_results%</script> +<script id="units-json" type="application/json">%json_units%</script> +</body> +</html> |