1 <!DOCTYPE html>
      2 <html>
      3 <head>
      4 <title>Blink Performance Test Results</title>
      5 <style type="text/css">
      6 
      7 section {
      8     background: white;
      9     padding: 10px;
     10     position: relative;
     11 }
     12 
     13 .time-plots {
     14     padding-left: 25px;
     15 }
     16 
     17 .time-plots > div {
     18     display: inline-block;
     19     width: 90px;
     20     height: 40px;
     21     margin-right: 10px;
     22 }
     23 
     24 section h1 {
     25     text-align: center;
     26     font-size: 1em;
     27 }
     28 
     29 section .tooltip {
     30     position: absolute;
     31     text-align: center;
     32     background: #ffcc66;
     33     border-radius: 5px;
     34     padding: 0px 5px;
     35 }
     36 
     37 body {
     38     padding: 0px;
     39     margin: 0px;
     40     font-family: sans-serif;
     41 }
     42 
     43 table {
     44     background: white;
     45     width: 100%;
     46 }
     47 
     48 table, td, th {
     49     border-collapse: collapse;
     50     padding: 5px;
     51 }
     52 
     53 tr.even {
     54     background: #f6f6f6;
     55 }
     56 
     57 table td {
     58     position: relative;
     59     font-family: monospace;
     60 }
     61 
     62 th, td {
     63     cursor: pointer;
     64     cursor: hand;
     65 }
     66 
     67 th {
     68     background: #e6eeee;
     69     background: -webkit-gradient(linear, left top, left bottom, from(rgb(244, 244, 244)), to(rgb(217, 217, 217)));
     70     border: 1px solid #ccc;
     71 }
     72 
     73 th:after {
     74     content: ' \25B8';
     75 }
     76 
     77 th.headerSortUp:after {
     78     content: ' \25BE';
     79 }
     80 
     81 th.headerSortDown:after {
     82     content: ' \25B4';
     83 }
     84 
     85 td.comparison, td.result {
     86     text-align: right;
     87 }
     88 
     89 td.better {
     90     color: #6c6;
     91 }
     92 
     93 td.worse {
     94     color: #c66;
     95 }
     96 
     97 td.missing {
     98     text-align: center;
     99 }
    100 
    101 .checkbox {
    102     display: inline-block;
    103     background: #eee;
    104     background: -webkit-gradient(linear, left bottom, left top, from(rgb(220, 220, 220)), to(rgb(200, 200, 200)));
    105     border: inset 1px #ddd;
    106     border-radius: 5px;
    107     margin: 10px;
    108     font-size: small;
    109     cursor: pointer;
    110     cursor: hand;
    111     -webkit-user-select: none;
    112     font-weight: bold;
    113 }
    114 
    115 .checkbox span {
    116     display: inline-block;
    117     line-height: 100%;
    118     padding: 5px 8px;
    119     border: outset 1px transparent;
    120 }
    121 
    122 .checkbox .checked {
    123     background: #e6eeee;
    124     background: -webkit-gradient(linear, left top, left bottom, from(rgb(255, 255, 255)), to(rgb(235, 235, 235)));
    125     border: outset 1px #eee;
    126     border-radius: 5px;
    127 }
    128 
    129 </style>
    130 </head>
    131 <body>
    132 <div style="padding: 0 10px;">
    133 Result <span id="time-memory" class="checkbox"><span class="checked">Time</span><span>Memory</span></span>
    134 Reference <span id="reference" class="checkbox"></span>
    135 </div>
    136 <table id="container"></table>
    137 <script>
    138 
    139 (function () {
    140     var jQuery = 'PerformanceTests/Dromaeo/resources/dromaeo/web/lib/jquery-1.6.4.js';
    141     var plugins = ['PerformanceTests/resources/jquery.flot.min.js', 'PerformanceTests/resources/jquery.tablesorter.min.js',
    142         'PerformanceTests/resources/statistics.js'];
    143     var localPath = '%AbsolutePathToWebKitTrunk%';
    144     var remotePath = 'https://svn.webkit.org/repository/webkit/trunk';
    145     var numberOfFailures = 0;
    146     var startedLoadingPlugins = false;
    147     var numberOfLoadedPlugins = 0;
    148 
    149     function loadScript(src, loaded, failed) {
    150         var script = document.createElement('script');
    151         script.async = true;
    152         script.src = src;
    153         script.onload = loaded;
    154         if (failed)
    155             script.onerror = failed;
    156         document.body.appendChild(script);
    157     }
    158 
    159     function loadPlugins(trunkPath) {
    160         for (var i = 0; i < plugins.length; i++)
    161             loadScript(trunkPath + '/' + plugins[i], loadedPlugin, createFailedToLoadPlugin(plugins[i]));
    162     }
    163 
    164     function loadedPlugin() {
    165         numberOfLoadedPlugins++;
    166         if (numberOfLoadedPlugins == plugins.length)
    167             setTimeout(init, 0);            
    168     }
    169 
    170     function createFailedToLoadPlugin(plugin) {
    171         return function () { alert("Failed to load " + plugin); }
    172     }
    173 
    174     function createLoadedJQuery(trunkPath) {
    175         return function () { loadPlugins(trunkPath); }
    176     }
    177 
    178     loadScript(localPath + '/' + jQuery,
    179         createLoadedJQuery(localPath),
    180         function () {
    181             loadScript(remotePath + '/' + jQuery,
    182                 createLoadedJQuery(remotePath),
    183                 function () { alert("Failed to load jQuery."); });
    184         });
    185 })();
    186 
    187 function TestResult(metric, values, associatedRun) {
    188     if (values[0] instanceof Array) {
    189         var flattenedValues = [];
    190         for (var i = 0; i < values.length; i++)
    191             flattenedValues = flattenedValues.concat(values[i]);
    192         values = flattenedValues;
    193     }
    194 
    195     this.test = function () { return metric; }
    196     this.values = function () { return values.map(function (value) { return metric.scalingFactor() * value; }); }
    197     this.unscaledMean = function () { return Statistics.sum(values) / values.length; }
    198     this.mean = function () { return metric.scalingFactor() * this.unscaledMean(); }
    199     this.min = function () { return metric.scalingFactor() * Statistics.min(values); }
    200     this.max = function () { return metric.scalingFactor() * Statistics.max(values); }
    201     this.confidenceIntervalDelta = function () {
    202         return metric.scalingFactor() * Statistics.confidenceIntervalDelta(0.95, values.length,
    203             Statistics.sum(values), Statistics.squareSum(values));
    204     }
    205     this.confidenceIntervalDeltaRatio = function () { return this.confidenceIntervalDelta() / this.mean(); }
    206     this.percentDifference = function(other) { return (other.unscaledMean() - this.unscaledMean()) / this.unscaledMean(); }
    207     this.isStatisticallySignificant = function (other) {
    208         var diff = Math.abs(other.mean() - this.mean());
    209         return diff > this.confidenceIntervalDelta() && diff > other.confidenceIntervalDelta();
    210     }
    211     this.run = function () { return associatedRun; }
    212 }
    213 
    214 function TestRun(entry) {
    215     this.description = function () { return entry['description']; }
    216     this.webkitRevision = function () { return entry['revisions']['blink']['revision']; }
    217     this.label = function () {
    218         var label = 'r' + this.webkitRevision();
    219         if (this.description())
    220             label += ' ‐ ' + this.description();
    221         return label;
    222     }
    223 }
    224 
    225 function PerfTestMetric(name, metric) {
    226     var testResults = [];
    227     var cachedUnit = null;
    228     var cachedScalingFactor = null;
    229     var unit = {'FrameRate': 'fps', 'Runs': 'runs/s', 'Time': 'ms', 'Malloc': 'bytes', 'JSHeap': 'bytes'}[metric];
    230 
    231     // We can't do this in TestResult because all results for each test need to share the same unit and the same scaling factor.
    232     function computeScalingFactorIfNeeded() {
    233         // FIXME: We shouldn't be adjusting units on every test result.
    234         // We can only do this on the first test.
    235         if (!testResults.length || cachedUnit)
    236             return;
    237 
    238         var mean = testResults[0].unscaledMean(); // FIXME: We should look at all values.
    239         var kilo = unit == 'bytes' ? 1024 : 1000;
    240         if (mean > 10 * kilo * kilo && unit != 'ms') {
    241             cachedScalingFactor = 1 / kilo / kilo;
    242             cachedUnit = 'M ' + unit;
    243         } else if (mean > 10 * kilo) {
    244             cachedScalingFactor = 1 / kilo;
    245             cachedUnit = unit == 'ms' ? 's' : ('K ' + unit);
    246         } else {
    247             cachedScalingFactor = 1;
    248             cachedUnit = unit;
    249         }
    250     }
    251 
    252     this.name = function () { return name + ':' + metric; }
    253     this.isMemoryTest = function () { return metric == 'JSHeap' || metric == 'Malloc'; }
    254     this.addResult = function (newResult) {
    255         testResults.push(newResult);
    256         cachedUnit = null;
    257         cachedScalingFactor = null;
    258     }
    259     this.results = function () { return testResults; }
    260     this.scalingFactor = function() {
    261         computeScalingFactorIfNeeded();
    262         return cachedScalingFactor;
    263     }
    264     this.unit = function () {
    265         computeScalingFactorIfNeeded();
    266         return cachedUnit;
    267     }
    268     this.smallerIsBetter = function () { return unit == 'ms' || unit == 'bytes'; }
    269 }
    270 
    271 var plotColor = 'rgb(230,50,50)';
    272 var subpointsPlotOptions = {
    273     lines: {show:true, lineWidth: 0},
    274     color: plotColor,
    275     points: {show: true, radius: 1},
    276     bars: {show: false}};
    277 
    278 var mainPlotOptions = {
    279     xaxis: {
    280         min: -0.5,
    281         tickSize: 1,
    282     },
    283     crosshair: { mode: 'y' },
    284     series: { shadowSize: 0 },
    285     bars: {show: true, align: 'center', barWidth: 0.5},
    286     lines: { show: false },
    287     points: { show: true },
    288     grid: {
    289         borderWidth: 1,
    290         borderColor: '#ccc',
    291         backgroundColor: '#fff',
    292         hoverable: true,
    293         autoHighlight: false,
    294     }
    295 };
    296 
    297 var timePlotOptions = {
    298     yaxis: { show: false },
    299     xaxis: { show: false },
    300     lines: { show: true },
    301     grid: { borderWidth: 1, borderColor: '#ccc' },
    302     colors: [ plotColor ]
    303 };
    304 
    305 function createPlot(container, test) {
    306     var section = $('<section><div class="plot"></div><div class="time-plots"></div>'
    307         + '<span class="tooltip"></span></section>');
    308     section.children('.plot').css({'width': (100 * test.results().length + 25) + 'px', 'height': '300px'});
    309     $(container).append(section);
    310 
    311     var plotContainer = section.children('.plot');
    312     var minIsZero = true;
    313     attachPlot(test, plotContainer, minIsZero);
    314 
    315     attachTimePlots(test, section.children('.time-plots'));
    316 
    317     var tooltip = section.children('.tooltip');
    318     plotContainer.bind('plothover', function (event, position, item) {
    319         if (item) {
    320             var postfix = item.series.id ? ' (' + item.series.id + ')' : '';
    321             tooltip.html(item.datapoint[1].toPrecision(4) + postfix);
    322             var sectionOffset = $(section).offset();
    323             tooltip.css({left: item.pageX - sectionOffset.left - tooltip.outerWidth() / 2, top: item.pageY - sectionOffset.top + 10});
    324             tooltip.fadeIn(200);
    325         } else
    326             tooltip.hide();
    327     });
    328     plotContainer.mouseout(function () {
    329         tooltip.hide();
    330     });
    331     plotContainer.click(function (event) {
    332         event.preventDefault();
    333         minIsZero = !minIsZero;
    334         attachPlot(test, plotContainer, minIsZero);
    335     });
    336 
    337     return section;
    338 }
    339 
    340 function attachTimePlots(test, container) {
    341     var results = test.results();
    342     var attachedPlot = false;
    343     for (var i = 0; i < results.length; i++) {
    344         container.append('');
    345         var values = results[i].values();
    346         if (!values)
    347             continue;
    348         attachedPlot = true;
    349 
    350         $.plot(container.children().last(), [values.map(function (value, index) { return [index, value]; })],
    351             $.extend(true, {}, timePlotOptions, {yaxis: {min: Math.min.apply(Math, values) * 0.9, max: Math.max.apply(Math, values) * 1.1},
    352                 xaxis: {min: -0.5, max: values.length - 0.5}}));
    353     }
    354     if (!attachedPlot)
    355         container.children().remove();
    356 }
    357 
    358 function attachPlot(test, plotContainer, minIsZero) {
    359     var results = test.results();
    360 
    361     var values = results.reduce(function (values, result, index) {
    362         var newValues = result.values();
    363         return newValues ? values.concat(newValues.map(function (value) { return [index, value]; })) : values;
    364     }, []);
    365 
    366     var plotData = [$.extend(true, {}, subpointsPlotOptions, {data: values})];
    367     plotData.push({id: 'μ', data: results.map(function (result, index) { return [index, result.mean()]; }), color: plotColor});
    368 
    369     var overallMax = Statistics.max(results.map(function (result, index) { return result.max(); }));
    370     var overallMin = Statistics.min(results.map(function (result, index) { return result.min(); }));
    371     var margin = (overallMax - overallMin) * 0.1;
    372     var currentPlotOptions = $.extend(true, {}, mainPlotOptions, {yaxis: {
    373         min: minIsZero ? 0 : overallMin - margin,
    374         max: minIsZero ? overallMax * 1.1 : overallMax + margin}});
    375 
    376     currentPlotOptions.xaxis.max = results.length - 0.5;
    377     currentPlotOptions.xaxis.ticks = results.map(function (result, index) { return [index, result.run().label()]; });
    378 
    379     $.plot(plotContainer, plotData, currentPlotOptions);
    380 }
    381 
    382 function toFixedWidthPrecision(value) {
    383     var decimal = value.toFixed(2);
    384     return decimal;
    385 }
    386 
    387 function formatPercentage(fraction) {
    388     var percentage = fraction * 100;
    389     return (fraction * 100).toFixed(2) + '%';
    390 }
    391 
    392 function createTable(tests, runs, shouldIgnoreMemory, referenceIndex) {
    393     $('#container').html('| Test | Unit' + runs.map(function (run, index) {
    394         return ' | referenceIndex ? 2 : 3) + '" class="{sorter: \'comparison\'}">' + run.label() + '';
    395     }).reduce(function (markup, cell) { return markup + cell; }, '') + ' | 
|---|
');
    396 
    397     var testNames = [];
    398     for (testName in tests)
    399         testNames.push(testName);
    400 
    401     testNames.sort().map(function (testName) {
    402         var test = tests[testName];
    403         if (test.isMemoryTest() != shouldIgnoreMemory)
    404             createTableRow(runs, test, referenceIndex);
    405     });
    406 
    407     $('#container').tablesorter({widgets: ['zebra']});
    408 }
    409 
    410 function linearRegression(points) {
    411     // Implement http://www.easycalculation.com/statistics/learn-correlation.php.
    412     // x = magnitude
    413     // y = iterations
    414     var sumX = 0;
    415     var sumY = 0;
    416     var sumXSquared = 0;
    417     var sumYSquared = 0;
    418     var sumXTimesY = 0;
    419 
    420     for (var i = 0; i < points.length; i++) {
    421         var x = i;
    422         var y = points[i];
    423         sumX += x;
    424         sumY += y;
    425         sumXSquared += x * x;
    426         sumYSquared += y * y;
    427         sumXTimesY += x * y;
    428     }
    429 
    430     var r = (points.length * sumXTimesY - sumX * sumY) /
    431         Math.sqrt((points.length * sumXSquared - sumX * sumX) *
    432                   (points.length * sumYSquared - sumY * sumY));
    433 
    434     if (isNaN(r) || r == Math.Infinity)
    435         r = 0;
    436 
    437     var slope = (points.length * sumXTimesY - sumX * sumY) / (points.length * sumXSquared - sumX * sumX);
    438     var intercept = sumY / points.length - slope * sumX / points.length;
    439     return {slope: slope, intercept: intercept, rSquared: r * r};
    440 }
    441 
    442 var warningSign = '<svg viewBox="0 0 100 100" style="width: 18px; height: 18px; vertical-align: bottom;" version="1.1">'
    443     + '<polygon fill="red" points="50,10 90,80 10,80 50,10" stroke="red" stroke-width="10" stroke-linejoin="round" />'
    444     + '<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" />'
    445     + '<circle cx="50" cy="73" r="6" fill="white" />'
    446     + '</svg>';
    447 
    448 function createTableRow(runs, test, referenceIndex) {
    449     var tableRow = $('<tr><td class="test">' + test.name() + '</td><td class="unit">' + test.unit() + '</td></tr>');
    450 
    451     function markupForRun(result, referenceResult) {
    452         var comparisonCell = '';
    453         var hiddenValue = '';
    454         var shouldCompare = result !== referenceResult;
    455         if (shouldCompare && referenceResult) {
    456             var percentDifference = referenceResult.percentDifference(result);
    457             var better = test.smallerIsBetter() ? percentDifference < 0 : percentDifference > 0;
    458             var comparison = '';
    459             var className = 'comparison';
    460             if (referenceResult.isStatisticallySignificant(result)) {
    461                 comparison = formatPercentage(Math.abs(percentDifference)) + (better ? ' Better' : ' Worse ');
    462                 className += better ? ' better' : ' worse';
    463             }
    464             hiddenValue = '<span style="display: none">|' + comparison + '</span>';
    465             comparisonCell = '<td class="' + className + '">' + comparison + '</td>';
    466         } else if (shouldCompare)
    467             comparisonCell = '<td class="comparison"></td>';
    468 
    469         var values = result.values();
    470         var warning = '';
    471         var regressionAnalysis = '';
    472         if (values && values.length > 3) {
    473             regressionResult = linearRegression(values);
    474             regressionAnalysis = 'slope=' + toFixedWidthPrecision(regressionResult.slope)
    475                 + ', R^2=' + toFixedWidthPrecision(regressionResult.rSquared);
    476             if (regressionResult.rSquared > 0.6 && Math.abs(regressionResult.slope) > 0.01) {
    477                 warning = ' <span class="regression-warning" title="Detected a time dependency with ' + regressionAnalysis + '">' + warningSign + ' </span>';
    478             }
    479         }
    480 
    481         var statistics = 'σ=' + toFixedWidthPrecision(result.confidenceIntervalDelta()) + ', min=' + toFixedWidthPrecision(result.min())
    482             + ', max=' + toFixedWidthPrecision(result.max()) + '\n' + regressionAnalysis;
    483 
    484         // Tablesorter doesn't know about the second cell so put the comparison in the invisible element.
    485         return '<td class="result" title="' + statistics + '">' + toFixedWidthPrecision(result.mean()) + hiddenValue
    486             + '</td><td class="confidenceIntervalDelta" title="' + statistics + '">± '
    487             + formatPercentage(result.confidenceIntervalDeltaRatio()) + warning + '</td>' + comparisonCell;
    488     }
    489 
    490     function markupForMissingRun(isReference) {
    491         return '<td colspan="' + (isReference ? 2 : 3) + '" class="missing">Missing</td>';
    492     }
    493 
    494     var runIndex = 0;
    495     var results = test.results();
    496     var referenceResult = undefined;
    497     var resultIndexMap = {};
    498     for (var i = 0; i < results.length; i++) {
    499         while (runs[runIndex] !== results[i].run())
    500             runIndex++;
    501         if (runIndex == referenceIndex)
    502             referenceResult = results[i];
    503         resultIndexMap[runIndex] = i;
    504     }
    505     for (var i = 0; i < runs.length; i++) {
    506         var resultIndex = resultIndexMap[i];
    507         if (resultIndex == undefined)
    508             tableRow.append(markupForMissingRun(i == referenceIndex));
    509         else
    510             tableRow.append(markupForRun(results[resultIndex], referenceResult));
    511     }
    512 
    513     $('#container').children('tbody').last().append(tableRow);
    514 
    515     tableRow.click(function (event) {
    516         if (event.target != tableRow[0] && event.target.parentNode != tableRow[0])
    517             return;
    518 
    519         event.preventDefault();
    520 
    521         var firstCell = tableRow.children('td').first();
    522         if (firstCell.children('section').length) {
    523             firstCell.children('section').remove();
    524             tableRow.children('td').css({'padding-bottom': ''});
    525         } else {
    526             var plot = createPlot(firstCell, test);
    527             plot.css({'position': 'absolute', 'z-index': 2});
    528             var offset = tableRow.offset();
    529             offset.left += 1;
    530             offset.top += tableRow.outerHeight();
    531             plot.offset(offset);
    532             tableRow.children('td').css({'padding-bottom': plot.outerHeight() + 5});
    533         }
    534 
    535         return false;
    536     });
    537 }
    538 
    539 function init() {
    540     $.tablesorter.addParser({
    541         id: 'comparison',
    542         is: function(s) {
    543             return s.indexOf('|') >= 0;
    544         },
    545         format: function(s) {
    546             var parsed = parseFloat(s.substring(s.indexOf('|') + 1));
    547             return isNaN(parsed) ? 0 : parsed;
    548         },
    549         type: 'numeric',
    550     });
    551 
    552     var runs = [];
    553     var metrics = {};
    554     $.each(JSON.parse(document.getElementById('json').textContent), function (index, entry) {
    555         var run = new TestRun(entry);
    556         runs.push(run);
    557 
    558         function addTests(tests, parentFullName) {
    559             for (var testName in tests) {
    560                 var fullTestName = parentFullName + '/' + testName;
    561                 var rawMetrics = tests[testName].metrics;
    562 
    563                 for (var metricName in rawMetrics) {
    564                     var fullMetricName = fullTestName + ':' + metricName;
    565                     var metric = metrics[fullMetricName];
    566                     if (!metric) {
    567                         metric = new PerfTestMetric(fullTestName, metricName);
    568                         metrics[fullMetricName] = metric;
    569                     }
    570                     metric.addResult(new TestResult(metric, rawMetrics[metricName].current, run));
    571                 }
    572 
    573                 if (tests[testName].tests)
    574                     addTests(tests[testName].tests, fullTestName);
    575             }
    576         }
    577 
    578         addTests(entry.tests, '');
    579     });
    580 
    581     var shouldIgnoreMemory= true;
    582     var referenceIndex = 0;
    583 
    584     createTable(metrics, runs, shouldIgnoreMemory, referenceIndex);
    585 
    586     $('#time-memory').bind('change', function (event, checkedElement) {
    587         shouldIgnoreMemory = checkedElement.textContent == 'Time';
    588         createTable(metrics, runs, shouldIgnoreMemory, referenceIndex);
    589     });
    590 
    591     runs.map(function (run, index) {
    592         $('#reference').append('<span value="' + index + '"' + (index == referenceIndex ? ' class="checked"' : '') + '>' + run.label() + '</span>');
    593     })
    594 
    595     $('#reference').bind('change', function (event, checkedElement) {
    596         referenceIndex = parseInt(checkedElement.getAttribute('value'));
    597         createTable(metrics, runs, shouldIgnoreMemory, referenceIndex);
    598     });
    599 
    600     $('.checkbox').each(function (index, checkbox) {
    601         $(checkbox).children('span').click(function (event) {
    602             if ($(this).hasClass('checked'))
    603                 return;
    604             $(checkbox).children('span').removeClass('checked');
    605             $(this).addClass('checked');
    606             $(checkbox).trigger('change', $(this));
    607         });
    608     });
    609 }
    610 
    611 </script>
    612 <script id="json" type="application/json">%PeformanceTestsResultsJSON%</script>
    613 </body>
    614 </html>
    615