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