1<!DOCTYPE html> 2<html> 3<head> 4<title>Blink Performance Test Results</title> 5<style type="text/css"> 6 7section { 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 24section h1 { 25 text-align: center; 26 font-size: 1em; 27} 28 29section .tooltip { 30 position: absolute; 31 text-align: center; 32 background: #ffcc66; 33 border-radius: 5px; 34 padding: 0px 5px; 35} 36 37body { 38 padding: 0px; 39 margin: 0px; 40 font-family: sans-serif; 41} 42 43table { 44 background: white; 45 width: 100%; 46} 47 48table, td, th { 49 border-collapse: collapse; 50 padding: 5px; 51} 52 53tr.even { 54 background: #f6f6f6; 55} 56 57table td { 58 position: relative; 59 font-family: monospace; 60} 61 62th, td { 63 cursor: pointer; 64 cursor: hand; 65} 66 67th { 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 73th:after { 74 content: ' \25B8'; 75} 76 77th.headerSortUp:after { 78 content: ' \25BE'; 79} 80 81th.headerSortDown:after { 82 content: ' \25B4'; 83} 84 85td.comparison, td.result { 86 text-align: right; 87} 88 89td.better { 90 color: #6c6; 91} 92 93td.worse { 94 color: #c66; 95} 96 97td.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;"> 133Result <span id="time-memory" class="checkbox"><span class="checked">Time</span><span>Memory</span></span> 134Reference <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 187function 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 214function 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 225function 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 271var plotColor = 'rgb(230,50,50)'; 272var subpointsPlotOptions = { 273 lines: {show:true, lineWidth: 0}, 274 color: plotColor, 275 points: {show: true, radius: 1}, 276 bars: {show: false}}; 277 278var 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 297var timePlotOptions = { 298 yaxis: { show: false }, 299 xaxis: { show: false }, 300 lines: { show: true }, 301 grid: { borderWidth: 1, borderColor: '#ccc' }, 302 colors: [ plotColor ] 303}; 304 305function 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 340function 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(); 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 358function 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 382function toFixedWidthPrecision(value) { 383 var decimal = value.toFixed(2); 384 return decimal; 385} 386 387function formatPercentage(fraction) { 388 var percentage = fraction * 100; 389 return (fraction * 100).toFixed(2) + '%'; 390} 391 392function 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>'); 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 410function 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 442var 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 448function 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 539function 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