4 <title>Blink Performance Test Results
</title>
5 <style type=
"text/css">
18 display: inline-block
;
40 font-family: sans-serif
;
49 border-collapse: collapse
;
59 font-family: monospace
;
69 background: -webkit-gradient
(linear
, left top
, left bottom
, from
(rgb
(244, 244, 244)), to
(rgb
(217, 217, 217)));
70 border: 1px solid
#ccc;
77 th
.headerSortUp:after
{
81 th
.headerSortDown:after
{
85 td
.comparison
, td
.result
{
102 display: inline-block
;
104 background: -webkit-gradient
(linear
, left bottom
, left top
, from
(rgb
(220, 220, 220)), to
(rgb
(200, 200, 200)));
105 border: inset
1px #ddd;
111 -webkit-user-select: none
;
116 display: inline-block
;
119 border: outset
1px transparent
;
124 background: -webkit-gradient
(linear
, left top
, left bottom
, from
(rgb
(255, 255, 255)), to
(rgb
(235, 235, 235)));
125 border: outset
1px #eee;
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>
136 <table id=
"container"></table>
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;
149 function loadScript(src
, loaded
, failed
) {
150 var script
= document
.createElement('script');
153 script
.onload
= loaded
;
155 script
.onerror
= failed
;
156 document
.body
.appendChild(script
);
159 function loadPlugins(trunkPath
) {
160 for (var i
= 0; i
< plugins
.length
; i
++)
161 loadScript(trunkPath
+ '/' + plugins
[i
], loadedPlugin
, createFailedToLoadPlugin(plugins
[i
]));
164 function loadedPlugin() {
165 numberOfLoadedPlugins
++;
166 if (numberOfLoadedPlugins
== plugins
.length
)
170 function createFailedToLoadPlugin(plugin
) {
171 return function () { alert("Failed to load " + plugin
); }
174 function createLoadedJQuery(trunkPath
) {
175 return function () { loadPlugins(trunkPath
); }
178 loadScript(localPath
+ '/' + jQuery
,
179 createLoadedJQuery(localPath
),
181 loadScript(remotePath
+ '/' + jQuery
,
182 createLoadedJQuery(remotePath
),
183 function () { alert("Failed to load jQuery."); });
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
;
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
));
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();
211 this.run = function () { return associatedRun
; }
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();
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
];
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
)
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
);
247 cachedScalingFactor
= 1;
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
);
257 cachedScalingFactor
= null;
259 this.results = function () { return testResults
; }
260 this.scalingFactor = function() {
261 computeScalingFactorIfNeeded();
262 return cachedScalingFactor
;
264 this.unit = function () {
265 computeScalingFactorIfNeeded();
268 this.smallerIsBetter = function () { return unit
== 'ms' || unit
== 'bytes'; }
271 var plotColor
= 'rgb(230,50,50)';
272 var subpointsPlotOptions
= {
273 lines
: {show
:true, lineWidth
: 0},
275 points
: {show
: true, radius
: 1},
276 bars
: {show
: false}};
278 var mainPlotOptions
= {
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 },
291 backgroundColor
: '#fff',
293 autoHighlight
: false,
297 var timePlotOptions
= {
298 yaxis
: { show
: false },
299 xaxis
: { show
: false },
300 lines
: { show
: true },
301 grid
: { borderWidth
: 1, borderColor
: '#ccc' },
302 colors
: [ plotColor
]
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
);
311 var plotContainer
= section
.children('.plot');
312 var minIsZero
= true;
313 attachPlot(test
, plotContainer
, minIsZero
);
315 attachTimePlots(test
, section
.children('.time-plots'));
317 var tooltip
= section
.children('.tooltip');
318 plotContainer
.bind('plothover', function (event
, position
, 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});
328 plotContainer
.mouseout(function () {
331 plotContainer
.click(function (event
) {
332 event
.preventDefault();
333 minIsZero
= !minIsZero
;
334 attachPlot(test
, plotContainer
, minIsZero
);
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('<div></div>');
345 var values
= results
[i
].values();
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}}));
355 container
.children().remove();
358 function attachPlot(test
, plotContainer
, minIsZero
) {
359 var results
= test
.results();
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
;
366 var plotData
= [$.extend(true, {}, subpointsPlotOptions
, {data
: values
})];
367 plotData
.push({id
: 'μ', data
: results
.map(function (result
, index
) { return [index
, result
.mean()]; }), color
: plotColor
});
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
}});
376 currentPlotOptions
.xaxis
.max
= results
.length
- 0.5;
377 currentPlotOptions
.xaxis
.ticks
= results
.map(function (result
, index
) { return [index
, result
.run().label()]; });
379 $.plot(plotContainer
, plotData
, currentPlotOptions
);
382 function toFixedWidthPrecision(value
) {
383 var decimal = value
.toFixed(2);
387 function formatPercentage(fraction
) {
388 var percentage
= fraction
* 100;
389 return (fraction
* 100).toFixed(2) + '%';
392 function createTable(tests
, runs
, shouldIgnoreMemory
, referenceIndex
) {
393 $('#container').html('<thead><tr><th>Test</th><th>Unit</th>' + runs
.map(function (run
, index
) {
394 return '<th colspan="' + (index
== referenceIndex
? 2 : 3) + '" class="{sorter: \'comparison\'}">' + run
.label() + '</th>';
395 }).reduce(function (markup
, cell
) { return markup
+ cell
; }, '') + '</tr></head><tbody></tbody>');
398 for (testName
in tests
)
399 testNames
.push(testName
);
401 testNames
.sort().map(function (testName
) {
402 var test
= tests
[testName
];
403 if (test
.isMemoryTest() != shouldIgnoreMemory
)
404 createTableRow(runs
, test
, referenceIndex
);
407 $('#container').tablesorter({widgets
: ['zebra']});
410 function linearRegression(points
) {
411 // Implement http://www.easycalculation.com/statistics/learn-correlation.php.
420 for (var i
= 0; i
< points
.length
; i
++) {
425 sumXSquared
+= x
* x
;
426 sumYSquared
+= y
* y
;
430 var r
= (points
.length
* sumXTimesY
- sumX
* sumY
) /
431 Math
.sqrt((points
.length
* sumXSquared
- sumX
* sumX
) *
432 (points
.length
* sumYSquared
- sumY
* sumY
));
434 if (isNaN(r
) || r
== Math
.Infinity
)
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
};
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" />'
448 function createTableRow(runs
, test
, referenceIndex
) {
449 var tableRow
= $('<tr><td class="test">' + test
.name() + '</td><td class="unit">' + test
.unit() + '</td></tr>');
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;
459 var className
= 'comparison';
460 if (referenceResult
.isStatisticallySignificant(result
)) {
461 comparison
= formatPercentage(Math
.abs(percentDifference
)) + (better
? ' Better' : ' Worse ');
462 className
+= better
? ' better' : ' worse';
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>';
469 var values
= result
.values();
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>';
481 var statistics
= 'σ=' + toFixedWidthPrecision(result
.confidenceIntervalDelta()) + ', min=' + toFixedWidthPrecision(result
.min())
482 + ', max=' + toFixedWidthPrecision(result
.max()) + '\n' + regressionAnalysis
;
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
;
490 function markupForMissingRun(isReference
) {
491 return '<td colspan="' + (isReference
? 2 : 3) + '" class="missing">Missing</td>';
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())
501 if (runIndex
== referenceIndex
)
502 referenceResult
= results
[i
];
503 resultIndexMap
[runIndex
] = i
;
505 for (var i
= 0; i
< runs
.length
; i
++) {
506 var resultIndex
= resultIndexMap
[i
];
507 if (resultIndex
== undefined)
508 tableRow
.append(markupForMissingRun(i
== referenceIndex
));
510 tableRow
.append(markupForRun(results
[resultIndex
], referenceResult
));
513 $('#container').children('tbody').last().append(tableRow
);
515 tableRow
.click(function (event
) {
516 if (event
.target
!= tableRow
[0] && event
.target
.parentNode
!= tableRow
[0])
519 event
.preventDefault();
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': ''});
526 var plot
= createPlot(firstCell
, test
);
527 plot
.css({'position': 'absolute', 'z-index': 2});
528 var offset
= tableRow
.offset();
530 offset
.top
+= tableRow
.outerHeight();
532 tableRow
.children('td').css({'padding-bottom': plot
.outerHeight() + 5});
540 $.tablesorter
.addParser({
543 return s
.indexOf('|') >= 0;
545 format: function(s
) {
546 var parsed
= parseFloat(s
.substring(s
.indexOf('|') + 1));
547 return isNaN(parsed
) ? 0 : parsed
;
554 $.each(JSON
.parse(document
.getElementById('json').textContent
), function (index
, entry
) {
555 var run
= new TestRun(entry
);
558 function addTests(tests
, parentFullName
) {
559 for (var testName
in tests
) {
560 var fullTestName
= parentFullName
+ '/' + testName
;
561 var rawMetrics
= tests
[testName
].metrics
;
563 for (var metricName
in rawMetrics
) {
564 var fullMetricName
= fullTestName
+ ':' + metricName
;
565 var metric
= metrics
[fullMetricName
];
567 metric
= new PerfTestMetric(fullTestName
, metricName
);
568 metrics
[fullMetricName
] = metric
;
570 metric
.addResult(new TestResult(metric
, rawMetrics
[metricName
].current
, run
));
573 if (tests
[testName
].tests
)
574 addTests(tests
[testName
].tests
, fullTestName
);
578 addTests(entry
.tests
, '');
581 var shouldIgnoreMemory
= true;
582 var referenceIndex
= 0;
584 createTable(metrics
, runs
, shouldIgnoreMemory
, referenceIndex
);
586 $('#time-memory').bind('change', function (event
, checkedElement
) {
587 shouldIgnoreMemory
= checkedElement
.textContent
== 'Time';
588 createTable(metrics
, runs
, shouldIgnoreMemory
, referenceIndex
);
591 runs
.map(function (run
, index
) {
592 $('#reference').append('<span value="' + index
+ '"' + (index
== referenceIndex
? ' class="checked"' : '') + '>' + run
.label() + '</span>');
595 $('#reference').bind('change', function (event
, checkedElement
) {
596 referenceIndex
= parseInt(checkedElement
.getAttribute('value'));
597 createTable(metrics
, runs
, shouldIgnoreMemory
, referenceIndex
);
600 $('.checkbox').each(function (index
, checkbox
) {
601 $(checkbox
).children('span').click(function (event
) {
602 if ($(this).hasClass('checked'))
604 $(checkbox
).children('span').removeClass('checked');
605 $(this).addClass('checked');
606 $(checkbox
).trigger('change', $(this));
612 <script id=
"json" type=
"application/json">%PeformanceTestsResultsJSON%
</script>