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