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