1<!DOCTYPE html> 2<html> 3<head> 4<title>Telemetry Performance Test Results</title> 5<style type="text/css"> 6 7section { 8 background: white; 9 padding: 10px; 10 position: relative; 11} 12 13.collapsed:before { 14 color: #ccc; 15 content: '\25B8\00A0'; 16} 17 18.expanded:before { 19 color: #eee; 20 content: '\25BE\00A0'; 21} 22 23.line-plots { 24 padding-left: 25px; 25} 26 27.line-plots > div { 28 display: inline-block; 29 width: 90px; 30 height: 40px; 31 margin-right: 10px; 32} 33 34.lage-line-plots { 35 padding-left: 25px; 36} 37 38.large-line-plots > div, .histogram-plots > div { 39 display: inline-block; 40 width: 400px; 41 height: 200px; 42 margin-right: 10px; 43} 44 45.large-line-plot-labels > div, .histogram-plot-labels > div { 46 display: inline-block; 47 width: 400px; 48 height: 11px; 49 margin-right: 10px; 50 color: #545454; 51 text-align: center; 52 font-size: 11px; 53} 54 55.closeButton { 56 display: inline-block; 57 background: #eee; 58 background: linear-gradient(rgb(220, 220, 220), rgb(255, 255, 255)); 59 border: inset 1px #ddd; 60 border-radius: 4px; 61 float: right; 62 font-size: small; 63 -webkit-user-select: none; 64 font-weight: bold; 65 padding: 1px 4px; 66} 67 68.closeButton:hover { 69 background: #F09C9C; 70} 71 72.label { 73 cursor: text; 74} 75 76.label:hover { 77 background: #ffcc66; 78} 79 80section h1 { 81 text-align: center; 82 font-size: 1em; 83} 84 85section .tooltip { 86 position: absolute; 87 text-align: center; 88 background: #ffcc66; 89 border-radius: 5px; 90 padding: 0px 5px; 91} 92 93body { 94 padding: 0px; 95 margin: 0px; 96 font-family: sans-serif; 97} 98 99table { 100 background: white; 101 width: 100%; 102} 103 104table, td, th { 105 border-collapse: collapse; 106 padding: 5px; 107 white-space: nowrap; 108} 109 110.highlight:hover { 111 color: #202020; 112 background: #e0e0e0; 113} 114 115.nestedRow { 116 background: #f8f8f8; 117} 118 119.importantNestedRow { 120 background: #e0e0e0; 121 font-weight: bold; 122} 123 124table td { 125 position: relative; 126} 127 128th, td { 129 cursor: pointer; 130 cursor: hand; 131} 132 133th { 134 background: #e6eeee; 135 background: linear-gradient(rgb(244, 244, 244), rgb(217, 217, 217)); 136 border: 1px solid #ccc; 137} 138 139th.sortUp:after { 140 content: ' \25BE'; 141} 142 143th.sortDown:after { 144 content: ' \25B4'; 145} 146 147td.comparison, td.result { 148 text-align: right; 149} 150 151td.better { 152 color: #6c6; 153} 154 155td.fadeOut { 156 opacity: 0.5; 157} 158 159td.unknown { 160 color: #ccc; 161} 162 163td.worse { 164 color: #c66; 165} 166 167td.reference { 168 font-style: italic; 169 font-weight: bold; 170 color: #444; 171} 172 173td.missing { 174 color: #ccc; 175 text-align: center; 176} 177 178td.missingReference { 179 color: #ccc; 180 text-align: center; 181 font-style: italic; 182} 183 184.checkbox { 185 display: inline-block; 186 background: #eee; 187 background: linear-gradient(rgb(220, 220, 220), rgb(200, 200, 200)); 188 border: inset 1px #ddd; 189 border-radius: 5px; 190 margin: 10px; 191 font-size: small; 192 cursor: pointer; 193 cursor: hand; 194 -webkit-user-select: none; 195 font-weight: bold; 196} 197 198.checkbox span { 199 display: inline-block; 200 line-height: 100%; 201 padding: 5px 8px; 202 border: outset 1px transparent; 203} 204 205.checkbox .checked { 206 background: #e6eeee; 207 background: linear-gradient(rgb(255, 255, 255), rgb(235, 235, 235)); 208 border: outset 1px #eee; 209 border-radius: 5px; 210} 211 212.openAllButton { 213 display: inline-block; 214 colour: #6c6 215 background: #eee; 216 background: linear-gradient(rgb(220, 220, 220), rgb(255, 255, 255)); 217 border: inset 1px #ddd; 218 border-radius: 5px; 219 float: left; 220 font-size: small; 221 -webkit-user-select: none; 222 font-weight: bold; 223 padding: 1px 4px; 224} 225 226.openAllButton:hover { 227 background: #60f060; 228} 229 230.closeAllButton { 231 display: inline-block; 232 colour: #c66 233 background: #eee; 234 background: linear-gradient(rgb(220, 220, 220),rgb(255, 255, 255)); 235 border: inset 1px #ddd; 236 border-radius: 5px; 237 float: left; 238 font-size: small; 239 -webkit-user-select: none; 240 font-weight: bold; 241 padding: 1px 4px; 242} 243 244.closeAllButton:hover { 245 background: #f04040; 246} 247 248</style> 249</head> 250<body onload="init()"> 251<div style="padding: 0 10px; white-space: nowrap;"> 252Result <span id="time-memory" class="checkbox"></span> 253Reference <span id="reference" class="checkbox"></span> 254Style <span id="scatter-line" class="checkbox"><span class="checked">Scatter</span><span>Line</span></span> 255<span class="checkbox"><span class="checked" id="undelete">Undelete</span></span><br> 256Run your test with --reset-results to clear all runs 257</div> 258<table id="container"></table> 259<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.4/jquery.min.js"></script> 260<script> 261%plugins% 262</script> 263<script> 264 265var EXPANDED = true; 266var COLLAPSED = false; 267var SMALLEST_PERCENT_DISPLAYED = 0.01; 268var INVISIBLE = false; 269var VISIBLE = true; 270var COMPARISON_SUFFIX = '_compare'; 271var SORT_DOWN_CLASS = 'sortDown'; 272var SORT_UP_CLASS = 'sortUp'; 273var BETTER_CLASS = 'better'; 274var WORSE_CLASS = 'worse'; 275var UNKNOWN_CLASS = 'unknown' 276// px Indentation for graphs 277var GRAPH_INDENT = 64; 278var PADDING_UNDER_GRAPH = 5; 279// px Indentation for nested children left-margins 280var INDENTATION = 40; 281 282function TestResult(metric, values, associatedRun, std, degreesOfFreedom) { 283 if (values) { 284 if (values[0] instanceof Array) { 285 var flattenedValues = []; 286 for (var i = 0; i < values.length; i++) 287 flattenedValues = flattenedValues.concat(values[i]); 288 values = flattenedValues; 289 } 290 291 if (jQuery.type(values[0]) === 'string') { 292 try { 293 var current = JSON.parse(values[0]); 294 if (current.params.type === 'HISTOGRAM') { 295 this.histogramValues = current; 296 // Histogram results have no values (per se). Instead we calculate 297 // the values from the histogram bins. 298 var values = []; 299 var buckets = current.buckets 300 for (var i = 0; i < buckets.length; i++) { 301 var bucket = buckets[i]; 302 var bucket_mean = (bucket.high + bucket.low) / 2; 303 for (var b = 0; b < bucket.count; b++) { 304 values.push(bucket_mean); 305 } 306 } 307 } 308 } 309 catch (e) { 310 console.error(e, e.stack); 311 } 312 } 313 } else { 314 values = []; 315 } 316 317 this.test = function() { return metric; } 318 this.values = function() { return values.map(function(value) { return metric.scalingFactor() * value; }); } 319 this.unscaledMean = function() { return Statistics.sum(values) / values.length; } 320 this.mean = function() { return metric.scalingFactor() * this.unscaledMean(); } 321 this.min = function() { return metric.scalingFactor() * Statistics.min(values); } 322 this.max = function() { return metric.scalingFactor() * Statistics.max(values); } 323 this.confidenceIntervalDelta = function() { 324 if (std !== undefined) { 325 return metric.scalingFactor() * Statistics.confidenceIntervalDeltaFromStd(0.95, values.length, 326 std, degreesOfFreedom); 327 } 328 return metric.scalingFactor() * Statistics.confidenceIntervalDelta(0.95, values.length, 329 Statistics.sum(values), Statistics.squareSum(values)); 330 } 331 this.confidenceIntervalDeltaRatio = function() { return this.confidenceIntervalDelta() / this.mean(); } 332 this.percentDifference = function(other) { 333 if (other === undefined) { 334 return undefined; 335 } 336 return (other.unscaledMean() - this.unscaledMean()) / this.unscaledMean(); 337 } 338 this.isStatisticallySignificant = function(other) { 339 if (other === undefined) { 340 return false; 341 } 342 var diff = Math.abs(other.mean() - this.mean()); 343 return diff > this.confidenceIntervalDelta() && diff > other.confidenceIntervalDelta(); 344 } 345 this.run = function() { return associatedRun; } 346} 347 348function TestRun(entry) { 349 this.id = function() { return entry['buildTime'].replace(/[:.-]/g,''); } 350 this.label = function() { 351 if (labelKey in localStorage) 352 return localStorage[labelKey]; 353 return entry['label']; 354 } 355 this.setLabel = function(label) { localStorage[labelKey] = label; } 356 this.isHidden = function() { return localStorage[hiddenKey]; } 357 this.hide = function() { localStorage[hiddenKey] = true; } 358 this.show = function() { localStorage.removeItem(hiddenKey); } 359 this.description = function() { 360 return new Date(entry['buildTime']).toLocaleString() + '\n' + entry['platform'] + ' ' + this.label(); 361 } 362 363 var labelKey = 'telemetry_label_' + this.id(); 364 var hiddenKey = 'telemetry_hide_' + this.id(); 365} 366 367function PerfTestMetric(name, metric, unit, isImportant) { 368 var testResults = []; 369 var cachedUnit = null; 370 var cachedScalingFactor = null; 371 372 // We can't do this in TestResult because all results for each test need to share the same unit and the same scaling factor. 373 function computeScalingFactorIfNeeded() { 374 // FIXME: We shouldn't be adjusting units on every test result. 375 // We can only do this on the first test. 376 if (!testResults.length || cachedUnit) 377 return; 378 379 var mean = testResults[0].unscaledMean(); // FIXME: We should look at all values. 380 var kilo = unit == 'bytes' ? 1024 : 1000; 381 if (mean > 10 * kilo * kilo && unit != 'ms') { 382 cachedScalingFactor = 1 / kilo / kilo; 383 cachedUnit = 'M ' + unit; 384 } else if (mean > 10 * kilo) { 385 cachedScalingFactor = 1 / kilo; 386 cachedUnit = unit == 'ms' ? 's' : ('K ' + unit); 387 } else { 388 cachedScalingFactor = 1; 389 cachedUnit = unit; 390 } 391 } 392 393 this.name = function() { return name + ':' + metric; } 394 this.isImportant = isImportant; 395 this.isMemoryTest = function() { 396 return (unit == 'kb' || 397 unit == 'KB' || 398 unit == 'MB' || 399 unit == 'bytes' || 400 unit == 'count' || 401 !metric.indexOf('V8.')); 402 } 403 this.addResult = function(newResult) { 404 testResults.push(newResult); 405 cachedUnit = null; 406 cachedScalingFactor = null; 407 } 408 this.results = function() { return testResults; } 409 this.scalingFactor = function() { 410 computeScalingFactorIfNeeded(); 411 return cachedScalingFactor; 412 } 413 this.unit = function() { 414 computeScalingFactorIfNeeded(); 415 return cachedUnit; 416 } 417 this.biggerIsBetter = function() { 418 if (window.unitToBiggerIsBetter == undefined) { 419 window.unitToBiggerIsBetter = {}; 420 var units = JSON.parse(document.getElementById('units-json').textContent); 421 for (var u in units) { 422 if (units[u].improvement_direction == 'up') { 423 window.unitToBiggerIsBetter[u] = true; 424 } 425 } 426 } 427 return window.unitToBiggerIsBetter[unit]; 428 } 429} 430 431function UndeleteManager() { 432 var key = 'telemetry_undeleteIds' 433 var undeleteIds = localStorage[key]; 434 if (undeleteIds) { 435 undeleteIds = JSON.parse(undeleteIds); 436 } else { 437 undeleteIds = []; 438 } 439 440 this.ondelete = function(id) { 441 undeleteIds.push(id); 442 localStorage[key] = JSON.stringify(undeleteIds); 443 } 444 this.undeleteMostRecent = function() { 445 if (!this.mostRecentlyDeletedId()) 446 return; 447 undeleteIds.pop(); 448 localStorage[key] = JSON.stringify(undeleteIds); 449 } 450 this.mostRecentlyDeletedId = function() { 451 if (!undeleteIds.length) 452 return undefined; 453 return undeleteIds[undeleteIds.length-1]; 454 } 455} 456var undeleteManager = new UndeleteManager(); 457 458var plotColor = 'rgb(230,50,50)'; 459var subpointsPlotOptions = { 460 lines: {show:true, lineWidth: 0}, 461 color: plotColor, 462 points: {show: true, radius: 1}, 463 bars: {show: false}}; 464 465var mainPlotOptions = { 466 xaxis: { 467 min: -0.5, 468 tickSize: 1, 469 }, 470 crosshair: { mode: 'y' }, 471 series: { shadowSize: 0 }, 472 bars: {show: true, align: 'center', barWidth: 0.5}, 473 lines: { show: false }, 474 points: { show: true }, 475 grid: { 476 borderWidth: 1, 477 borderColor: '#ccc', 478 backgroundColor: '#fff', 479 hoverable: true, 480 autoHighlight: false, 481 } 482}; 483 484var linePlotOptions = { 485 yaxis: { show: false }, 486 xaxis: { show: false }, 487 lines: { show: true }, 488 grid: { borderWidth: 1, borderColor: '#ccc' }, 489 colors: [ plotColor ] 490}; 491 492var largeLinePlotOptions = { 493 xaxis: { 494 show: true, 495 tickDecimals: 0, 496 }, 497 lines: { show: true }, 498 grid: { borderWidth: 1, borderColor: '#ccc' }, 499 colors: [ plotColor ] 500}; 501 502var histogramPlotOptions = { 503 bars: {show: true, fill: 1} 504}; 505 506function createPlot(container, test, useLargeLinePlots) { 507 if (test.results()[0].histogramValues) { 508 var section = $('<section><div class="histogram-plots"></div>' 509 + '<div class="histogram-plot-labels"></div>' 510 + '<span class="tooltip"></span></section>'); 511 $(container).append(section); 512 attachHistogramPlots(test, section.children('.histogram-plots')); 513 } 514 else if (useLargeLinePlots) { 515 var section = $('<section><div class="large-line-plots"></div>' 516 + '<div class="large-line-plot-labels"></div>' 517 + '<span class="tooltip"></span></section>'); 518 $(container).append(section); 519 attachLinePlots(test, section.children('.large-line-plots'), useLargeLinePlots); 520 attachLinePlotLabels(test, section.children('.large-line-plot-labels')); 521 } else { 522 var section = $('<section><div class="plot"></div><div class="line-plots"></div>' 523 + '<span class="tooltip"></span></section>'); 524 section.children('.plot').css({'width': (100 * test.results().length + 25) + 'px', 'height': '300px'}); 525 $(container).append(section); 526 527 var plotContainer = section.children('.plot'); 528 var minIsZero = true; 529 attachPlot(test, plotContainer, minIsZero); 530 531 attachLinePlots(test, section.children('.line-plots'), useLargeLinePlots); 532 533 var tooltip = section.children('.tooltip'); 534 plotContainer.bind('plothover', function(event, position, item) { 535 if (item) { 536 var postfix = item.series.id ? ' (' + item.series.id + ')' : ''; 537 tooltip.html(item.datapoint[1].toPrecision(4) + postfix); 538 var sectionOffset = $(section).offset(); 539 tooltip.css({left: item.pageX - sectionOffset.left - tooltip.outerWidth() / 2, top: item.pageY - sectionOffset.top + 10}); 540 tooltip.fadeIn(200); 541 } else 542 tooltip.hide(); 543 }); 544 plotContainer.mouseout(function() { 545 tooltip.hide(); 546 }); 547 plotContainer.click(function(event) { 548 event.preventDefault(); 549 minIsZero = !minIsZero; 550 attachPlot(test, plotContainer, minIsZero); 551 }); 552 } 553 return section; 554} 555 556function attachLinePlots(test, container, useLargeLinePlots) { 557 var results = test.results(); 558 var attachedPlot = false; 559 560 if (useLargeLinePlots) { 561 var maximum = 0; 562 for (var i = 0; i < results.length; i++) { 563 var values = results[i].values(); 564 if (!values) 565 continue; 566 var local_max = Math.max.apply(Math, values); 567 if (local_max > maximum) 568 maximum = local_max; 569 } 570 } 571 572 for (var i = 0; i < results.length; i++) { 573 container.append('<div></div>'); 574 var values = results[i].values(); 575 if (!values) 576 continue; 577 attachedPlot = true; 578 579 if (useLargeLinePlots) { 580 var options = $.extend(true, {}, largeLinePlotOptions, 581 {yaxis: {min: 0.0, max: maximum}, 582 xaxis: {min: 0.0, max: values.length - 1}, 583 points: {show: (values.length < 2) ? true : false}}); 584 } else { 585 var options = $.extend(true, {}, linePlotOptions, 586 {yaxis: {min: Math.min.apply(Math, values) * 0.9, max: Math.max.apply(Math, values) * 1.1}, 587 xaxis: {min: -0.5, max: values.length - 0.5}, 588 points: {show: (values.length < 2) ? true : false}}); 589 } 590 $.plot(container.children().last(), [values.map(function(value, index) { return [index, value]; })], options); 591 } 592 if (!attachedPlot) 593 container.children().remove(); 594} 595 596function attachHistogramPlots(test, container) { 597 var results = test.results(); 598 var attachedPlot = false; 599 600 for (var i = 0; i < results.length; i++) { 601 container.append('<div></div>'); 602 var histogram = results[i].histogramValues 603 if (!histogram) 604 continue; 605 attachedPlot = true; 606 607 var buckets = histogram.buckets 608 var bucket; 609 var max_count = 0; 610 for (var j = 0; j < buckets.length; j++) { 611 bucket = buckets[j]; 612 max_count = Math.max(max_count, bucket.count); 613 } 614 var xmax = bucket.high * 1.1; 615 var ymax = max_count * 1.1; 616 617 var options = $.extend(true, {}, histogramPlotOptions, 618 {yaxis: {min: 0.0, max: ymax}, 619 xaxis: {min: histogram.params.min, max: xmax}}); 620 var plot = $.plot(container.children().last(), [[]], options); 621 // Flot only supports fixed with bars and our histogram's buckets are 622 // variable width, so we need to do our own bar drawing. 623 var ctx = plot.getCanvas().getContext("2d"); 624 ctx.lineWidth="1"; 625 ctx.fillStyle = "rgba(255, 0, 0, 0.2)"; 626 ctx.strokeStyle="red"; 627 for (var j = 0; j < buckets.length; j++) { 628 bucket = buckets[j]; 629 var bl = plot.pointOffset({ x: bucket.low, y: 0}); 630 var tr = plot.pointOffset({ x: bucket.high, y: bucket.count}); 631 ctx.fillRect(bl.left, bl.top, tr.left - bl.left, tr.top - bl.top); 632 ctx.strokeRect(bl.left, bl.top, tr.left - bl.left, tr.top - bl.top); 633 } 634 } 635 if (!attachedPlot) 636 container.children().remove(); 637} 638 639function attachLinePlotLabels(test, container) { 640 var results = test.results(); 641 var attachedPlot = false; 642 for (var i = 0; i < results.length; i++) { 643 container.append('<div>' + results[i].run().label() + '</div>'); 644 } 645} 646 647function attachPlot(test, plotContainer, minIsZero) { 648 var results = test.results(); 649 650 var values = results.reduce(function(values, result, index) { 651 var newValues = result.values(); 652 return newValues ? values.concat(newValues.map(function(value) { return [index, value]; })) : values; 653 }, []); 654 655 var plotData = [$.extend(true, {}, subpointsPlotOptions, {data: values})]; 656 plotData.push({id: 'μ', data: results.map(function(result, index) { return [index, result.mean()]; }), color: plotColor}); 657 658 var overallMax = Statistics.max(results.map(function(result, index) { return result.max(); })); 659 var overallMin = Statistics.min(results.map(function(result, index) { return result.min(); })); 660 var margin = (overallMax - overallMin) * 0.1; 661 var currentPlotOptions = $.extend(true, {}, mainPlotOptions, {yaxis: { 662 min: minIsZero ? 0 : overallMin - margin, 663 max: minIsZero ? overallMax * 1.1 : overallMax + margin}}); 664 665 currentPlotOptions.xaxis.max = results.length - 0.5; 666 currentPlotOptions.xaxis.ticks = results.map(function(result, index) { return [index, result.run().label()]; }); 667 668 $.plot(plotContainer, plotData, currentPlotOptions); 669} 670 671function toFixedWidthPrecision(value) { 672 var decimal = value.toFixed(2); 673 return decimal; 674} 675 676function formatPercentage(fraction) { 677 var percentage = fraction * 100; 678 return (fraction * 100).toFixed(2) + '%'; 679} 680 681function setUpSortClicks(runs) 682{ 683 $('#nameColumn').click(sortByName); 684 685 $('#unitColumn').click(sortByUnit); 686 687 runs.forEach(function(run) { 688 $('#' + run.id()).click(sortByResult); 689 $('#' + run.id() + COMPARISON_SUFFIX).click(sortByReference); 690 }); 691} 692 693function TestTypeSelector(tests) { 694 this.recognizers = { 695 'Time': function(test) { return !test.isMemoryTest(); }, 696 'Memory': function(test) { return test.isMemoryTest(); } 697 }; 698 this.testTypeNames = this.generateUsedTestTypeNames(tests); 699 // Default to selecting the first test-type name in the list. 700 this.testTypeName = this.testTypeNames[0]; 701} 702 703TestTypeSelector.prototype = { 704 set testTypeName(testTypeName) { 705 this._testTypeName = testTypeName; 706 this.shouldShowTest = this.recognizers[testTypeName]; 707 }, 708 709 generateUsedTestTypeNames: function(allTests) { 710 var testTypeNames = []; 711 712 for (var recognizedTestName in this.recognizers) { 713 var recognizes = this.recognizers[recognizedTestName]; 714 for (var testName in allTests) { 715 var test = allTests[testName]; 716 if (recognizes(test)) { 717 testTypeNames.push(recognizedTestName); 718 break; 719 } 720 } 721 } 722 723 if (testTypeNames.length === 0) { 724 // No test types we recognize, add 'No Results' with a dummy recognizer. 725 var noResults = 'No Results'; 726 this.recognizers[noResults] = function() { return false; }; 727 testTypeNames.push(noResults); 728 } else if (testTypeNames.length > 1) { 729 // We have more than one test type, so add 'All' with a recognizer that always succeeds. 730 var allResults = 'All'; 731 this.recognizers[allResults] = function() { return true; }; 732 testTypeNames.push(allResults); 733 } 734 735 return testTypeNames; 736 }, 737 738 buildButtonHTMLForUsedTestTypes: function() { 739 var selectedTestTypeName = this._testTypeName; 740 // Build spans for all recognised test names with the selected test highlighted. 741 return this.testTypeNames.map(function(testTypeName) { 742 var classAttribute = testTypeName === selectedTestTypeName ? ' class=checked' : ''; 743 return '<span' + classAttribute + '>' + testTypeName + '</span>'; 744 }).join(''); 745 } 746}; 747 748var topLevelRows; 749var allTableRows; 750 751function displayTable(tests, runs, testTypeSelector, referenceIndex, useLargeLinePlots) { 752 var resultHeaders = runs.map(function(run, index) { 753 var header = '<th id="' + run.id() + '" ' + 754 'colspan=2 ' + 755 'title="' + run.description() + '">' + 756 '<span class="label" ' + 757 'title="Edit run label">' + 758 run.label() + 759 '</span>' + 760 '<div class="closeButton" ' + 761 'title="Delete run">' + 762 '×' + 763 '</div>' + 764 '</th>'; 765 if (index !== referenceIndex) { 766 header += '<th id="' + run.id() + COMPARISON_SUFFIX + '" ' + 767 'title="Sort by better/worse">' + 768 'Δ' + 769 '</th>'; 770 } 771 return header; 772 }); 773 774 resultHeaders = resultHeaders.join(''); 775 776 htmlString = '<thead>' + 777 '<tr>' + 778 '<th id="nameColumn">' + 779 '<div class="openAllButton" ' + 780 'title="Open all rows or graphs">' + 781 'Open All' + 782 '</div>' + 783 '<div class="closeAllButton" ' + 784 'title="Close all rows">' + 785 'Close All' + 786 '</div>' + 787 'Test' + 788 '</th>' + 789 '<th id="unitColumn">' + 790 'Unit' + 791 '</th>' + 792 resultHeaders + 793 '</tr>' + 794 '</head>' + 795 '<tbody>' + 796 '</tbody>'; 797 798 $('#container').html(htmlString); 799 800 var testNames = []; 801 for (testName in tests) 802 testNames.push(testName); 803 804 allTableRows = []; 805 testNames.forEach(function(testName) { 806 var test = tests[testName]; 807 if (testTypeSelector.shouldShowTest(test)) { 808 allTableRows.push(new TableRow(runs, test, referenceIndex, useLargeLinePlots)); 809 } 810 }); 811 812 // Build a list of top level rows with attached children 813 topLevelRows = []; 814 allTableRows.forEach(function(row) { 815 // Add us to top level if we are a top-level row... 816 if (row.hasNoURL) { 817 topLevelRows.push(row); 818 // Add a duplicate child row that holds the graph for the parent 819 var graphHolder = new TableRow(runs, row.test, referenceIndex, useLargeLinePlots); 820 graphHolder.isImportant = true; 821 graphHolder.URL = 'Summary'; 822 graphHolder.hideRowData(); 823 allTableRows.push(graphHolder); 824 row.addNestedChild(graphHolder); 825 return; 826 } 827 828 // ...or add us to our parent if we have one ... 829 for (var i = 0; i < allTableRows.length; i++) { 830 if (allTableRows[i].isParentOf(row)) { 831 allTableRows[i].addNestedChild(row); 832 return; 833 } 834 } 835 836 // ...otherwise this result is orphaned, display it at top level with a graph 837 row.hasGraph = true; 838 topLevelRows.push(row); 839 }); 840 841 buildTable(topLevelRows); 842 843 $('.closeButton').click(function(event) { 844 for (var i = 0; i < runs.length; i++) { 845 if (runs[i].id() == event.target.parentNode.id) { 846 runs[i].hide(); 847 undeleteManager.ondelete(runs[i].id()); 848 location.reload(); 849 break; 850 } 851 } 852 event.stopPropagation(); 853 }); 854 855 $('.closeAllButton').click(function(event) { 856 for (var i = 0; i < allTableRows.length; i++) { 857 allTableRows[i].closeRow(); 858 } 859 event.stopPropagation(); 860 }); 861 862 $('.openAllButton').click(function(event) { 863 for (var i = 0; i < topLevelRows.length; i++) { 864 topLevelRows[i].openRow(); 865 } 866 event.stopPropagation(); 867 }); 868 869 setUpSortClicks(runs); 870 871 $('.label').click(function(event) { 872 for (var i = 0; i < runs.length; i++) { 873 if (runs[i].id() == event.target.parentNode.id) { 874 $(event.target).replaceWith('<input id="labelEditor" type="text" value="' + runs[i].label() + '">'); 875 $('#labelEditor').focusout(function() { 876 runs[i].setLabel(this.value); 877 location.reload(); 878 }); 879 $('#labelEditor').keypress(function(event) { 880 if (event.which == 13) { 881 runs[i].setLabel(this.value); 882 location.reload(); 883 } 884 }); 885 $('#labelEditor').click(function(event) { 886 event.stopPropagation(); 887 }); 888 $('#labelEditor').mousedown(function(event) { 889 event.stopPropagation(); 890 }); 891 $('#labelEditor').select(); 892 break; 893 } 894 } 895 event.stopPropagation(); 896 }); 897} 898 899function validForSorting(row) { 900 return ($.type(row.sortValue) === 'string') || !isNaN(row.sortValue); 901} 902 903var sortDirection = 1; 904 905function sortRows(rows) { 906 rows.sort( 907 function(rowA,rowB) { 908 if (validForSorting(rowA) !== validForSorting(rowB)) { 909 // Sort valid values upwards when compared to invalid 910 if (validForSorting(rowA)) { 911 return -1; 912 } 913 if (validForSorting(rowB)) { 914 return 1; 915 } 916 } 917 918 // Some rows always sort to the top 919 if (rowA.isImportant) { 920 return -1; 921 } 922 if (rowB.isImportant) { 923 return 1; 924 } 925 926 if (rowA.sortValue === rowB.sortValue) { 927 // Sort identical values by name to keep the sort stable, 928 // always keep name alphabetical (even if a & b sort values 929 // are invalid) 930 return rowA.test.name() > rowB.test.name() ? 1 : -1; 931 } 932 933 return rowA.sortValue > rowB.sortValue ? sortDirection : -sortDirection; 934 } ); 935 936 // Sort the rows' children 937 rows.forEach(function(row) { 938 sortRows(row.children); 939 }); 940} 941 942function buildTable(rows) { 943 rows.forEach(function(row) { 944 row.removeFromPage(); 945 }); 946 947 sortRows(rows); 948 949 rows.forEach(function(row) { 950 row.addToPage(); 951 }); 952} 953 954var activeSortHeaderElement = undefined; 955var columnSortDirection = {}; 956 957function determineColumnSortDirection(element) { 958 columnDirection = columnSortDirection[element.id]; 959 960 if (columnDirection === undefined) { 961 // First time we've sorted this row, default to down 962 columnSortDirection[element.id] = SORT_DOWN_CLASS; 963 } else if (element === activeSortHeaderElement) { 964 // Clicking on same header again, swap direction 965 columnSortDirection[element.id] = (columnDirection === SORT_UP_CLASS) ? SORT_DOWN_CLASS : SORT_UP_CLASS; 966 } 967} 968 969function updateSortDirection(element) { 970 // Remove old header's sort arrow 971 if (activeSortHeaderElement !== undefined) { 972 activeSortHeaderElement.classList.remove(columnSortDirection[activeSortHeaderElement.id]); 973 } 974 975 determineColumnSortDirection(element); 976 977 sortDirection = (columnSortDirection[element.id] === SORT_UP_CLASS) ? 1 : -1; 978 979 // Add new header's sort arrow 980 element.classList.add(columnSortDirection[element.id]); 981 activeSortHeaderElement = element; 982} 983 984function sortByName(event) { 985 updateSortDirection(event.toElement); 986 987 allTableRows.forEach(function(row) { 988 row.prepareToSortByName(); 989 }); 990 991 buildTable(topLevelRows); 992} 993 994function sortByUnit(event) { 995 updateSortDirection(event.toElement); 996 997 allTableRows.forEach(function(row) { 998 row.prepareToSortByUnit(); 999 }); 1000 1001 buildTable(topLevelRows); 1002} 1003 1004function sortByResult(event) { 1005 updateSortDirection(event.toElement); 1006 1007 var runId = event.target.id; 1008 1009 allTableRows.forEach(function(row) { 1010 row.prepareToSortByTestResults(runId); 1011 }); 1012 1013 buildTable(topLevelRows); 1014} 1015 1016function sortByReference(event) { 1017 updateSortDirection(event.toElement); 1018 1019 // The element ID has _compare appended to allow us to set up a click event 1020 // remove the _compare to return a useful Id 1021 var runIdWithCompare = event.target.id; 1022 var runId = runIdWithCompare.split('_')[0]; 1023 1024 allTableRows.forEach(function(row) { 1025 row.prepareToSortRelativeToReference(runId); 1026 }); 1027 1028 buildTable(topLevelRows); 1029} 1030 1031function linearRegression(points) { 1032 // Implement http://www.easycalculation.com/statistics/learn-correlation.php. 1033 // x = magnitude 1034 // y = iterations 1035 var sumX = 0; 1036 var sumY = 0; 1037 var sumXSquared = 0; 1038 var sumYSquared = 0; 1039 var sumXTimesY = 0; 1040 1041 for (var i = 0; i < points.length; i++) { 1042 var x = i; 1043 var y = points[i]; 1044 sumX += x; 1045 sumY += y; 1046 sumXSquared += x * x; 1047 sumYSquared += y * y; 1048 sumXTimesY += x * y; 1049 } 1050 1051 var r = (points.length * sumXTimesY - sumX * sumY) / 1052 Math.sqrt((points.length * sumXSquared - sumX * sumX) * 1053 (points.length * sumYSquared - sumY * sumY)); 1054 1055 if (isNaN(r) || r == Math.Infinity) 1056 r = 0; 1057 1058 var slope = (points.length * sumXTimesY - sumX * sumY) / (points.length * sumXSquared - sumX * sumX); 1059 var intercept = sumY / points.length - slope * sumX / points.length; 1060 return {slope: slope, intercept: intercept, rSquared: r * r}; 1061} 1062 1063var warningSign = '<svg viewBox="0 0 100 100" style="width: 18px; height: 18px; vertical-align: bottom;" version="1.1">' 1064 + '<polygon fill="red" points="50,10 90,80 10,80 50,10" stroke="red" stroke-width="10" stroke-linejoin="round" />' 1065 + '<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" />' 1066 + '<circle cx="50" cy="73" r="6" fill="white" />' 1067 + '</svg>'; 1068 1069function TableRow(runs, test, referenceIndex, useLargeLinePlots) { 1070 this.runs = runs; 1071 this.test = test; 1072 this.referenceIndex = referenceIndex; 1073 this.useLargeLinePlots = useLargeLinePlots; 1074 this.children = []; 1075 1076 this.tableRow = $('<tr class="highlight">' + 1077 '<td class="test collapsed" >' + 1078 this.test.name() + 1079 '</td>' + 1080 '<td class="unit">' + 1081 this.test.unit() + 1082 '</td>' + 1083 '</tr>'); 1084 1085 var runIndex = 0; 1086 var results = this.test.results(); 1087 var referenceResult = undefined; 1088 1089 this.resultIndexMap = {}; 1090 for (var i = 0; i < results.length; i++) { 1091 while (this.runs[runIndex] !== results[i].run()) 1092 runIndex++; 1093 if (runIndex === this.referenceIndex) 1094 referenceResult = results[i]; 1095 this.resultIndexMap[runIndex] = i; 1096 } 1097 for (var i = 0; i < this.runs.length; i++) { 1098 var resultIndex = this.resultIndexMap[i]; 1099 if (resultIndex === undefined) 1100 this.tableRow.append(this.markupForMissingRun(i == this.referenceIndex)); 1101 else 1102 this.tableRow.append(this.markupForRun(results[resultIndex], referenceResult)); 1103 } 1104 1105 // Use the test name (without URL) to bind parents and their children 1106 var nameAndURL = this.test.name().split('.'); 1107 var benchmarkName = nameAndURL.shift(); 1108 this.testName = nameAndURL.shift(); 1109 this.hasNoURL = (nameAndURL.length === 0); 1110 1111 if (!this.hasNoURL) { 1112 // Re-join the URL 1113 this.URL = nameAndURL.join('.'); 1114 } 1115 1116 this.isImportant = false; 1117 this.hasGraph = false; 1118 this.currentIndentationClass = '' 1119 this.indentLevel = 0; 1120 this.setRowNestedState(COLLAPSED); 1121 this.setVisibility(VISIBLE); 1122 this.prepareToSortByName(); 1123} 1124 1125TableRow.prototype.hideRowData = function() { 1126 data = this.tableRow.children('td'); 1127 1128 for (index in data) { 1129 if (index > 0) { 1130 // Blank out everything except the test name 1131 data[index].innerHTML = ''; 1132 } 1133 } 1134} 1135 1136TableRow.prototype.prepareToSortByTestResults = function(runId) { 1137 var testResults = this.test.results(); 1138 // Find the column in this row that matches the runId and prepare to 1139 // sort by the mean of that test. 1140 for (index in testResults) { 1141 sourceId = testResults[index].run().id(); 1142 if (runId === sourceId) { 1143 this.sortValue = testResults[index].mean(); 1144 return; 1145 } 1146 } 1147 // This row doesn't have any results for the passed runId 1148 this.sortValue = undefined; 1149} 1150 1151TableRow.prototype.prepareToSortRelativeToReference = function(runId) { 1152 var testResults = this.test.results(); 1153 1154 // Get index of test results that correspond to the reference column. 1155 var remappedReferenceIndex = this.resultIndexMap[this.referenceIndex]; 1156 1157 if (remappedReferenceIndex === undefined) { 1158 // This test has no results in the reference run. 1159 this.sortValue = undefined; 1160 return; 1161 } 1162 1163 otherResults = testResults[remappedReferenceIndex]; 1164 1165 // Find the column in this row that matches the runId and prepare to 1166 // sort by the difference from the reference. 1167 for (index in testResults) { 1168 sourceId = testResults[index].run().id(); 1169 if (runId === sourceId) { 1170 this.sortValue = testResults[index].percentDifference(otherResults); 1171 if (this.test.biggerIsBetter()) { 1172 // For this test bigger is not better 1173 this.sortValue = -this.sortValue; 1174 } 1175 return; 1176 } 1177 } 1178 // This row doesn't have any results for the passed runId 1179 this.sortValue = undefined; 1180} 1181 1182TableRow.prototype.prepareToSortByUnit = function() { 1183 this.sortValue = this.test.unit().toLowerCase(); 1184} 1185 1186TableRow.prototype.prepareToSortByName = function() { 1187 this.sortValue = this.test.name().toLowerCase(); 1188} 1189 1190TableRow.prototype.isParentOf = function(row) { 1191 return this.hasNoURL && (this.testName === row.testName); 1192} 1193 1194TableRow.prototype.addNestedChild = function(child) { 1195 this.children.push(child); 1196 1197 // Indent child one step in from parent 1198 child.indentLevel = this.indentLevel + INDENTATION; 1199 child.hasGraph = true; 1200 // Start child off as hidden (i.e. collapsed inside parent) 1201 child.setVisibility(INVISIBLE); 1202 child.updateIndentation(); 1203 // Show URL in the title column 1204 child.tableRow.children()[0].innerHTML = child.URL; 1205 // Set up class to change background colour of nested rows 1206 if (child.isImportant) { 1207 child.tableRow.addClass('importantNestedRow'); 1208 } else { 1209 child.tableRow.addClass('nestedRow'); 1210 } 1211} 1212 1213TableRow.prototype.setVisibility = function(visibility) { 1214 this.visibility = visibility; 1215 this.tableRow[0].style.display = (visibility === INVISIBLE) ? 'none' : ''; 1216} 1217 1218TableRow.prototype.setRowNestedState = function(newState) { 1219 this.rowState = newState; 1220 this.updateIndentation(); 1221} 1222 1223TableRow.prototype.updateIndentation = function() { 1224 var element = this.tableRow.children('td').first(); 1225 1226 element.removeClass(this.currentIndentationClass); 1227 1228 this.currentIndentationClass = (this.rowState === COLLAPSED) ? 'collapsed' : 'expanded'; 1229 1230 element[0].style.marginLeft = this.indentLevel.toString() + 'px'; 1231 element[0].style.float = 'left'; 1232 1233 element.addClass(this.currentIndentationClass); 1234} 1235 1236TableRow.prototype.addToPage = function() { 1237 $('#container').children('tbody').last().append(this.tableRow); 1238 1239 // Set up click callback 1240 var owningObject = this; 1241 this.tableRow.click(function(event) { 1242 event.preventDefault(); 1243 owningObject.toggle(); 1244 }); 1245 1246 // Add children to the page too 1247 this.children.forEach(function(child) { 1248 child.addToPage(); 1249 }); 1250} 1251 1252TableRow.prototype.removeFromPage = function() { 1253 // Remove children 1254 this.children.forEach(function(child) { 1255 child.removeFromPage(); 1256 }); 1257 // Remove us 1258 this.tableRow.remove(); 1259} 1260 1261 1262TableRow.prototype.markupForRun = function(result, referenceResult) { 1263 var comparisonCell = ''; 1264 var shouldCompare = result !== referenceResult; 1265 if (shouldCompare) { 1266 var comparisonText = ''; 1267 var className = ''; 1268 1269 if (referenceResult) { 1270 var percentDifference = referenceResult.percentDifference(result); 1271 if (isNaN(percentDifference)) { 1272 comparisonText = 'Unknown'; 1273 className = UNKNOWN_CLASS; 1274 } else if (Math.abs(percentDifference) < SMALLEST_PERCENT_DISPLAYED) { 1275 comparisonText = 'Equal'; 1276 // Show equal values in green 1277 className = BETTER_CLASS; 1278 } else { 1279 var better = this.test.biggerIsBetter() ? percentDifference > 0 : percentDifference < 0; 1280 comparisonText = formatPercentage(Math.abs(percentDifference)) + (better ? ' Better' : ' Worse'); 1281 className = better ? BETTER_CLASS : WORSE_CLASS; 1282 } 1283 1284 if (!referenceResult.isStatisticallySignificant(result)) { 1285 // Put result in brackets and fade if not statistically significant 1286 className += ' fadeOut'; 1287 comparisonText = '(' + comparisonText + ')'; 1288 } 1289 } 1290 comparisonCell = '<td class="comparison ' + className + '">' + comparisonText + '</td>'; 1291 } 1292 1293 var values = result.values(); 1294 var warning = ''; 1295 var regressionAnalysis = ''; 1296 if (result.histogramValues) { 1297 // Don't calculate regression result for histograms. 1298 } else if (values && values.length > 3) { 1299 regressionResult = linearRegression(values); 1300 regressionAnalysis = 'slope=' + toFixedWidthPrecision(regressionResult.slope) 1301 + ', R^2=' + toFixedWidthPrecision(regressionResult.rSquared); 1302 if (regressionResult.rSquared > 0.6 && Math.abs(regressionResult.slope) > 0.01) { 1303 warning = ' <span class="regression-warning" title="Detected a time dependency with ' + regressionAnalysis + '">' + warningSign + ' </span>'; 1304 } 1305 } 1306 1307 var referenceClass = shouldCompare ? '' : 'reference'; 1308 1309 var statistics = 'σ=' + toFixedWidthPrecision(result.confidenceIntervalDelta()) + ', min=' + toFixedWidthPrecision(result.min()) 1310 + ', max=' + toFixedWidthPrecision(result.max()) + '\n' + regressionAnalysis; 1311 1312 var confidence; 1313 if (isNaN(result.confidenceIntervalDeltaRatio())) { 1314 // Don't bother showing +- Nan as it is meaningless 1315 confidence = ''; 1316 } else { 1317 confidence = '± ' + formatPercentage(result.confidenceIntervalDeltaRatio()); 1318 } 1319 1320 return '<td class="result ' + referenceClass + '" title="' + statistics + '">' + toFixedWidthPrecision(result.mean()) 1321 + '</td><td class="confidenceIntervalDelta ' + referenceClass + '" title="' + statistics + '">' + confidence + warning + '</td>' + comparisonCell; 1322} 1323 1324TableRow.prototype.markupForMissingRun = function(isReference) { 1325 if (isReference) { 1326 return '<td colspan=2 class="missingReference">Missing</td>'; 1327 } 1328 return '<td colspan=3 class="missing">Missing</td>'; 1329} 1330 1331TableRow.prototype.openRow = function() { 1332 if (this.rowState === EXPANDED) { 1333 // If we're already expanded, open our children instead 1334 this.children.forEach(function(child) { 1335 child.openRow(); 1336 }); 1337 return; 1338 } 1339 1340 this.setRowNestedState(EXPANDED); 1341 1342 if (this.hasGraph) { 1343 var firstCell = this.tableRow.children('td').first(); 1344 var plot = createPlot(firstCell, this.test, this.useLargeLinePlots); 1345 plot.css({'position': 'absolute', 'z-index': 2}); 1346 var offset = this.tableRow.offset(); 1347 offset.left += GRAPH_INDENT; 1348 offset.top += this.tableRow.outerHeight(); 1349 plot.offset(offset); 1350 this.tableRow.children('td').css({'padding-bottom': plot.outerHeight() + PADDING_UNDER_GRAPH}); 1351 } 1352 1353 this.children.forEach(function(child) { 1354 child.setVisibility(VISIBLE); 1355 }); 1356 1357 if (this.children.length === 1) { 1358 // If we only have a single child... 1359 var child = this.children[0]; 1360 if (child.isImportant) { 1361 // ... and it is important (i.e. the summary row) just open it when 1362 // parent is opened to save needless clicking 1363 child.openRow(); 1364 } 1365 } 1366} 1367 1368TableRow.prototype.closeRow = function() { 1369 if (this.rowState === COLLAPSED) { 1370 return; 1371 } 1372 1373 this.setRowNestedState(COLLAPSED); 1374 1375 if (this.hasGraph) { 1376 var firstCell = this.tableRow.children('td').first(); 1377 firstCell.children('section').remove(); 1378 this.tableRow.children('td').css({'padding-bottom': ''}); 1379 } 1380 1381 this.children.forEach(function(child) { 1382 // Make children invisible, but leave their collapsed status alone 1383 child.setVisibility(INVISIBLE); 1384 }); 1385} 1386 1387TableRow.prototype.toggle = function() { 1388 if (this.rowState === EXPANDED) { 1389 this.closeRow(); 1390 } else { 1391 this.openRow(); 1392 } 1393 return false; 1394} 1395 1396function init() { 1397 var runs = []; 1398 var metrics = {}; 1399 var deletedRunsById = {}; 1400 $.each(JSON.parse(document.getElementById('results-json').textContent), function(index, entry) { 1401 var run = new TestRun(entry); 1402 if (run.isHidden()) { 1403 deletedRunsById[run.id()] = run; 1404 return; 1405 } 1406 1407 runs.push(run); 1408 1409 function addTests(tests) { 1410 for (var testName in tests) { 1411 var rawMetrics = tests[testName].metrics; 1412 1413 for (var metricName in rawMetrics) { 1414 var fullMetricName = testName + ':' + metricName; 1415 var metric = metrics[fullMetricName]; 1416 if (!metric) { 1417 metric = new PerfTestMetric(testName, metricName, rawMetrics[metricName].units, rawMetrics[metricName].important); 1418 metrics[fullMetricName] = metric; 1419 } 1420 // std & degrees_of_freedom could be undefined 1421 metric.addResult( 1422 new TestResult(metric, rawMetrics[metricName].current, 1423 run, rawMetrics[metricName]['std'], rawMetrics[metricName]['degrees_of_freedom'])); 1424 } 1425 } 1426 } 1427 1428 addTests(entry.tests); 1429 }); 1430 1431 var useLargeLinePlots = false; 1432 var referenceIndex = 0; 1433 1434 var testTypeSelector = new TestTypeSelector(metrics); 1435 var buttonHTML = testTypeSelector.buildButtonHTMLForUsedTestTypes(); 1436 $('#time-memory').append(buttonHTML); 1437 1438 $('#scatter-line').bind('change', function(event, checkedElement) { 1439 useLargeLinePlots = checkedElement.textContent == 'Line'; 1440 displayTable(metrics, runs, testTypeSelector, referenceIndex, useLargeLinePlots); 1441 }); 1442 1443 runs.map(function(run, index) { 1444 $('#reference').append('<span value="' + index + '"' + (index == referenceIndex ? ' class="checked"' : '') + ' title="' + run.description() + '">' + run.label() + '</span>'); 1445 }) 1446 1447 $('#time-memory').bind('change', function(event, checkedElement) { 1448 testTypeSelector.testTypeName = checkedElement.textContent; 1449 displayTable(metrics, runs, testTypeSelector, referenceIndex, useLargeLinePlots); 1450 }); 1451 1452 $('#reference').bind('change', function(event, checkedElement) { 1453 referenceIndex = parseInt(checkedElement.getAttribute('value')); 1454 displayTable(metrics, runs, testTypeSelector, referenceIndex, useLargeLinePlots); 1455 }); 1456 1457 displayTable(metrics, runs, testTypeSelector, referenceIndex, useLargeLinePlots); 1458 1459 $('.checkbox').each(function(index, checkbox) { 1460 $(checkbox).children('span').click(function(event) { 1461 if ($(this).hasClass('checked')) 1462 return; 1463 $(checkbox).children('span').removeClass('checked'); 1464 $(this).addClass('checked'); 1465 $(checkbox).trigger('change', $(this)); 1466 }); 1467 }); 1468 1469 runToUndelete = deletedRunsById[undeleteManager.mostRecentlyDeletedId()]; 1470 1471 if (runToUndelete) { 1472 $('#undelete').html('Undelete ' + runToUndelete.label()); 1473 $('#undelete').attr('title', runToUndelete.description()); 1474 $('#undelete').click(function(event) { 1475 runToUndelete.show(); 1476 undeleteManager.undeleteMostRecent(); 1477 location.reload(); 1478 }); 1479 } else { 1480 $('#undelete').hide(); 1481 } 1482} 1483 1484</script> 1485<script id="results-json" type="application/json">%json_results%</script> 1486<script id="units-json" type="application/json">%json_units%</script> 1487</body> 1488</html> 1489