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