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