Add a bisect button to automatically schedule bisecting A/B tasks.
[WebKit-https.git] / Websites / perf.webkit.org / public / index.html
1 <!DOCTYPE html>
2 <html>
3 <head>
4 <title>Perf Monitor is Loading...</title>
5 <script>
6
7 if (location.hash.indexOf('#mode=dashboard') >= 0 || !location.hash || location.hash == '#')
8     location.href = '/v3/';
9
10 </script>
11 <script src="js/jquery.js" defer></script>
12 <script src="js/jquery.flot.js" defer></script>
13 <script src="js/jquery.flot.crosshair.js" defer></script>
14 <script src="js/jquery.flot.fillbetween.js" defer></script>
15 <script src="js/jquery.flot.resize.js" defer></script>
16 <script src="js/jquery.flot.selection.js" defer></script>
17 <script src="js/jquery.flot.time.js" defer></script>
18 <script src="js/helper-classes.js" defer></script>
19 <script src="shared/statistics.js" defer></script>
20 <link rel="stylesheet" href="common.css">
21 <style type="text/css">
22
23 #numberOfDaysPicker {
24     color: #666;
25 }
26
27 #numberOfDaysPicker input {
28     height: 0.9em;
29     margin-right: 1em;
30 }
31
32 td, th {
33     border: none;
34     border-collapse: collapse;
35     padding-top: 0.5em;
36 }
37
38 #dashboard > tbody > tr > td {
39     vertical-align: top;
40 }
41
42 #dashboard > thead th {
43     text-shadow: #bbb 1px 1px 2px;
44     font-size: large;
45     font-weight: normal;
46 }
47
48 .chart {
49     position: relative;
50     border: solid 1px #ccc;
51     border-radius: 5px;
52     margin: 0px 0px 10px 0px;
53 }
54
55 .chart.worse {
56     background: #fcc;
57 }
58
59 .chart.better {
60     background: #cfc;
61 }
62
63 #charts .pane {
64     position: absolute;
65     left: 10px;
66     top: 10px;
67     width: 230px;
68 }
69
70 .chart header {
71     height: 3em;
72     display: table-cell;
73     vertical-align: middle;
74 }
75
76 #dashboard header {
77     padding: 0px 10px;
78 }
79
80 .chart h2, .chart h3 {
81     margin: 0 0 0.3em 0;
82     padding: 0;
83     font-size: 1em;
84     font-weight: normal;
85     word-break: break-all;
86 }
87
88 .chart h2 {
89     font-size: normal;
90 }
91
92 .chart h3 {
93     font-size: normal;
94 }
95
96 #dashboard .chart h3 {
97     display: none;
98 }
99
100 #dashboard .chart .status {
101     margin: 0px 10px;
102 }
103
104 .chart .status {
105     font-size: small;
106     color: #666;
107 }
108
109 .plot {
110     margin: 5px 5px 10px 5px;
111 }
112
113 .closeButton svg {
114     position: absolute;
115     left: 8px;
116     bottom: 8px;
117     width: 15px;
118     height: 15px;
119 }
120
121 #dashboard .chart {
122     width: 410px;
123 }
124
125 #dashboard .plot {
126     width: 400px;
127     height: 100px;
128     cursor: pointer;
129     cursor: hand;
130 }
131
132 #charts .plot {
133     height: 320px;
134     margin-left: 250px;
135 }
136
137 #dashboard .overviewPlot {
138     display: none;
139 }
140
141 #charts .overviewPlot {
142     margin: 10px 0px 0px 0px;
143     padding: 0;
144     height: 70px;
145 }
146
147 .chart .summaryTable {
148     font-size: small;
149     color: #666;
150     border: 0;
151 }
152
153 .chart .meta {
154     position: relative;
155 }
156
157 .chart .meta table,
158 .chart .meta td,
159 .tooltip table,
160 .tooltip td {
161     margin: 0;
162     padding: 0;
163 }
164
165 .chart .meta th,
166 .tooltip th {
167     margin: 0;
168     padding: 0 0.2em 0 0;
169     text-align: right;
170     font-weight: normal;
171 }
172
173 .chart .meta td:not(:first-child):before,
174 .tooltip td:not(:first-child):before {
175     content: ": ";
176 }
177
178 #dashboard .chart .summaryTable {
179     position: absolute;
180     right: 10px;
181     top: 0px;
182 }
183
184 #charts .summaryTable {
185     margin-top: 0.3em;
186 }
187
188 #dashboard .arrow {
189     width: 20px;
190     height: 40px;
191     position: absolute;
192     bottom: 50px;
193     left: 5px;
194 }
195
196 #charts .arrow {
197     width: 20px;
198     height: 40px;
199     position: absolute;
200     top: 10px;
201     left: 240px;
202 }
203
204 .chart svg {
205     stroke: #ccc;
206     fill: #ccc;
207     color: #ccc;
208 }
209
210 .chart.worse svg {
211     stroke: #c99;
212     fill: #c99;
213     color: #c99;
214 }
215
216 .chart.better svg {
217     stroke: #9c9;
218     fill: #9c9;
219     color: #9c9;
220 }
221
222 .chart .yaxistoggler {
223     position: absolute;
224     bottom: 10px;
225     left: 225px;
226     width: 10px;
227     height: 25px;
228 }
229
230 #dashboard  .yaxistoggler {
231     display: none;
232 }
233
234 .tooltip {
235     position: relative;
236     border-radius: 5px;
237     padding: 5px;
238     opacity: 0.8;
239     background: #333;
240     color: #eee;
241     font-size: small;
242     line-height: 130%;
243     white-space: nowrap;
244 }
245
246 .tooltip:before {
247     position: absolute;
248     width: 0;
249     height: 0;
250     left: 50%;
251     margin-left: -9px;
252     top: -19px;
253     content: "";
254     display: none;
255     border-style: solid;
256     border-width: 10px;
257     border-color: transparent transparent #333 transparent;
258 }
259
260 .tooltip.inverted:before {
261     display: block;
262 }
263
264 .tooltip.inverted:after {
265     display: none;
266 }
267
268 .tooltip:after {
269     position: absolute;
270     width: 0;
271     height: 0;
272     left: 50%;
273     margin-left: -9px;
274     bottom: -19px;
275     content: "";
276     display: block;
277     border-style: solid;
278     border-width: 10px;
279     border-color: #333 transparent transparent transparent;
280 }
281
282 .tooltip a {
283     text-decoration: underline;
284     color: #fff;
285     text-shadow: none;
286 }
287
288 .clickTooltip {
289     opacity: 0.6;
290 }
291
292 .hoverTooltip {
293     z-index: 99999;
294 }
295
296 #testPicker {
297     display: inline-block;
298     margin: 10px 0px;
299     border: solid 1px #ccc;
300     color: #666;
301     border-radius: 5px;
302     padding: 5px 8px;
303 }
304
305 </style>
306 <script>
307
308 (function () {
309     var charts = [];
310     var minTime;
311     var currentZoom;
312     var sharedPlotOptions = {
313         lines: { show: true, lineWidth: 1 },
314         xaxis: {
315             mode: "time",
316             timeformat: "%m/%d",
317             minTickSize: [1, 'day'],
318             max: Date.now(), // FIXME: This is likely broken for non-PST
319         },
320         yaxis: { tickLength: 0 },
321         series: { shadowSize: 0 },
322         points: { show: false },
323         grid: {
324             hoverable: true,
325             borderWidth: 1,
326             borderColor: '#999',
327             backgroundColor: '#fff',
328         }
329     };
330
331     function adjustedIntervalForRun(results, minTime, minRatioToFitInAdjustedInterval) {
332         if (!results)
333             return {min: Number.MAX_VALUE, max: Number.MIN_VALUE};
334         var degreeOfWeightingDecrease = 0.2;
335         var movingAverage = results.exponentialMovingArithmeticMean(minTime, degreeOfWeightingDecrease);
336         var resultsCount = results.countResults(minTime);
337         var adjustmentDelta = results.sampleStandardDeviation(minTime) / 4;
338         var adjustedMin = movingAverage;
339         var adjustedMax = movingAverage;
340         var adjustmentCount;
341         for (adjustmentCount = 0; adjustmentCount < 4 * 4; adjustmentCount++) { // Don't expand beyond 4 standard deviations.
342             adjustedMin -= adjustmentDelta;
343             adjustedMax += adjustmentDelta;
344             if (results.countResultsInInterval(minTime, adjustedMin, adjustedMax) / resultsCount >= minRatioToFitInAdjustedInterval)
345                 break;
346         }
347         for (var i = 0; i < adjustmentCount; i++) {
348             if (results.countResultsInInterval(minTime, adjustedMin + adjustmentDelta, adjustedMax) / resultsCount < minRatioToFitInAdjustedInterval)
349                 break;
350             adjustedMin += adjustmentDelta;
351         }
352         for (var i = 0; i < adjustmentCount; i++) {
353             if (results.countResultsInInterval(minTime, adjustedMin, adjustedMax - adjustmentDelta) / resultsCount < minRatioToFitInAdjustedInterval)
354                 break;
355             adjustedMax -= adjustmentDelta;
356         }
357         return {min: adjustedMin, max: adjustedMax};
358     }
359
360     function computeYAxisBoundsToFitLines(minTime, results, baseline, target) {
361         var minOfAllRuns = results.min(minTime);
362         var maxOfAllRuns = results.max(minTime);
363         if (baseline) {
364             minOfAllRuns = Math.min(minOfAllRuns, baseline.min(minTime));
365             maxOfAllRuns = Math.max(maxOfAllRuns, baseline.max(minTime));
366         }
367         if (target) {
368             minOfAllRuns = Math.min(minOfAllRuns, target.min(minTime));
369             maxOfAllRuns = Math.max(maxOfAllRuns, target.max(minTime));
370         }
371         var marginSize = (maxOfAllRuns - minOfAllRuns) * 0.1;
372
373         var minRatioToFitInAdjustedInterval = 0.9;
374         var intervalForResults = adjustedIntervalForRun(results, minTime, minRatioToFitInAdjustedInterval);
375         var intervalForBaseline = adjustedIntervalForRun(baseline, minTime, minRatioToFitInAdjustedInterval);
376         var intervalForTarget = adjustedIntervalForRun(target, minTime, minRatioToFitInAdjustedInterval);
377         var adjustedMin = Math.min(intervalForResults.min, intervalForBaseline.min, intervalForTarget.min);
378         var adjustedMax = Math.max(intervalForResults.max, intervalForBaseline.max, intervalForTarget.max);
379         var adjsutedMarginSize = (adjustedMax - adjustedMin) * 0.1;
380         return {min: minOfAllRuns - marginSize, max: maxOfAllRuns + marginSize,
381             adjustedMin: Math.max(minOfAllRuns - marginSize, adjustedMin - adjsutedMarginSize),
382             adjustedMax: Math.min(maxOfAllRuns + marginSize, adjustedMax + adjsutedMarginSize)};
383     }
384
385     function computeStatus(smallerIsBetter, lastResult, baseline, target) {
386         var relativeDifferenceWithBaseline = baseline ? lastResult.relativeDifference(baseline.lastResult()) : 0;
387         var relativeDifferenceWithTarget = target ? lastResult.relativeDifference(target.lastResult()) : 0;
388         var statusText = '';
389         var status = '';
390
391         if (relativeDifferenceWithBaseline && relativeDifferenceWithBaseline > 0 != smallerIsBetter) {
392             statusText = Math.abs(relativeDifferenceWithBaseline * 100).toFixed(2) + '% ' + (smallerIsBetter ? 'above' : 'below') + ' baseline';
393             status = 'worse';
394         } else if (relativeDifferenceWithTarget && relativeDifferenceWithTarget > 0 == smallerIsBetter) {
395             statusText = Math.abs(relativeDifferenceWithTarget * 100).toFixed(2) + '% ' + (smallerIsBetter ? 'below' : 'above') + ' target';
396             status = 'better';
397         } else if (relativeDifferenceWithTarget)
398             statusText = Math.abs(relativeDifferenceWithTarget * 100).toFixed(2) + '% until target';
399
400         return {class: status, text: statusText};
401     }
402
403     function addPlotDataForRun(plotData, name, runs, color, interactive) {
404         var entry = {color: color, data: runs.meanPlotData()};
405         if (!interactive) {
406             entry.clickable = false;
407             entry.hoverable = false;
408         }
409         if (runs.hasConfidenceInterval()) {
410             var lowerName = name.toLowerCase();
411             var confienceEntry = $.extend(true, {}, entry, {lines: {lineWidth: 0}, clickable: false, hoverable: false});
412             plotData.push($.extend(true, {}, confienceEntry, {id: lowerName, data: runs.upperConfidencePlotData()}));
413             plotData.push($.extend(true, {}, confienceEntry,
414                 {fillBetween: lowerName, lines: {fill: 0.3}, data: runs.lowerConfidencePlotData()}));
415         }
416         plotData.push(entry);
417     }
418
419     function createSummaryRowMarkup(name, runs) {
420         return '<tr><th>' + name + '</th><td>' + runs.lastResult().label() + '</td></tr>';
421     }
422
423     function buildLabelWithLinks(build, previousBuild) {
424         function linkifyIfNotNull(label, url) {
425             return url ? '<a href="' + url + '" target="_blank">' + label + '</a>' : label;
426         }
427
428         var formattedRevisions = build.formattedRevisions(previousBuild);
429         var buildInfo = {
430             'Commit': build.formattedTime(),
431             'Build': linkifyIfNotNull(build.buildNumber(), build.buildUrl()) + '(' + build.formattedBuildTime() + ')'
432         };
433         for (var repositoryName in formattedRevisions)
434             buildInfo[repositoryName] = linkifyIfNotNull(formattedRevisions[repositoryName].label, formattedRevisions[repositoryName].url);
435         var markup = '';
436         for (var key in buildInfo)
437             markup += '<tr><th>' + key + '</th><td>' + buildInfo[key] + '</td>';
438         return '<tr>' + markup + '</tr>';
439     }
440
441     function Chart(container, isDashboard, platform, metric, bugTrackers, onClose) {
442         var linkifiedFullName = metric.fullName;
443         if (metric.test.url)
444             linkifiedFullName = '<a href="' + metric.test.url + '">' + linkifiedFullName + '</a>';
445         var section = $('<section class="chart"><div class="pane"><header><h2>' + linkifiedFullName + '</h2>'
446             + '<h3 class="platform">' + platform.name + '</h3></header>'
447             + '<div class="meta"><table class="status"><tbody></tbody></table><table class="summaryTable"><tbody></tbody></table></div>'
448             + '<div class="overviewPlot"></div></div>'
449             + '<div class="plot"></div>'
450             + '<div class="unit"></div>'
451             + '<svg viewBox="0 0 20 100" class="arrow">'
452             + '<g stroke-width="10"><line x1="10" y1="8" x2="10" y2="92" />'
453             + '<polygon points="5,85 15,85 10,90" class="downwardArrowHead" />'
454             + '<polygon points="5,15 15,15 10,10" class="upwardArrowHead" />'
455             + '</g></svg>'
456             + '<a href="#" class="toggleYAxis"><svg viewBox="0 0 40 100" class="yaxistoggler"><g stroke-width="10">'
457             + '<line x1="20" y1="8" x2="20" y2="82" />'
458             + '<polygon points="15,15 25,15 20,10" />'
459             + '<polygon points="15,75 25,75 20,80" />'
460             + '<line x1="0" y1="92" x2="40" y2="92" />'
461             + '</g></svg></a>'
462             + '<a href="#" class="closeButton"><svg viewBox="0 0 100 100">'
463             + '<g stroke-width="10"><circle cx="50" cy="50" r="45" fill="transparent"/><polygon points="30,30 70,70" />'
464             + '<polygon points="30,70 70,30" /></g></svg></a></section>');
465
466         $(container).append(section);
467
468         var self = this;
469         if (onClose) {
470             section.find('.closeButton').bind('click', function (event) {
471                 event.preventDefault();
472                 section.remove();
473                 charts.splice(charts.indexOf(self), 1);
474                 onClose(self);
475                 return false;
476             });
477         } else
478             section.find('.closeButton').hide();
479
480         section.find('.yaxistoggler').bind('click', function (event) {
481             self.toggleYAxis();
482             event.preventDefault();
483             return false;
484         });
485
486         var plotData = [];
487         var results;
488         var baseline;
489         var target;
490
491         var tooltip = new Tooltip(container, 'tooltip hoverTooltip');
492         var bounds;
493         var plotContainer = section.find('.plot');
494         var mainPlot;
495         var overviewPlot;
496         var clickTooltips = [];
497         var shouldShowEntireYAxis = false;
498
499         this.platform = function () { return platform; }
500         this.metric = function () { return metric; }
501
502         this.populate = function (passedResults, passedBaseline, passedTarget) {
503             results = passedResults;
504             baseline = passedBaseline;
505             target = passedTarget;
506
507             var summaryRows = '';
508             if (target) {
509                 addPlotDataForRun(plotData, 'Target', target, '#039');
510                 summaryRows = createSummaryRowMarkup('Target', target) + summaryRows;
511             }
512             if (baseline) {
513                 addPlotDataForRun(plotData, 'Baseline', baseline, '#930');
514                 summaryRows = createSummaryRowMarkup('Baseline', baseline) + summaryRows;
515             }
516             addPlotDataForRun(plotData, 'Current', results, '#666', true);
517             summaryRows = createSummaryRowMarkup('Current', results) + summaryRows;
518
519             var status = computeStatus(results.smallerIsBetter(), results.lastResult(), baseline, target);
520             if (status.text)
521                 summaryRows = '<tr><td colspan="2">' + status.text + '</td></tr>' + summaryRows;
522             section.addClass(status.class);
523             section.find('.status tbody').html(buildLabelWithLinks(results.lastResult().build()));
524             section.find('.summaryTable tbody').html(summaryRows);
525             if (results.smallerIsBetter()) {
526                 section.find('.arrow .downwardArrowHead').show();
527                 section.find('.arrow .upwardArrowHead').hide();
528             } else {
529                 section.find('.arrow .downwardArrowHead').hide();
530                 section.find('.arrow .upwardArrowHead').show();
531             }
532         }
533
534         this.attachMainPlot = function (xMin, xMax) {
535             if (!bounds)
536                 return;
537
538             var mainPlotOptions = $.extend(true, {}, sharedPlotOptions, {
539                 xaxis: {
540                     min: xMin,
541                 },
542                 yaxis: {
543                     tickLength: null,
544                     min: shouldShowEntireYAxis ? 0 : bounds.adjustedMin,
545                     max: shouldShowEntireYAxis ? bounds.max : bounds.adjustedMax,
546                 },
547                 crosshair: {'mode': 'x', color: '#c90', lineWidth: 1},
548                 grid: {clickable: true},
549             });
550             if (xMax)
551                 mainPlotOptions.xaxis.max = xMax;
552             if (isDashboard) {
553                 mainPlotOptions.yaxis.labelWidth = 20;
554                 mainPlotOptions.yaxis.ticks = [mainPlotOptions.yaxis.min, mainPlotOptions.yaxis.max];
555                 mainPlotOptions.grid.autoHighlight = false;
556             } else {
557                 mainPlotOptions.yaxis.labelWidth = 30;
558                 mainPlotOptions.selection = {mode: "x"};
559                 plotData[plotData.length - 1].points = {show: true, radius: 1};
560             }
561             mainPlot = $.plot(plotContainer, plotData, mainPlotOptions);
562             plotData[plotData.length - 1].points = {};
563
564             for (var i = 0; i < clickTooltips.length; i++) {
565                 if (clickTooltips[i])
566                     clickTooltips[i].remove();
567             }
568             clickTooltips = [];
569         }
570
571         this.toggleYAxis = function () {
572             shouldShowEntireYAxis = !shouldShowEntireYAxis;
573             if (currentZoom)
574                 this.attachMainPlot(currentZoom.from, currentZoom.to);
575             else
576                 this.attachMainPlot(minTime);
577         }
578
579         this.zoom = function (from, to) {
580             this.attachMainPlot(from, to);
581             if (overviewPlot)
582                 overviewPlot.setSelection({xaxis: {from: from, to: to}}, true);
583         }
584
585         this.clearZoom = function () {
586             this.attachMainPlot(minTime);
587             if (overviewPlot)
588                 overviewPlot.clearSelection();
589         }
590         
591         this.setCrosshair = function (pos) {
592             if (mainPlot)
593                 mainPlot.setCrosshair(pos);
594         }
595
596         this.clearCrosshair = function () {
597             if (mainPlot)
598                 mainPlot.clearCrosshair();
599         }
600
601         this.hideTooltip = function() {
602             if (tooltip)
603                 tooltip.hide();
604         }
605
606         function toggleClickTooltip(index, pageX, pageY) {
607             if (clickTooltips[index])
608                 clickTooltips[index].toggle();
609             else {
610                 // FIXME: Put this on URLState.
611                 var newTooltip = new Tooltip(container, 'tooltip clickTooltip');
612                 showTooltipWithResults(newTooltip, pageX, pageY, results.resultAt(index), results.resultAt(index - 1));
613                 newTooltip.bindClick(function () { toggleClickTooltip(index, pageX, pageY); });
614                 newTooltip.bindMouseEnter(function () { tooltip.hide(); });
615                 clickTooltips[index] = newTooltip;
616             }
617             tooltip.hide();
618         }
619
620         function showTooltipWithResults(tooltip, x, y, result, resultToCompare) {
621             var newBugUrls = '';
622             if (resultToCompare) {
623                 var title = (resultToCompare.isBetterThan(result) ? 'REGRESSION: ' : '') + result.metric().fullName
624                     + ' got ' + result.formattedProgressionOrRegression(resultToCompare)
625                     + ' around ' + result.build().formattedTime();
626                 var revisions = result.build().formattedRevisions(resultToCompare.build());
627
628                 for (var trackerId in bugTrackers) {
629                     var tracker = bugTrackers[trackerId];
630                     var repositories = tracker.repositories;
631                     var description = 'Platform: ' + result.build().platform().name + '\n\n';
632                     for (var i = 0; i < repositories.length; ++i) {
633                         var repositoryName = repositories[i];
634                         var revision = revisions[repositoryName];
635                         if (!revision)
636                             continue;
637                         if (revision.url)
638                             description += repositoryName + ': ' + revision.url;
639                         else
640                             description += revision.label;
641                         description += '\n';
642                     }
643                     var url = tracker.newBugUrl
644                         .replace(/\$title/g, encodeURIComponent(title))
645                         .replace(/\$description/g, encodeURIComponent(description))
646                         .replace(/\$link/g, encodeURIComponent(location.href));
647                     if (newBugUrls)
648                         newBugUrls += ',';
649                     newBugUrls += ' <a href="' + url + '" target="_blank">' + tracker.name + '</a>';
650                 }
651                 newBugUrls = 'File:' + newBugUrls;
652             }
653             tooltip.show(x, y, result.label(resultToCompare) + '<table>'
654                 + buildLabelWithLinks(result.build(), resultToCompare ? resultToCompare.build() : null) + '</table>'
655                 + newBugUrls);
656         }
657
658         tooltip.bindClick(function () {
659             if (tooltip.currentItem)
660                 toggleClickTooltip(tooltip.currentItem.dataIndex, tooltip.currentItem.pageX, tooltip.currentItem.pageY);
661         });
662
663         function closestItemForPageXRespectingPlotOffset(item, plot, series, pageX) {
664             if (!series || !series.data.length)
665                 return null;
666
667             var offset = $(plotContainer).offset();
668             var points = series.datapoints.points;
669             var size = series.datapoints.pointsize;
670             var xInPlot = pageX - offset.left;
671
672             if (xInPlot < plot.getPlotOffset().left)
673                 return null;
674             if (item)
675                 return item;
676
677             var previousPoint;
678             var index = 0;
679             while (1) {
680                 var currentPoint = plot.pointOffset({x: points[index * size], y: points[index * size + 1]});
681                 if (xInPlot < currentPoint.left) {
682                     if (previousPoint && xInPlot < (previousPoint.left + currentPoint.left) / 2) {
683                         index -= 1;
684                         currentPoint = previousPoint;
685                     }
686                     break;
687                 }
688                 if (index + 1 >= series.data.length)
689                     break;
690                 previousPoint = currentPoint;
691                 index++;
692             }
693
694             // Ideally we want to return a real item object but flot doesn't provide an API to obtain one
695             // so create an object that contain properties we use.
696             return {dataIndex: index, pageX: offset.left + currentPoint.left, pageY: offset.top + currentPoint.top};
697         }
698
699         // Return a plot generator. This function is called when we change the number of days to show.
700         this.attach = function () {
701             if (!results)
702                 return;
703
704             bounds = computeYAxisBoundsToFitLines(minTime, results, baseline, target);
705
706             var overviewContainer = section.find('.overviewPlot');
707             if (!isDashboard) {
708                 overviewPlot = $.plot(overviewContainer, plotData,
709                     $.extend(true, {}, sharedPlotOptions, {
710                         xaxis: {
711                             min: minTime,
712                             // The maximum number of ticks we can fit on the overflow plot is 4.
713                             tickSize: [(TestBuild.now() - minTime) / 4 / 1000, 'second']
714                         },
715                         grid: { hoverable: false, clickable: false },
716                         selection: { mode: "x" },
717                         yaxis: {
718                             show: false,
719                             min: bounds.min,
720                             max: bounds.max,
721                         }
722                 }));
723
724                 $(plotContainer).bind("plotselected", function (event, ranges) { Chart.zoom(ranges.xaxis.from, ranges.xaxis.to); });
725                 $(overviewContainer).bind("plotselected", function (event, ranges) { Chart.zoom(ranges.xaxis.from, ranges.xaxis.to); });
726                 $(overviewContainer).bind("plotunselected", function () { Chart.clearZoom(minTime) });
727             }
728
729             if (currentZoom)
730                 this.zoom(currentZoom.from, currentZoom.to);
731             else
732                 this.attachMainPlot(minTime);
733
734             if (bindPlotEventHandlers)
735                 bindPlotEventHandlers(this);
736             bindPlotEventHandlers = null;
737         };
738
739         function bindPlotEventHandlers(chart) {
740             // FIXME: Crosshair should stay where it was between charts.
741             $(plotContainer).bind("plothover", function (event, pos, item) {
742                 for (var i = 0; i < charts.length; i++) {
743                     if (charts[i] !== chart) {
744                         charts[i].setCrosshair(pos);
745                         charts[i].hideTooltip();
746                     }
747                 }
748                 if (isDashboard)
749                     return;
750                 var data = mainPlot.getData();
751                 item = closestItemForPageXRespectingPlotOffset(item, mainPlot, data[data.length - 1], pos.pageX);
752                 if (!item)
753                     return;
754
755                 showTooltipWithResults(tooltip, item.pageX, item.pageY, results.resultAt(item.dataIndex), results.resultAt(item.dataIndex - 1));
756                 tooltip.currentItem = item;
757             });
758
759             $(plotContainer).bind("mouseleave", function (event) {
760                 var offset = $(plotContainer).offset();
761                 if (offset.left <= event.pageX && offset.top <= event.pageY && event.pageX <= offset.left + $(plotContainer).outerWidth()
762                     && event.pageY <= offset.top + $(plotContainer).outerHeight())
763                     return 0;
764                 for (var i = 0; i < charts.length; i++) {
765                     charts[i].clearCrosshair();
766                     charts[i].hideTooltip();
767                 }
768
769             });
770
771             if (isDashboard) { // FIXME: This code doesn't belong here.
772                 $(plotContainer).bind('click', function (event) {
773                     openChart(results.platform(), results.metric());
774                 });
775             } else {
776                 $(plotContainer).bind("plotclick", function (event, pos, item) {
777                     if (tooltip.currentItem)
778                         toggleClickTooltip(tooltip.currentItem.dataIndex, tooltip.currentItem.pageX, tooltip.currentItem.pageY);
779                 });
780             }
781         }
782
783         charts.push(this);
784     }
785
786     Chart.clear = function () {
787         charts = [];
788     }
789
790     Chart.setMinTime = function (newMinTime) {
791         minTime = newMinTime;
792         for (var i = 0; i < charts.length; i++)
793             charts[i].attach();
794     }
795
796     Chart.onzoomchange = function (from, to) { };
797
798     Chart.zoom = function (from, to) {
799         currentZoom = {from: from, to: to};
800         for (var i = 0; i < charts.length; i++)
801             charts[i].zoom(from, to);
802         this.onzoomchange(from, to);
803     }
804
805     Chart.clearZoom = function (minTime) {
806         currentZoom = undefined;
807         for (var i = 0; i < charts.length; i++)
808             charts[i].clearZoom();
809         this.onzoomchange();
810     }
811
812     window.Chart = Chart;
813 })();
814
815 // FIXME: We need to devise a way to fetch runs in multiple chunks so that
816 // we don't have to fetch the entire time series to just show the last 3 days.
817 // FIXME: We should do a mass-fetch where we fetch JSONs for multiple runs at once.
818 function fetchTest(repositories, builders, filename, platform, metric, callback) {
819
820     function createRunAndResults(rawRuns) {
821         if (!rawRuns)
822             return null;
823
824         var runs = new PerfTestRuns(metric, platform);
825         var results = rawRuns.map(function (rawRun) {
826             // FIXME: Creating PerfTestResult and keeping them alive in memory all the time seems like a terrible idea.
827             // We should create PerfTestResult on demand.
828             return new PerfTestResult(runs, rawRun, new TestBuild(repositories, builders, platform, rawRun));
829         });
830         runs.setResults(results.sort(function (a, b) { return a.build().time() - b.build().time(); }));
831         return runs;
832     }
833
834     $.getJSON('api/runs/' + filename + '?cache=true', function (response) {
835         var data = response.configurations;
836         callback(createRunAndResults(data.current), createRunAndResults(data.baseline), createRunAndResults(data.target));
837     });
838 }
839
840 function fileNameFromPlatformAndTest(platformId, testId) {
841     return platformId + '-' + testId + '.json';
842 }
843
844 function init() {
845     var allPlatforms;
846     var tests = [];
847     var fullNameToMetric;
848     var dashboardPlatforms;
849     var runsCache = {}; // FIXME: We need to clear this cache at some point.
850     var repositories;
851     var builders;
852     var bugTrackers;
853
854     // FIXME: Show some error message when we get 404.
855     function getOrFetchTest(platform, metric, callback) {
856         var filename = fileNameFromPlatformAndTest(platform.id, metric.id);
857         var caches = runsCache[filename];
858
859         if (caches)
860             setTimeout(function () { callback(caches.current, caches.baseline, caches.target); }, 0);
861         else {
862             fetchTest(repositories, builders, filename, platform, metric, function (current, baseline, target) {
863                 runsCache[filename] = {current:current, baseline:baseline, target:target};
864                 callback(current, baseline, target);
865             });
866         }
867     }
868
869     function showDashboard() {
870         var dashboardTable = $('<table id="dashboard"></table>');
871         $('#mainContents').html(dashboardTable);
872
873         // Split dashboard platforms into groups of three so that it doesn't grow horizontally too much.
874         // FIXME: Make this adaptive.
875         for (var i = 0; i < Math.ceil(dashboardPlatforms.length / 3); i++)
876             addPlatformsToDashboard(dashboardTable, dashboardPlatforms.slice(i * 3, (i + 1) * 3));
877
878         URLState.remove('chartList');
879     }
880
881     function addPlatformsToDashboard(dashboardTable, selectedPlatforms) {
882         var header = document.createElement('thead');
883         selectedPlatforms.forEach(function (platform) {
884             var cell = document.createElement('th');
885             $(cell).text(platform.name);
886             header.appendChild(cell);
887         });
888         dashboardTable.append(header);
889
890         var tbody = $(document.createElement('tbody'));
891         dashboardTable.append(tbody);
892         var row = document.createElement('tr');
893         tbody.append(row);
894
895         selectedPlatforms.forEach(function (platform) {
896             var cell = document.createElement('td');
897             row.appendChild(cell);
898
899             platform.metrics.forEach(function (metric) {
900                 var chart = new Chart(cell, true, platform, metric, bugTrackers);
901                 getOrFetchTest(platform, metric, function (results, baseline, target) {
902                     // FIXME: We shouldn't rely on the order in which XHR finishes to order plots.
903                     if (dashboardTable.parent().length) {
904                         chart.populate(results, baseline, target);
905                         chart.attach();
906                     }
907                 });
908             });
909         });
910     }
911
912     function showCharts(lists) {
913         var chartsContainer = document.createElement('section');
914         chartsContainer.id = 'charts';
915         $('#mainContents').html(chartsContainer);
916
917         var testPicker = document.createElement('section');
918         testPicker.id = 'testPicker';
919
920         function addOption(select, label, value) {
921             var option = document.createElement('option');
922             option.appendChild(document.createTextNode(label));
923             if (value)
924                 option.value = value;
925             select.appendChild(option);
926         }
927
928         var testList = document.createElement('select');
929         testList.id = 'testList';
930         testPicker.appendChild(testList);
931         for (var i = 0; i < tests.length; ++i) {
932             if (tests[i].parentTest)
933                 continue;
934             addOption(testList, tests[i].fullName, tests[i].id);
935         }
936
937         var metricList = document.createElement('select');
938         metricList.id = 'metricList';
939         testPicker.appendChild(metricList);
940
941         var platformList = document.createElement('select');
942         platformList.id = 'platformList';
943         testPicker.appendChild(platformList);
944         const OPTION_VALUE_FOR_ALL = '-';
945         addOption(platformList, 'All platforms', OPTION_VALUE_FOR_ALL);
946         for (var i = 0; i < allPlatforms.length; ++i)
947             addOption(platformList, allPlatforms[i].name);
948
949         testList.onchange = function () {
950             while (metricList.firstChild)
951                 metricList.removeChild(metricList.firstChild);
952
953             var metricsGroup = document.createElement('optgroup');
954             metricsGroup.label = 'Metrics';
955             metricList.appendChild(metricsGroup);
956             addOption(metricsGroup, 'All metrics', OPTION_VALUE_FOR_ALL);
957             for (var i = 0; i < tests.length; ++i) {
958                 if (tests[i].id == testList.value) {
959                     var selectedTest = tests[i];
960                     for (var j = 0; j < selectedTest.metrics.length; ++j) {
961                         var fullName = selectedTest.metrics[j].fullName;
962                         var relativeName = fullName.replace(selectedTest.fullName, '').replace(/^[:/]/, '');
963                         addOption(metricsGroup, relativeName, fullName);
964                     }
965                 }
966             }
967             var subtestsGroup = document.createElement('optgroup');
968             subtestsGroup.label = 'Tests';
969             metricList.appendChild(subtestsGroup);
970             addOption(subtestsGroup, 'All subtests', OPTION_VALUE_FOR_ALL);
971             for (var i = 0; i < tests.length; ++i) {
972                 if (!tests[i].parentTest || tests[i].parentTest.id != testList.value)
973                     continue;
974                 var subtest = tests[i];
975                 var selectedTest = subtest.parentTest;
976                 for (var j = 0; j < subtest.metrics.length; ++j) {
977                     var fullName = subtest.metrics[j].fullName;
978                     var relativeName = fullName.replace(selectedTest.fullName, '').replace(/^[:/]/, '');
979                     addOption(subtestsGroup, relativeName, fullName);
980                 }
981             }
982         }
983         metricList.onchange = function () {
984             var metric = fullNameToMetric[metricList.value];
985             var shouldAddAllMetrics = metricList.value === OPTION_VALUE_FOR_ALL;
986             for (var i = 0; i < platformList.options.length; ++i) {
987                 var option = platformList.options[i];
988                 if (option.value === OPTION_VALUE_FOR_ALL) // Adding all metrics for all platforms will be too slow.
989                     option.disabled = shouldAddAllMetrics;
990                 else {
991                     var platform = nameToPlatform[option.value];
992                     var platformHasMetric = platform.metrics.indexOf(metric) >= 0;
993                     option.disabled = !shouldAddAllMetrics && !platformHasMetric;
994                 }
995             }
996         }
997         testList.onchange();
998         metricList.onchange();
999
1000         $(testPicker).append(' <a href="">Add Chart</a>');
1001
1002         function removeChart(chart) {
1003             for (var i = 0; i < chartList.length; i++) {
1004                 if (chartList[i][0] == chart.platform().name && chartList[i][1] == chart.metric().fullName) {
1005                     chartList.splice(i, 1);
1006                     break;
1007                 }
1008             }
1009             URLState.set('chartList', JSON.stringify(chartList));
1010         }
1011
1012         function createChartFromListPair(platformName, metricFullName) {
1013             var platform = nameToPlatform[platformName];
1014             var metric = fullNameToMetric[metricFullName]
1015             var chart = new Chart(chartsContainer, false, platform, metric, bugTrackers, removeChart);
1016
1017             getOrFetchTest(platform, metric, function (results, baseline, target) {
1018                 if (!chartsContainer.parentNode)
1019                     return;
1020                 chart.populate(results, baseline, target);
1021                 chart.attach();
1022             });
1023         }
1024
1025         $(testPicker).children('a').bind('click', function (event) {
1026             event.preventDefault();
1027
1028             var newChartList = [];
1029             if (platformList.value === OPTION_VALUE_FOR_ALL) {
1030                 for (var i = 0; i < allPlatforms.length; ++i) {
1031                     createChartFromListPair(allPlatforms[i].name, metricList.value);
1032                     newChartList.push([allPlatforms[i].name, metricList.value]);
1033                 }
1034             } else if (metricList.value === OPTION_VALUE_FOR_ALL) {
1035                 var group = metricList.selectedOptions[0].parentNode;
1036                 var metricsToAdd = [];
1037                 for (var i = 0; i < group.children.length; i++) {
1038                     var metric = group.children[i].value;
1039                     if (metric == OPTION_VALUE_FOR_ALL)
1040                         continue;
1041                     createChartFromListPair(platformList.value, metric);
1042                     newChartList.push([platformList.value, metric]);
1043                 }
1044             } else {
1045                 createChartFromListPair(platformList.value, metricList.value);
1046                 newChartList.push([platformList.value, metricList.value]);
1047             }
1048
1049             chartList = chartList.concat(newChartList);
1050             URLState.set('chartList', JSON.stringify(chartList));
1051
1052             return false;
1053         });
1054
1055         $('#mainContents').append(testPicker);
1056
1057         var chartList = [];
1058         try {
1059             chartList = JSON.parse(URLState.get('chartList', ''));
1060             // FIXME: Should we verify that platform and test names are valid here?
1061         } catch (exception) {
1062             // Ignore any exception thrown by parse.
1063         }
1064
1065         redirectChartsToV3(chartList);
1066
1067         chartList.forEach(function (item) { createChartFromListPair(item[0], item[1]); });
1068     }
1069
1070     function redirectChartsToV3(chartList)
1071     {
1072         var v3URLParams = [];
1073         var numberOfDays = URLState.get('days');
1074         if (parseInt(numberOfDays) == numberOfDays)
1075             v3URLParams.push('since=' + (+Date.now() - numberOfDays * 24 * 3600 * 1000));
1076
1077         var v3PaneList = [];
1078         for (var item of chartList) {
1079             var platform = nameToPlatform[item[0]];
1080             var metric = fullNameToMetric[item[1]];
1081             if (platform && metric)
1082                 v3PaneList.push(`(${platform.id}-${metric.id})`);
1083         }
1084         v3URLParams.push(`paneList=(${v3PaneList.join('-')})`);
1085
1086         try {
1087             var zoomValues = JSON.parse(URLState.get('zoom', '[]'));
1088             if (zoomValues.length == 2)
1089                 v3URLParams.push(`zoom=(${zoomValues[0]}-${zoomValues[1]})`);
1090         } catch (error) { }
1091
1092         location.href = '/v3/#/charts?' + v3URLParams.join('&');
1093     }
1094
1095     // FIXME: We should use exponential slider for charts page where we expect to have
1096     // the full JSON as opposed to the dashboard where we can't afford loading really big JSON files.
1097     var exponential = true;
1098     (function () {
1099         var input = $('#numberOfDays')[0];
1100         var updating = 0;
1101         function updateSpanAndCall(newNumberOfDays) {
1102             $('#numberOfDays').next().text(newNumberOfDays + ' days');
1103             // FIXME: This is likely broken for non-PST.
1104             Chart.setMinTime(Date.now() - newNumberOfDays * 24 * 3600 * 1000);
1105         }
1106         $('#numberOfDays').bind('change', function () {
1107             var newNumberOfDays = Math.round(exponential ? Math.exp(input.value) : input.value);
1108             URLState.remove('zoom');
1109             Chart.clearZoom();
1110             URLState.set('days', newNumberOfDays);
1111             updateSpanAndCall(newNumberOfDays);
1112         });
1113         function onchange() {
1114             var newNumberOfDays = URLState.get('days', Math.round(exponential ? Math.exp(input.defaultValue) : input.defaultValue));
1115             $('#numberOfDays').val(exponential ? Math.log(newNumberOfDays) : newNumberOfDays);
1116             updateSpanAndCall(newNumberOfDays);
1117         }
1118         URLState.watch('days', onchange);
1119         onchange();
1120     })();
1121
1122     URLState.watch('mode', function () { setMode(URLState.get('mode')); });
1123     URLState.watch('chartList', function (changedStates) {
1124         var modeChanged = changedStates.indexOf('mode') >= 0;
1125         if (URLState.get('mode') == 'charts' && !modeChanged)
1126             setMode('charts');
1127     });
1128
1129     function zoomChartsIfParsedCorrectly() {
1130         try {
1131             zoomValues = JSON.parse(URLState.get('zoom', '[]'));
1132             if (zoomValues.length != 2)
1133                 return false;
1134             Chart.zoom(parseFloat(zoomValues[0]), parseFloat(zoomValues[1]));
1135         } catch (exception) {
1136             // Ignore all exceptions thrown by JSON.parse.
1137         }
1138         return true;
1139     }
1140
1141     URLState.watch('zoom', function (changedStates) {
1142         var modeChanged = changedStates.indexOf('mode') >= 0;
1143         if (URLState.get('mode') == 'charts' && !modeChanged) {
1144             if (!zoomChartsIfParsedCorrectly())
1145                 return Chart.clearZoom();
1146         }
1147     });
1148
1149     Chart.onzoomchange = function (from, to) {
1150         if (from && to)
1151             URLState.set('zoom', JSON.stringify([from, to]));
1152         else
1153             URLState.remove('zoom');
1154     }
1155
1156     window.openChart = function (platform, metric) {
1157         URLState.set('chartList', JSON.stringify([[platform.name, metric.fullName]]));
1158         setMode('charts');
1159     }
1160
1161     window.setMode = function (newMode) {
1162         URLState.set('mode', newMode);
1163
1164         Chart.clear();
1165         if (newMode == 'dashboard') {
1166             URLState.remove('zoom');
1167             Chart.clearZoom();
1168             showDashboard();
1169         } else { // FIXME: Dynamically obtain the list of tests to show.
1170             showCharts();
1171             zoomChartsIfParsedCorrectly();
1172         }
1173     }
1174
1175     function fullName(test) {
1176         var names = [];
1177         do {
1178             names.push(test.name);
1179             test = test.parentTest;
1180         } while (test);
1181         names.reverse();
1182         return names.join('/');
1183     }
1184
1185     // Perhaps we want an ordered list of platforms.
1186     $.getJSON('data/manifest.json', function (data) {
1187         var manifest = data;
1188
1189         nameToTest = {};
1190         for (var testId in manifest.tests) {
1191             var test = manifest.tests[testId];
1192             test.parentTest = manifest.tests[manifest.tests[testId].parentId];
1193             test.id = testId;
1194             test.metrics = [];
1195             tests.push(test);
1196         }
1197         tests.forEach(function (test) {
1198             test.fullName = fullName(test);
1199             nameToTest[test.fullName] = test;
1200         });
1201         tests.sort(function (a, b) {
1202             if (a.fullName < b.fullName)
1203                 return -1;
1204             if (a.fullName > b.fullName)
1205                 return 1;
1206             return 0;
1207         });
1208
1209         fullNameToMetric = {};
1210         for (var metricId in manifest.metrics) {
1211             var entry = manifest.metrics[metricId];
1212             entry.id = metricId;
1213             entry.test = manifest.tests[entry.test];
1214             entry.fullName = entry.test.fullName + ':' + entry.name;
1215             if (entry.aggregator)
1216                 entry.fullName += ':' + entry.aggregator;
1217             entry.test.metrics.push(entry);
1218             fullNameToMetric[entry.fullName] = entry;
1219         }
1220
1221         tests.forEach(function (test) {
1222             test.metrics.sort(function (a, b) {
1223                 if (a.name < b.name)
1224                     return -1;
1225                 if (a.name > b.name)
1226                     return 1;
1227                 return 0;
1228             });
1229         });
1230
1231         allPlatforms = [];
1232         nameToPlatform = {};
1233         for (var platformId in manifest.all) {
1234             var platform = manifest.all[platformId];
1235             platform.id = platformId;
1236             allPlatforms.push(platform);
1237             nameToPlatform[platform.name] = platform;
1238             // FIXME: Sort tests
1239             for (var i = 0; i < platform.metrics.length; ++i)
1240                 platform.metrics[i] = manifest.metrics[platform.metrics[i]];
1241         }
1242         allPlatforms.sort(function (a, b) { return a.name < b.name ? -1 : (a.name > b.name ? 1 : 0); });
1243
1244         dashboardPlatforms = [];
1245         for (var platformId in manifest.dashboard) {
1246             var platform = manifest.dashboard[platformId];
1247             platform.id = platformId;
1248             for (var i = 0; i < platform.metrics.length; ++i)
1249                 platform.metrics[i] = manifest.metrics[platform.metrics[i]];
1250             platform.metrics.sort(function (a, b) { return a.fullName < b.fullName ? -1 : (a.fullName > b.fullName ? 1 : 0); });
1251             dashboardPlatforms.push(manifest.dashboard[platformId]);
1252         }
1253         dashboardPlatforms.sort(function (a, b) { return a.name < b.name ? -1 : (a.name > b.name ? 1 : 0); });
1254
1255         repositories = manifest.repositories;
1256         builders = manifest.builders;
1257         bugTrackers = manifest.bugTrackers;
1258
1259         document.title = manifest.siteTitle;
1260         document.getElementById('siteTitle').textContent = manifest.siteTitle;
1261
1262         setMode(URLState.get('mode', 'dashboard'));
1263     });
1264 }
1265
1266 window.addEventListener('DOMContentLoaded', init, false);
1267
1268 </script>
1269 </head>
1270 <body>
1271
1272 <header id="title">
1273 <h1><a id="siteTitle" href="/">Perf Monitor</a></h1>
1274 <ul>
1275     <li id="numberOfDaysPicker"><input id="numberOfDays" type="range" min="1" max="5.9" step="0.001" value="2.3"><span class="output"></span></li>
1276     <li><a href="javascript:setMode('dashboard');">Dashboard</a></li>
1277     <li><a href="javascript:setMode('charts');">Charts</a></li>
1278 </ul>
1279 </header>
1280
1281 <div id="mainContents"></div>
1282 </body>
1283 </html>