| <!DOCTYPE html> |
| <html> |
| <head> |
| <title>WebKit Performance Test Results</title> |
| <script src="%AbsolutePathToWebKitTrunk%/PerformanceTests/Dromaeo/resources/dromaeo/web/lib/jquery-1.6.4.js"></script> |
| <script src="https://trac.webkit.org/browser/trunk/PerformanceTests/Dromaeo/resources/dromaeo/web/lib/jquery-1.6.4.js?format=txt"></script> |
| <script src="%AbsolutePathToWebKitTrunk%/PerformanceTests/resources/jquery.flot.min.js"></script> |
| <script src="https://trac.webkit.org/browser/trunk/PerformanceTests/resources/jquery.flot.min.js?format=txt"></script> |
| <script src="%AbsolutePathToWebKitTrunk%/PerformanceTests/resources/jquery.tablesorter.min.js"></script> |
| <script src="https://trac.webkit.org/browser/trunk/PerformanceTests/resources/jquery.tablesorter.min.js?format=txt"></script> |
| <script id="json" type="application/json">%PeformanceTestsResultsJSON%</script> |
| <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; |
| } |
| |
| 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; |
| } |
| |
| .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> |
| <div style="padding: 0 10px;"> |
| Result <span id="time-memory" class="checkbox"><span class="checked">Time</span><span>Memory</span></span> |
| Reference <span id="reference" class="checkbox"></span> |
| </div> |
| <script> |
| |
| $(document).ready(function () { |
| $('.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> |
| <table id="container"></table> |
| <script> |
| |
| function TestResult(associatedTest, result, associatedRun) { |
| this.unit = function () { return result.unit; } |
| this.test = function () { return associatedTest; } |
| this.values = function () { return result.values ? result.values.map(function (value) { return associatedTest.scalingFactor() * value; }) : undefined; } |
| this.unscaledMean = function () { return result.avg; } |
| this.mean = function () { return associatedTest.scalingFactor() * result.avg; } |
| this.min = function () { return associatedTest.scalingFactor() * result.min; } |
| this.max = function () { return associatedTest.scalingFactor() * result.max; } |
| this.stdev = function () { return associatedTest.scalingFactor() * result.stdev; } |
| this.stdevRatio = function () { return result.stdev / result.avg; } |
| this.percentDifference = function(other) { return (other.mean() - this.mean()) / this.mean(); } |
| this.isStatisticallySignificant = function (other) { |
| var diff = Math.abs(other.mean() - this.mean()); |
| return diff > this.stdev() && diff > other.stdev(); |
| } |
| this.run = function () { return associatedRun; } |
| } |
| |
| function TestRun(entry) { |
| this.description = function () { return entry['description']; } |
| this.webkitRevision = function () { return entry['webkit-revision']; } |
| this.label = function () { |
| var label = 'r' + this.webkitRevision(); |
| if (this.description()) |
| label += ' ‐ ' + this.description(); |
| return label; |
| } |
| } |
| |
| function PerfTest(name) { |
| 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 unit = testResults[0].unit(); // FIXME: We should verify that all results have the same unit. |
| 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; } |
| this.isMemoryTest = function () { return name.indexOf(':') >= 0; } |
| 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.smallerIsBetter = function () { return this.unit() == 'ms' || this.unit() == 'bytes'; } |
| } |
| |
| 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 = []; |
| if (values.length) |
| plotData = [$.extend(true, {}, subpointsPlotOptions, {data: values})]; |
| else { |
| function makeSubpoints(id, callback) { return $.extend(true, {}, subpointsPlotOptions, {id: id, data: results.map(callback)}); } |
| plotData = [makeSubpoints('min', function (result, index) { return [index, result.min()]; }), |
| makeSubpoints('max', function (result, index) { return [index, result.max()]; }), |
| makeSubpoints('-σ', function (result, index) { return [index, result.mean() - result.stdev()]; }), |
| makeSubpoints('+σ', function (result, index) { return [index, result.mean() + result.stdev()]; })]; |
| } |
| |
| plotData.push({id: 'μ', data: results.map(function (result, index) { return [index, result.mean()]; }), color: plotColor}); |
| |
| var currentPlotOptions = $.extend(true, {}, mainPlotOptions, {yaxis: { |
| min: minIsZero ? 0 : Math.min.apply(Math, results.map(function (result, index) { return result.min(); })) * 0.98, |
| max: Math.max.apply(Math, results.map(function (result, index) { return result.max(); })) * (minIsZero ? 1.1 : 1.01)}}); |
| |
| 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(test, test.results()[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(test, referenceResult) { |
| var tableRow = $('<tr><td class="test">' + test.name() + '</td><td class="unit">' + test.unit() + '</td></tr>'); |
| |
| tableRow.append(test.results().map(function (result, index) { |
| var secondCell = ''; |
| var hiddenValue = ''; |
| if (result !== referenceResult) { |
| var percentDifference = referenceResult.percentDifference(result); |
| var better = test.smallerIsBetter() ? 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>'; |
| secondCell = '</td><td class="' + className + '">' + comparison; |
| } |
| |
| 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.stdev()) + ', 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="stdev" title="' + statistics + '">± ' |
| + formatPercentage(result.stdevRatio()) + warning + secondCell + '</td>'; |
| }).reduce(function (markup, cell) { return markup + cell; }, '')); |
| |
| $('#container').children('tbody').last().append(tableRow); |
| |
| tableRow.click(function (event) { |
| if (event.target != tableRow[0] && event.target.parentNode != tableRow[0]) |
| return; |
| |
| event.preventDefault(); |
| |
| 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; |
| }); |
| } |
| |
| 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 tests = {}; |
| $.each(JSON.parse(document.getElementById('json').textContent), function (index, entry) { |
| var run = new TestRun(entry); |
| runs.push(run); |
| $.each(entry.results, function (test, result) { |
| if (!tests[test]) |
| tests[test] = new PerfTest(test); |
| tests[test].addResult(new TestResult(tests[test], result, run)); |
| }); |
| }); |
| |
| var shouldIgnoreMemory= true; |
| var referenceIndex = 0; |
| createTable(tests, runs, shouldIgnoreMemory, referenceIndex); |
| |
| $('#time-memory').bind('change', function (event, checkedElement) { |
| shouldIgnoreMemory = checkedElement.textContent == 'Time'; |
| createTable(tests, 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(tests, runs, shouldIgnoreMemory, referenceIndex); |
| }); |
| } |
| |
| init(); |
| |
| </script> |
| </body> |
| </html> |