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