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: '&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
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                          '&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('<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 = '&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
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