Results page should show indivisual value
[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.values = function () { return result.values ? result.values.map(function (value) { return associatedTest.scalingFactor() * value; }) : undefined; }
150     this.unscaledMean = function () { return result.avg; }
151     this.mean = function () { return associatedTest.scalingFactor() * result.avg; }
152     this.min = function () { return associatedTest.scalingFactor() * result.min; }
153     this.max = function () { return associatedTest.scalingFactor() * result.max; }
154     this.stdev = function () { return associatedTest.scalingFactor() * result.stdev; }
155     this.stdevRatio = function () { return result.stdev / result.avg; }
156     this.percentDifference = function(other) { return (other.mean() - this.mean()) / this.mean(); }
157     this.isStatisticallySignificant = function (other) {
158         var diff = Math.abs(other.mean() - this.mean());
159         return diff > this.stdev() && diff > other.stdev();
160     }
161     this.run = function () { return associatedRun; }
162 }
163
164 function TestRun(entry) {
165     this.description = function () { return entry['description']; }
166     this.webkitRevision = function () { return entry['webkit-revision']; }
167     this.label = function () {
168         var label = 'r' + this.webkitRevision();
169         if (this.description())
170             label += ' &dash; ' + this.description();
171         return label;
172     }
173 }
174
175 function PerfTest(name) {
176     var testResults = [];
177     var cachedUnit = null;
178     var cachedScalingFactor = null;
179
180     // We can't do this in TestResult because all results for each test need to share the same unit and the same scaling factor.
181     function computeScalingFactorIfNeeded() {
182         // FIXME: We shouldn't be adjusting units on every test result.
183         // We can only do this on the first test.
184         if (!testResults.length || cachedUnit)
185             return;
186
187         var unit = testResults[0].unit(); // FIXME: We should verify that all results have the same unit.
188         var mean = testResults[0].unscaledMean(); // FIXME: We should look at all values.
189         var kilo = unit == 'bytes' ? 1024 : 1000;
190         if (mean > 10 * kilo * kilo && unit != 'ms') {
191             cachedScalingFactor = 1 / kilo / kilo;
192             cachedUnit = 'M ' + unit;
193         } else if (mean > 10 * kilo) {
194             cachedScalingFactor = 1 / kilo;
195             cachedUnit = unit == 'ms' ? 's' : ('K ' + unit);
196         } else {
197             cachedScalingFactor = 1;
198             cachedUnit = unit;
199         }
200     }
201
202     this.name = function () { return name; }
203     this.isMemoryTest = function () { return name.indexOf(':') >= 0; }
204     this.addResult = function (newResult) {
205         testResults.push(newResult);
206         cachedUnit = null;
207         cachedScalingFactor = null;
208     }
209     this.results = function () { return testResults; }
210     this.scalingFactor = function() {
211         computeScalingFactorIfNeeded();
212         return cachedScalingFactor;
213     }
214     this.unit = function () {
215         computeScalingFactorIfNeeded();
216         return cachedUnit;
217     }
218     this.smallerIsBetter = function () { return this.unit() == 'ms' || this.unit() == 'bytes'; }
219 }
220
221 var plotColor = 'rgb(230,50,50)';
222 var subpointsPlotOptions = {
223     lines: {show:true, lineWidth: 0},
224     color: plotColor,
225     points: {show: true, radius: 1},
226     bars: {show: false}};
227
228 var mainPlotOptions = {
229     xaxis: {
230         min: -0.5,
231         tickSize: 1,
232     },
233     crosshair: { mode: 'y' },
234     series: { shadowSize: 0 },
235     bars: {show: true, align: 'center', barWidth: 0.5},
236     lines: { show: false },
237     points: { show: true },
238     grid: {
239         borderWidth: 2,
240         backgroundColor: '#fff',
241         hoverable: true,
242         autoHighlight: false,
243     }
244 };
245
246 function createPlot(container, test) {
247     var section = $('<section><div class="plot"></div>'
248         + '<span class="tooltip"></span></section>');
249     section.children('.plot').css({'width': 100 * test.results().length + 'px', 'height': '300px'});
250     $(container).append(section);
251
252     var plotContainer = section.children('.plot');
253     var minIsZero = true;
254     attachPlot(test, plotContainer, minIsZero);
255
256     var tooltip = section.children('.tooltip');
257     plotContainer.bind('plothover', function (event, position, item) {
258         if (item) {
259             var postfix = item.series.id ? ' (' + item.series.id + ')' : '';
260             tooltip.html(item.datapoint[1].toPrecision(4) + postfix);
261             var sectionOffset = $(section).offset();
262             tooltip.css({left: item.pageX - sectionOffset.left - tooltip.outerWidth() / 2, top: item.pageY - sectionOffset.top + 10});
263             tooltip.fadeIn(200);
264         } else
265             tooltip.hide();
266     });
267     plotContainer.mouseout(function () {
268         tooltip.hide();
269     });
270     plotContainer.click(function (event) {
271         event.preventDefault();
272         minIsZero = !minIsZero;
273         attachPlot(test, plotContainer, minIsZero);
274     });
275
276     return section;
277 }
278
279 function attachPlot(test, plotContainer, minIsZero) {
280     var results = test.results();
281
282     var values = results.reduce(function (values, result, index) {
283         var newValues = result.values();
284         return newValues ? values.concat(newValues.map(function (value) { return [index, value]; })) : values;
285     }, []);
286
287     var plotData = [];
288     if (values.length)
289         plotData = [$.extend(true, {}, subpointsPlotOptions, {data: values})];
290     else {
291         function makeSubpoints(id, callback) { return $.extend(true, {}, subpointsPlotOptions, {id: id, data: results.map(callback)}); }
292         plotData = [makeSubpoints('min', function (result, index) { return [index, result.min()]; }),
293         makeSubpoints('max', function (result, index) { return [index, result.max()]; }),
294         makeSubpoints('-&#963;', function (result, index) { return [index, result.mean() - result.stdev()]; }),
295         makeSubpoints('+&#963;', function (result, index) { return [index, result.mean() + result.stdev()]; })];
296     }
297
298     plotData.push({id: '&mu;', data: results.map(function (result, index) { return [index, result.mean()]; }), color: plotColor});
299
300     var currentPlotOptions = $.extend(true, {}, mainPlotOptions, {yaxis: {
301         min: minIsZero ? 0 : Math.min.apply(Math, results.map(function (result, index) { return result.min(); })) * 0.98,
302         max: Math.max.apply(Math, results.map(function (result, index) { return result.max(); })) * (minIsZero ? 1.1 : 1.01)}});
303
304     currentPlotOptions.xaxis.max = results.length - 0.5;
305     currentPlotOptions.xaxis.ticks = results.map(function (result, index) { return [index, result.run().label()]; });
306
307     $.plot(plotContainer, plotData, currentPlotOptions);
308 }
309
310 function toFixedWidthPrecision(value) {
311     var decimal = value.toFixed(2);
312     return decimal;
313 }
314
315 function formatPercentage(fraction) {
316     var percentage = fraction * 100;
317     return (fraction * 100).toFixed(2) + '%';
318 }
319
320 function createTable(tests, runs, shouldIgnoreMemory, referenceIndex) {
321     $('#container').html('<thead><tr><th>Test</th><th>Unit</th>' + runs.map(function (run, index) {
322         return '<th colspan="' + (index == referenceIndex ? 2 : 3) + '" class="{sorter: \'comparison\'}">' + run.label() + '</th>';
323     }).reduce(function (markup, cell) { return markup + cell; }, '') + '</tr></head><tbody></tbody>');
324
325     var testNames = [];
326     for (testName in tests)
327         testNames.push(testName);
328
329     testNames.sort().map(function (testName) {
330         var test = tests[testName];
331         if (test.isMemoryTest() != shouldIgnoreMemory)
332             createTableRow(test, test.results()[referenceIndex]);
333     });
334
335     $('#container').tablesorter({widgets: ['zebra']});
336 }
337
338 function createTableRow(test, referenceResult) {
339     var tableRow = $('<tr><td class="test">' + test.name() + '</td><td class="unit">' + test.unit() + '</td></tr>');
340
341     tableRow.append(test.results().map(function (result, index) {
342         var secondCell = '';
343         var hiddenValue = '';
344         if (result !== referenceResult) {
345             var percentDifference = referenceResult.percentDifference(result);
346             var better = test.smallerIsBetter() ? percentDifference < 0 : percentDifference > 0;
347             var comparison = '';
348             var className = 'comparison';
349             if (referenceResult.isStatisticallySignificant(result)) {
350                 comparison = formatPercentage(Math.abs(percentDifference)) + (better ? ' Better' : ' Worse&nbsp;');
351                 className += better ? ' better' : ' worse';
352             }
353             hiddenValue = '<span style="display: none">|' + comparison + '</span>';
354             secondCell = '</td><td class="' + className + '">' + comparison;
355         }
356
357         var statistics = '&sigma;=' + toFixedWidthPrecision(result.stdev()) + ', min=' + toFixedWidthPrecision(result.min())
358             + ', max=' + toFixedWidthPrecision(result.max());
359
360         // Tablesorter doesn't know about the second cell so put the comparison in the invisible element.
361         return '<td class="result" title="' + statistics + '">' + toFixedWidthPrecision(result.mean()) + hiddenValue
362             + '</td><td class="stdev" title="' + statistics + '">&plusmn; '
363             + formatPercentage(result.stdevRatio()) + secondCell + '</td>';
364     }).reduce(function (markup, cell) { return markup + cell; }, ''));
365
366     $('#container').children('tbody').last().append(tableRow);
367
368     tableRow.click(function (event) {
369         if (event.target != tableRow[0] && event.target.parentNode != tableRow[0])
370             return;
371
372         event.preventDefault();
373
374         var firstCell = tableRow.children('td').first();
375         if (firstCell.children('section').length) {
376             firstCell.children('section').remove();
377             tableRow.children('td').css({'padding-bottom': ''});
378         } else {
379             var plot = createPlot(firstCell, test);
380             plot.css({'position': 'absolute', 'z-index': 2});
381             var offset = tableRow.offset();
382             offset.left += 1;
383             offset.top += tableRow.outerHeight();
384             plot.offset(offset);
385             tableRow.children('td').css({'padding-bottom': plot.outerHeight() + 5});
386         }
387
388         return false;
389     });
390 }
391
392 function init() {
393     $.tablesorter.addParser({
394         id: 'comparison',
395         is: function(s) {
396             return s.indexOf('|') >= 0;
397         },
398         format: function(s) {
399             var parsed = parseFloat(s.substring(s.indexOf('|') + 1));
400             return isNaN(parsed) ? 0 : parsed;
401         },
402         type: 'numeric',
403     });
404
405     var runs = [];
406     var tests = {};
407     $.each(JSON.parse(document.getElementById('json').textContent), function (index, entry) {
408         var run = new TestRun(entry);
409         runs.push(run);
410         $.each(entry.results, function (test, result) {
411             if (!tests[test])
412                 tests[test] = new PerfTest(test);
413             tests[test].addResult(new TestResult(tests[test], result, run));
414         });
415     });
416
417     var shouldIgnoreMemory= true;
418     var referenceIndex = 0;
419     createTable(tests, runs, shouldIgnoreMemory, referenceIndex);
420
421     $('#time-memory').bind('change', function (event, checkedElement) {
422         shouldIgnoreMemory = checkedElement.textContent == 'Time';
423         createTable(tests, runs, shouldIgnoreMemory, referenceIndex);
424     });
425
426     runs.map(function (run, index) {
427         $('#reference').append('<span value="' + index + '"' + (index == referenceIndex ? ' class="checked"' : '') + '>' + run.label() + '</span>');
428     })
429
430     $('#reference').bind('change', function (event, checkedElement) {
431         referenceIndex = parseInt(checkedElement.getAttribute('value'));
432         createTable(tests, runs, shouldIgnoreMemory, referenceIndex);
433     });
434 }
435
436 init();
437
438 </script>
439 </body>
440 </html>