Home | History | Annotate | Download | only in html_output
      1 <!DOCTYPE html>
      2 <html>
      3 <head>
      4 <title>Telemetry Performance Test Results</title>
      5 <style type="text/css">
      6 
      7 section {
      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 
     80 section h1 {
     81     text-align: center;
     82     font-size: 1em;
     83 }
     84 
     85 section .tooltip {
     86     position: absolute;
     87     text-align: center;
     88     background: #ffcc66;
     89     border-radius: 5px;
     90     padding: 0px 5px;
     91 }
     92 
     93 body {
     94     padding: 0px;
     95     margin: 0px;
     96     font-family: sans-serif;
     97 }
     98 
     99 table {
    100     background: white;
    101     width: 100%;
    102 }
    103 
    104 table, 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 
    124 table td {
    125     position: relative;
    126 }
    127 
    128 th, td {
    129     cursor: pointer;
    130     cursor: hand;
    131 }
    132 
    133 th {
    134     background: #e6eeee;
    135     background: linear-gradient(rgb(244, 244, 244), rgb(217, 217, 217));
    136     border: 1px solid #ccc;
    137 }
    138 
    139 th.sortUp:after {
    140     content: ' \25BE';
    141 }
    142 
    143 th.sortDown:after {
    144     content: ' \25B4';
    145 }
    146 
    147 td.comparison, td.result {
    148     text-align: right;
    149 }
    150 
    151 td.better {
    152     color: #6c6;
    153 }
    154 
    155 td.fadeOut {
    156     opacity: 0.5;
    157 }
    158 
    159 td.unknown {
    160     color: #ccc;
    161 }
    162 
    163 td.worse {
    164     color: #c66;
    165 }
    166 
    167 td.reference {
    168     font-style: italic;
    169     font-weight: bold;
    170     color: #444;
    171 }
    172 
    173 td.missing {
    174     color: #ccc;
    175     text-align: center;
    176 }
    177 
    178 td.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;">
    252 Result <span id="time-memory" class="checkbox"></span>
    253 Reference <span id="reference" class="checkbox"></span>
    254 Style <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>
    256 Run 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 
    265 var EXPANDED = true;
    266 var COLLAPSED = false;
    267 var SMALLEST_PERCENT_DISPLAYED = 0.01;
    268 var INVISIBLE = false;
    269 var VISIBLE = true;
    270 var COMPARISON_SUFFIX = '_compare';
    271 var SORT_DOWN_CLASS = 'sortDown';
    272 var SORT_UP_CLASS = 'sortUp';
    273 var BETTER_CLASS = 'better';
    274 var WORSE_CLASS = 'worse';
    275 var UNKNOWN_CLASS = 'unknown'
    276 // px Indentation for graphs
    277 var GRAPH_INDENT = 64;
    278 var PADDING_UNDER_GRAPH = 5;
    279 // px Indentation for nested children left-margins
    280 var INDENTATION = 40;
    281 
    282 function 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 
    348 function 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 
    367 function 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 
    431 function 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 }
    456 var undeleteManager = new UndeleteManager();
    457 
    458 var plotColor = 'rgb(230,50,50)';
    459 var subpointsPlotOptions = {
    460     lines: {show:true, lineWidth: 0},
    461     color: plotColor,
    462     points: {show: true, radius: 1},
    463     bars: {show: false}};
    464 
    465 var 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 
    484 var linePlotOptions = {
    485     yaxis: { show: false },
    486     xaxis: { show: false },
    487     lines: { show: true },
    488     grid: { borderWidth: 1, borderColor: '#ccc' },
    489     colors: [ plotColor ]
    490 };
    491 
    492 var 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 
    502 var histogramPlotOptions = {
    503     bars: {show: true, fill: 1}
    504 };
    505 
    506 function 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 
    556 function 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('
'
); 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 596 function 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 639 function 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 647 function 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: '&mu;', 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 671 function toFixedWidthPrecision(value) { 672 var decimal = value.toFixed(2); 673 return decimal; 674 } 675 676 function formatPercentage(fraction) { 677 var percentage = fraction * 100; 678 return (fraction * 100).toFixed(2) + '%'; 679 } 680 681 function 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 693 function 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 703 TestTypeSelector.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 748 var topLevelRows; 749 var allTableRows; 750 751 function 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 '&times;' + 763 '</div>' + 764 '</th>'; 765 if (index !== referenceIndex) { 766 header += '<th id="' + run.id() + COMPARISON_SUFFIX + '" ' + 767 'title="Sort by better/worse">' + 768 '&Delta;' + 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('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 899 function validForSorting(row) { 900 return ($.type(row.sortValue) === 'string') || !isNaN(row.sortValue); 901 } 902 903 var sortDirection = 1; 904 905 function 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 942 function 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 954 var activeSortHeaderElement = undefined; 955 var columnSortDirection = {}; 956 957 function 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 969 function 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 984 function sortByName(event) { 985 updateSortDirection(event.toElement); 986 987 allTableRows.forEach(function(row) { 988 row.prepareToSortByName(); 989 }); 990 991 buildTable(topLevelRows); 992 } 993 994 function sortByUnit(event) { 995 updateSortDirection(event.toElement); 996 997 allTableRows.forEach(function(row) { 998 row.prepareToSortByUnit(); 999 }); 1000 1001 buildTable(topLevelRows); 1002 } 1003 1004 function 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 1016 function 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 1031 function 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 1063 var warningSign = '' 1064 + '' 1065 + '' 1066 + '' 1067 + ''; 1068 1069 function 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 = $('' + 1077 '' + 1078 this.test.name() + 1079 '' + 1080 '' + 1081 this.test.unit() + 1082 '' + 1083 ''); 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 1125 TableRow.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 1136 TableRow.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 1151 TableRow.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 1182 TableRow.prototype.prepareToSortByUnit = function() { 1183 this.sortValue = this.test.unit().toLowerCase(); 1184 } 1185 1186 TableRow.prototype.prepareToSortByName = function() { 1187 this.sortValue = this.test.name().toLowerCase(); 1188 } 1189 1190 TableRow.prototype.isParentOf = function(row) { 1191 return this.hasNoURL && (this.testName === row.testName); 1192 } 1193 1194 TableRow.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 1213 TableRow.prototype.setVisibility = function(visibility) { 1214 this.visibility = visibility; 1215 this.tableRow[0].style.display = (visibility === INVISIBLE) ? 'none' : ''; 1216 } 1217 1218 TableRow.prototype.setRowNestedState = function(newState) { 1219 this.rowState = newState; 1220 this.updateIndentation(); 1221 } 1222 1223 TableRow.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 1236 TableRow.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 1252 TableRow.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 1262 TableRow.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 = 'className + '">' + comparisonText + ''; 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 = '&sigma;=' + 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 = '&plusmn; ' + 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 1324 TableRow.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 1331 TableRow.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 1368 TableRow.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 1387 TableRow.prototype.toggle = function() { 1388 if (this.rowState === EXPANDED) { 1389 this.closeRow(); 1390 } else { 1391 this.openRow(); 1392 } 1393 return false; 1394 } 1395 1396 function 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