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