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