Perf test results is incomprehensive
[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 section h1 {
21     text-align: center;
22     font-size: 1em;
23 }
24
25 section .tooltip {
26     position: absolute;
27     text-align: center;
28     background: #ffcc66;
29     border-radius: 5px;
30     padding: 0px 5px;
31 }
32
33 body {
34     padding: 0px;
35     margin: 0px;
36     font-family: sans-serif;
37 }
38
39 table {
40     background: white;
41     width: 100%;
42 }
43
44 table, td, th {
45     border-collapse: collapse;
46     padding: 5px;
47 }
48
49 tr.even {
50     background: #f6f6f6;
51 }
52
53 table td {
54     position: relative;
55     font-family: monospace;
56 }
57
58 th, td {
59     cursor: pointer;
60     cursor: hand;
61 }
62
63 th {
64     background: #e6eeee;
65     background: -webkit-gradient(linear, left top, left bottom, from(rgb(244, 244, 244)), to(rgb(217, 217, 217)));
66     border: 1px solid #ccc;
67 }
68
69 th:after {
70     content: ' \25B8';
71 }
72
73 th.headerSortUp:after {
74     content: ' \25BE';
75 }
76
77 th.headerSortDown:after {
78     content: ' \25B4';
79 }
80
81 td.comparison, td.result {
82     text-align: right;
83 }
84
85 td.better {
86     color: #6c6;
87 }
88
89 td.worse {
90     color: #c66;
91 }
92
93 .checkbox {
94     display: inline-block;
95     background: #eee;
96     background: -webkit-gradient(linear, left bottom, left top, from(rgb(220, 220, 220)), to(rgb(200, 200, 200)));
97     border: inset 1px #ddd;
98     border-radius: 5px;
99     margin: 10px;
100     font-size: small;
101     cursor: pointer;
102     cursor: hand;
103     -webkit-user-select: none;
104     font-weight: bold;
105 }
106
107 .checkbox span {
108     display: inline-block;
109     line-height: 100%;
110     padding: 5px 8px;
111     border: outset 1px transparent;
112 }
113
114 .checkbox .checked {
115     background: #e6eeee;
116     background: -webkit-gradient(linear, left top, left bottom, from(rgb(255, 255, 255)), to(rgb(235, 235, 235)));
117     border: outset 1px #eee;
118     border-radius: 5px;
119 }
120
121 </style>
122 </head>
123 <body>
124 <div style="padding: 0 10px;">
125 Result <span id="time-memory" class="checkbox"><span class="checked">Time</span><span>Memory</span></span>
126 Reference <span id="reference" class="checkbox"></span>
127 </div>
128 <script>
129
130 $(document).ready(function () {
131     $('.checkbox').each(function (index, checkbox) {
132         $(checkbox).children('span').click(function (event) {
133             if ($(this).hasClass('checked'))
134                 return;
135             $(checkbox).children('span').removeClass('checked');
136             $(this).addClass('checked');
137             $(checkbox).trigger('change', $(this));
138         });
139     });
140 })
141
142 </script>
143 <table id="container"></table>
144 <script>
145
146 function TestResult(associatedTest, result, associatedRun) {
147     this.unit = function () { return result.unit; }
148     this.test = function () { return associatedTest; }
149     this.unscaledMean = function () { return result.avg; }
150     this.mean = function () { return associatedTest.scalingFactor() * result.avg; }
151     this.min = function () { return associatedTest.scalingFactor() * result.min; }
152     this.max = function () { return associatedTest.scalingFactor() * result.max; }
153     this.stdev = function () { return associatedTest.scalingFactor() * result.stdev; }
154     this.stdevRatio = function () { return result.stdev / result.avg; }
155     this.percentDifference = function(other) { return (other.mean() - this.mean()) / this.mean(); }
156     this.isStatisticallySignificant = function (other) {
157         var diff = Math.abs(other.mean() - this.mean());
158         return diff > this.stdev() && diff > other.stdev();
159     }
160     this.run = function () { return associatedRun; }
161 }
162
163 function TestRun(entry) {
164     this.description = function () { return entry['description']; }
165     this.webkitRevision = function () { return entry['webkit-revision']; }
166     this.label = function () {
167         var label = 'r' + this.webkitRevision();
168         if (this.description())
169             label += ' &dash; ' + this.description();
170         return label;
171     }
172 }
173
174 function PerfTest(name) {
175     var testResults = [];
176     var cachedUnit = null;
177     var cachedScalingFactor = null;
178
179     // We can't do this in TestResult because all results for each test need to share the same unit and the same scaling factor.
180     function computeScalingFactorIfNeeded() {
181         // FIXME: We shouldn't be adjusting units on every test result.
182         // We can only do this on the first test.
183         if (!testResults.length || cachedUnit)
184             return;
185
186         var unit = testResults[0].unit(); // FIXME: We should verify that all results have the same unit.
187         var mean = testResults[0].unscaledMean(); // FIXME: We should look at all values.
188         var kilo = unit == 'bytes' ? 1024 : 1000;
189         if (mean > 10 * kilo * kilo && unit != 'ms') {
190             cachedScalingFactor = 1 / kilo / kilo;
191             cachedUnit = 'M ' + unit;
192         } else if (mean > 10 * kilo) {
193             cachedScalingFactor = 1 / kilo;
194             cachedUnit = unit == 'ms' ? 's' : ('K ' + unit);
195         } else {
196             cachedScalingFactor = 1;
197             cachedUnit = unit;
198         }
199     }
200
201     this.name = function () { return name; }
202     this.isMemoryTest = function () { return name.indexOf(':') >= 0; }
203     this.addResult = function (newResult) {
204         testResults.push(newResult);
205         cachedUnit = null;
206         cachedScalingFactor = null;
207     }
208     this.results = function () { return testResults; }
209     this.scalingFactor = function() {
210         computeScalingFactorIfNeeded();
211         return cachedScalingFactor;
212     }
213     this.unit = function () {
214         computeScalingFactorIfNeeded();
215         return cachedUnit;
216     }
217     this.smallerIsBetter = function () { return this.unit() == 'ms' || this.unit() == 'bytes'; }
218 }
219
220 var plotColor = 'rgb(230,50,50)';
221 var subpointsPlotOptions = {
222     lines: {show:true, lineWidth: 0},
223     color: plotColor,
224     points: {show: true, radius: 1},
225     bars: {show: false}};
226
227 var mainPlotOptions = {
228     xaxis: {
229         min: -0.5,
230         tickSize: 1,
231     },
232     crosshair: { mode: 'y' },
233     series: { shadowSize: 0 },
234     bars: {show: true, align: 'center', barWidth: 0.5},
235     lines: { show: false },
236     points: { show: true },
237     grid: {
238         borderWidth: 2,
239         backgroundColor: '#fff',
240         hoverable: true,
241         autoHighlight: false,
242     }
243 };
244
245 function createPlot(container, test) {
246     var section = $('<section><div class="plot"></div>'
247         + '<span class="tooltip"></span></section>');
248     section.children('.plot').css({'width': 100 * test.results().length + 'px', 'height': '300px'});
249     $(container).append(section);
250
251     var plotContainer = section.children('.plot');
252     var minIsZero = true;
253     attachPlot(test, plotContainer, minIsZero);
254
255     var tooltip = section.children('.tooltip');
256     plotContainer.bind('plothover', function (event, position, item) {
257         if (item) {
258             var postfix = item.series.id ? ' (' + item.series.id + ')' : '';
259             tooltip.html(item.datapoint[1].toPrecision(4) + postfix);
260             var sectionOffset = $(section).offset();
261             tooltip.css({left: item.pageX - sectionOffset.left - tooltip.outerWidth() / 2, top: item.pageY - sectionOffset.top + 10});
262             tooltip.fadeIn(200);
263         } else
264             tooltip.hide();
265     });
266     plotContainer.mouseout(function () {
267         tooltip.hide();
268     });
269     plotContainer.click(function (event) {
270         event.preventDefault();
271         minIsZero = !minIsZero;
272         attachPlot(test, plotContainer, minIsZero);
273     });
274
275     return section;
276 }
277
278 function attachPlot(test, plotContainer, minIsZero) {
279     var results = test.results();
280
281     function makeSubpoints(id, callback) { return $.extend(true, {}, subpointsPlotOptions, {id: id, data: results.map(callback)}); }
282     var plotData = [
283         makeSubpoints('min', function (result, index) { return [index, result.min()]; }),
284         makeSubpoints('max', function (result, index) { return [index, result.max()]; }),
285         makeSubpoints('-&#963;', function (result, index) { return [index, result.mean() - result.stdev()]; }),
286         makeSubpoints('+&#963;', function (result, index) { return [index, result.mean() + result.stdev()]; }),
287         {data: results.map(function (result, index) { return [index, result.mean()]; }), color: plotColor}];
288
289     var currentPlotOptions = $.extend(true, {}, mainPlotOptions, {yaxis: {
290         min: minIsZero ? 0 : Math.min.apply(Math, results.map(function (result, index) { return result.min(); })) * 0.98,
291         max: Math.max.apply(Math, results.map(function (result, index) { return result.max(); })) * (minIsZero ? 1.1 : 1.01)}});
292
293     currentPlotOptions.xaxis.max = results.length - 0.5;
294     currentPlotOptions.xaxis.ticks = results.map(function (result, index) { return [index, result.run().label()]; });
295
296     $.plot(plotContainer, plotData, currentPlotOptions);
297 }
298
299 function toFixedWidthPrecision(value) {
300     var decimal = value.toFixed(2);
301     return decimal;
302 }
303
304 function formatPercentage(fraction) {
305     var percentage = fraction * 100;
306     return (fraction * 100).toFixed(2) + '%';
307 }
308
309 function createTable(tests, runs, shouldIgnoreMemory, referenceIndex) {
310     $('#container').html('<thead><tr><th>Test</th><th>Unit</th>' + runs.map(function (run, index) {
311         return '<th colspan="' + (index == referenceIndex ? 2 : 3) + '" class="{sorter: \'comparison\'}">' + run.label() + '</th>';
312     }).reduce(function (markup, cell) { return markup + cell; }, '') + '</tr></head><tbody></tbody>');
313
314     var testNames = [];
315     for (testName in tests)
316         testNames.push(testName);
317
318     testNames.sort().map(function (testName) {
319         var test = tests[testName];
320         if (test.isMemoryTest() != shouldIgnoreMemory)
321             createTableRow(test, test.results()[referenceIndex]);
322     });
323
324     $('#container').tablesorter({widgets: ['zebra']});
325 }
326
327 function createTableRow(test, referenceResult) {
328     var tableRow = $('<tr><td class="test">' + test.name() + '</td><td class="unit">' + test.unit() + '</td></tr>');
329
330     tableRow.append(test.results().map(function (result, index) {
331         var secondCell = '';
332         var hiddenValue = '';
333         if (result !== referenceResult) {
334             var percentDifference = referenceResult.percentDifference(result);
335             var better = test.smallerIsBetter() ? percentDifference < 0 : percentDifference > 0;
336             var comparison = '';
337             var className = 'comparison';
338             if (referenceResult.isStatisticallySignificant(result)) {
339                 comparison = formatPercentage(Math.abs(percentDifference)) + (better ? ' Better' : ' Worse&nbsp;');
340                 className += better ? ' better' : ' worse';
341             }
342             hiddenValue = '<span style="display: none">|' + comparison + '</span>';
343             secondCell = '</td><td class="' + className + '">' + comparison;
344         }
345
346         // Tablesorter doesn't know about the second cell so put the comparison in the invisible element.
347         return '<td class="result">' + toFixedWidthPrecision(result.mean()) + hiddenValue + '</td><td class="stdev">&plusmn; '
348             + formatPercentage(result.stdevRatio()) + secondCell + '</td>';
349     }).reduce(function (markup, cell) { return markup + cell; }, ''));
350
351     $('#container').children('tbody').last().append(tableRow);
352
353     tableRow.click(function (event) {
354         if (event.target != tableRow[0] && event.target.parentNode != tableRow[0])
355             return;
356
357         event.preventDefault();
358
359         var firstCell = tableRow.children('td').first();
360         if (firstCell.children('section').length) {
361             firstCell.children('section').remove();
362             tableRow.children('td').css({'padding-bottom': ''});
363         } else {
364             var plot = createPlot(firstCell, test);
365             plot.css({'position': 'absolute', 'z-index': 2});
366             var offset = tableRow.offset();
367             offset.left += 1;
368             offset.top += tableRow.outerHeight();
369             plot.offset(offset);
370             tableRow.children('td').css({'padding-bottom': plot.outerHeight() + 5});
371         }
372
373         return false;
374     });
375 }
376
377 function init() {
378     $.tablesorter.addParser({
379         id: 'comparison',
380         is: function(s) {
381             return s.indexOf('|') >= 0;
382         },
383         format: function(s) {
384             var parsed = parseFloat(s.substring(s.indexOf('|') + 1));
385             return isNaN(parsed) ? 0 : parsed;
386         },
387         type: 'numeric',
388     });
389
390     var runs = [];
391     var tests = {};
392     $.each(JSON.parse(document.getElementById('json').textContent), function (index, entry) {
393         var run = new TestRun(entry);
394         runs.push(run);
395         $.each(entry.results, function (test, result) {
396             if (!tests[test])
397                 tests[test] = new PerfTest(test);
398             tests[test].addResult(new TestResult(tests[test], result, run));
399         });
400     });
401
402     var shouldIgnoreMemory= true;
403     var referenceIndex = 0;
404     createTable(tests, runs, shouldIgnoreMemory, referenceIndex);
405
406     $('#time-memory').bind('change', function (event, checkedElement) {
407         shouldIgnoreMemory = checkedElement.textContent == 'Time';
408         createTable(tests, runs, shouldIgnoreMemory, referenceIndex);
409     });
410
411     runs.map(function (run, index) {
412         $('#reference').append('<span value="' + index + '"' + (index == referenceIndex ? ' class="checked"' : '') + '>' + run.label() + '</span>');
413     })
414
415     $('#reference').bind('change', function (event, checkedElement) {
416         referenceIndex = parseInt(checkedElement.getAttribute('value'));
417         createTable(tests, runs, shouldIgnoreMemory, referenceIndex);
418     });
419 }
420
421 init();
422
423 </script>
424 </body>
425 </html>