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