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