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">
19 display: inline-block;
21 border: solid 1px #ccc;
27 #numberOfDaysPicker input {
34 border-collapse: collapse;
37 #dashboard > tbody > tr > td {
41 #dashboard > thead th {
43 text-shadow: #bbb 1px 1px 2px;
54 border: solid 1px #ccc;
56 margin: 10px 10px 0px 0px;
77 vertical-align: middle;
85 .chart h2, .chart h3 {
91 word-break: break-all;
102 #dashboard .chart h3 {
112 margin: 5px 5px 10px 5px;
135 #dashboard .overviewPlot {
139 #charts .overviewPlot {
140 margin: 10px 0px 0px 0px;
151 #dashboard .summaryTable {
205 .chart .yaxistoggler {
213 #dashboard .yaxistoggler {
240 border-color: #333 transparent transparent transparent;
244 text-decoration: underline;
258 display: inline-block;
260 border: solid 1px #ccc;
266 #numberOfDaysPicker {
267 display: inline-block;
269 border: solid 1px #ccc;
275 #numberOfDaysPicker input {
287 var sharedPlotOptions = {
288 lines: { show: true, lineWidth: 1 },
292 minTickSize: [1, 'day'],
293 max: Date.now(), // FIXME: This is likely broken for non-PST
295 yaxis: { tickLength: 0 },
296 series: { shadowSize: 0 },
297 points: { show: false },
302 backgroundColor: '#fff',
306 function computeYAxisBoundsToFitLines(minTime, results, baseline, target) {
307 var stdevOfAllRuns = results.sampleStandardDeviation(minTime);
308 var movingAverage = results.exponentialMovingArithmeticMean(minTime, /* alpha, the degree of weighting decrease */ 0.3);
309 var min = results.min(minTime);
310 var max = results.max(minTime);
313 min = Math.min(min, baseline.min(minTime));
314 max = Math.max(max, baseline.max(minTime));
317 min = Math.min(min, target.min(minTime));
318 max = Math.max(max, target.max(minTime));
321 var marginSize = (max - min) * 0.1;
322 return {min: min - marginSize, max: max + marginSize,
323 adjustedMin: Math.min(results.lastResult().mean() - marginSize, Math.max(movingAverage - stdevOfAllRuns * 2, min) - marginSize),
324 adjustedMax: Math.max(results.lastResult().mean() + marginSize, Math.min(movingAverage + stdevOfAllRuns * 2, max) + marginSize) };
327 function computeStatus(smallerIsBetter, lastResult, baseline, target) {
328 var relativeDifferenceWithBaseline = baseline ? lastResult.relativeDifference(baseline.lastResult()) : 0;
329 var relativeDifferenceWithTarget = target ? lastResult.relativeDifference(target.lastResult()) : 0;
333 if (relativeDifferenceWithBaseline && relativeDifferenceWithBaseline > 0 != smallerIsBetter) {
334 statusText = Math.abs(relativeDifferenceWithBaseline * 100).toFixed(2) + '% ' + (smallerIsBetter ? 'above' : 'below') + ' baseline';
336 } else if (relativeDifferenceWithTarget && relativeDifferenceWithTarget > 0 == smallerIsBetter) {
337 statusText = Math.abs(relativeDifferenceWithTarget * 100).toFixed(2) + '% ' + (smallerIsBetter ? 'below' : 'above') + ' target';
339 } else if (relativeDifferenceWithTarget)
340 statusText = Math.abs(relativeDifferenceWithTarget * 100).toFixed(2) + '% until target';
343 statusText += '<br>';
344 statusText += buildLabelWithLinks(lastResult.build());
346 return {class: status, text: statusText};
349 function addPlotDataForRun(plotData, name, runs, color, interactive) {
350 var entry = {color: color, data: runs.meanPlotData()};
352 entry.clickable = false;
353 entry.hoverable = false;
355 if (runs.hasConfidenceInterval()) {
356 var lowerName = name.toLowerCase();
357 var confienceEntry = $.extend(true, {}, entry, {lines: {lineWidth: 0}, clickable: false, hoverable: false});
358 plotData.push($.extend(true, {}, confienceEntry, {id: lowerName, data: runs.upperConfidencePlotData()}));
359 plotData.push($.extend(true, {}, confienceEntry,
360 {fillBetween: lowerName, lines: {fill: 0.3}, data: runs.lowerConfidencePlotData()}));
362 plotData.push(entry);
365 function createSummaryRowMarkup(name, runs) {
366 return '<tr><td>' + name + '</td><td>' + runs.lastResult().label() + '</td></tr>';
369 function buildLabelWithLinks(build, previousBuild) {
370 function linkifyIfNotNull(label, url) {
371 return url ? '<a href="' + url + '" target="_blank">' + label + '</a>' : label;
374 var formattedRevisions = build.formattedRevisions(previousBuild);
375 var markup = ['Committed: ' + build.formattedTime(),
376 'Build: ' + linkifyIfNotNull(build.buildNumber(), build.buildUrl()) + ' (' + build.formattedBuildTime() + ')'];
377 for (var repositoryName in formattedRevisions)
378 markup.push(linkifyIfNotNull(formattedRevisions[repositoryName].label, formattedRevisions[repositoryName].url));
379 return markup.join('<br>');
382 function Chart(container, isDashboard, platform, metric, bugTrackers, onClose) {
383 var linkifiedFullName = metric.fullName;
385 linkifiedFullName = '<a href="' + metric.test.url + '">' + linkifiedFullName + '</a>';
386 var section = $('<section class="chart"><div class="pane"><header><h2>' + linkifiedFullName + '</h2>'
387 + '<h3 class="platform">' + platform.name + '</h3>'
388 + '<span class="status"></span></header>'
389 + '<table class="summaryTable"><tbody></tbody></table>'
390 + '<div class="overviewPlot"></div></div>'
391 + '<div class="plot"></div>'
392 + '<div class="unit"></div>'
393 + '<svg viewBox="0 0 20 100" class="arrow">'
394 + '<g stroke-width="10"><line x1="10" y1="8" x2="10" y2="92" />'
395 + '<polygon points="5,85 15,85 10,90" class="downwardArrowHead" />'
396 + '<polygon points="5,15 15,15 10,10" class="upwardArrowHead" />'
398 + '<a href="#" class="toggleYAxis"><svg viewBox="0 0 40 100" class="yaxistoggler"><g stroke-width="10">'
399 + '<line x1="20" y1="8" x2="20" y2="82" />'
400 + '<polygon points="15,15 25,15 20,10" />'
401 + '<polygon points="15,75 25,75 20,80" />'
402 + '<line x1="0" y1="92" x2="40" y2="92" />'
404 + '<a href="#" class="closeButton"><svg viewBox="0 0 100 100">'
405 + '<g stroke-width="10"><circle cx="50" cy="50" r="45" fill="transparent"/><polygon points="30,30 70,70" />'
406 + '<polygon points="30,70 70,30" /></g></svg></a></section>');
408 $(container).append(section);
412 section.find('.closeButton').bind('click', function (event) {
413 event.preventDefault();
415 charts.splice(charts.indexOf(self), 1);
420 section.find('.closeButton').hide();
422 section.find('.yaxistoggler').bind('click', function (event) {
424 event.preventDefault();
433 var tooltip = new Tooltip(container, 'tooltip hoverTooltip');
435 var plotContainer = section.find('.plot');
438 var clickTooltips = [];
439 var shouldShowEntireYAxis = false;
441 this.platform = function () { return platform; }
442 this.metric = function () { return metric; }
444 this.populate = function (passedResults, passedBaseline, passedTarget) {
445 results = passedResults;
446 baseline = passedBaseline;
447 target = passedTarget;
449 var summaryRows = '';
451 addPlotDataForRun(plotData, 'Target', target, '#039');
452 summaryRows = createSummaryRowMarkup('Target', target) + summaryRows;
455 addPlotDataForRun(plotData, 'Baseline', baseline, '#930');
456 summaryRows = createSummaryRowMarkup('Baseline', baseline) + summaryRows;
458 addPlotDataForRun(plotData, 'Current', results, '#666', true);
459 summaryRows = createSummaryRowMarkup('Current', results) + summaryRows;
461 var status = computeStatus(results.smallerIsBetter(), results.lastResult(), baseline, target);
462 section.addClass(status.class);
463 section.find('.status').html(status.text);
464 section.find('.summaryTable tbody').html(summaryRows);
465 section.find('.unit').html(results.unit());
466 if (results.smallerIsBetter()) {
467 section.find('.arrow .downwardArrowHead').show();
468 section.find('.arrow .upwardArrowHead').hide();
470 section.find('.arrow .downwardArrowHead').hide();
471 section.find('.arrow .upwardArrowHead').show();
475 this.attachMainPlot = function (xMin, xMax) {
479 var mainPlotOptions = $.extend(true, {}, sharedPlotOptions, {
485 min: shouldShowEntireYAxis ? 0 : bounds.adjustedMin,
486 max: shouldShowEntireYAxis ? bounds.max : bounds.adjustedMax,
488 crosshair: {'mode': 'x', color: '#c90', lineWidth: 1},
489 grid: {clickable: true},
492 mainPlotOptions.xaxis.max = xMax;
494 mainPlotOptions.yaxis.labelWidth = 20;
495 mainPlotOptions.yaxis.ticks = [mainPlotOptions.yaxis.min, mainPlotOptions.yaxis.max];
496 mainPlotOptions.grid.autoHighlight = false;
498 mainPlotOptions.yaxis.labelWidth = 30;
499 mainPlotOptions.selection = {mode: "x"};
500 plotData[plotData.length - 1].points = {show: true, radius: 1};
502 mainPlot = $.plot(plotContainer, plotData, mainPlotOptions);
503 plotData[plotData.length - 1].points = {};
505 for (var i = 0; i < clickTooltips.length; i++) {
506 if (clickTooltips[i])
507 clickTooltips[i].remove();
512 this.toggleYAxis = function () {
513 shouldShowEntireYAxis = !shouldShowEntireYAxis;
515 this.attachMainPlot(currentZoom.from, currentZoom.to);
517 this.attachMainPlot(minTime);
520 this.zoom = function (from, to) {
521 this.attachMainPlot(from, to);
523 overviewPlot.setSelection({xaxis: {from: from, to: to}}, true);
526 this.clearZoom = function () {
527 this.attachMainPlot(minTime);
529 overviewPlot.clearSelection();
532 this.setCrosshair = function (pos) {
534 mainPlot.setCrosshair(pos);
537 this.clearCrosshair = function () {
539 mainPlot.clearCrosshair();
542 this.hideTooltip = function() {
547 function toggleClickTooltip(index, pageX, pageY) {
548 if (clickTooltips[index])
549 clickTooltips[index].toggle();
551 // FIXME: Put this on URLState.
552 var newTooltip = new Tooltip(container, 'tooltip clickTooltip');
553 showTooltipWithResults(newTooltip, pageX, pageY, results.resultAt(index), results.resultAt(index - 1));
554 newTooltip.bindClick(function () { toggleClickTooltip(index, pageX, pageY); });
555 newTooltip.bindMouseEnter(function () { tooltip.hide(); });
556 clickTooltips[index] = newTooltip;
561 function showTooltipWithResults(tooltip, x, y, result, resultToCompare) {
563 if (resultToCompare) {
564 var title = (resultToCompare.isBetterThan(result) ? 'REGRESSION: ' : '') + result.metric().fullName
565 + ' got ' + result.formattedProgressionOrRegression(resultToCompare)
566 + ' around ' + result.build().formattedTime();
567 var revisions = result.build().formattedRevisions(resultToCompare.build());
569 for (var trackerName in bugTrackers) {
570 var repositories = bugTrackers[trackerName].repositories;
571 var description = 'Platform: ' + result.build().platform().name + '\n\n';
572 for (var i = 0; i < repositories.length; ++i) {
573 var repositoryName = repositories[i];
574 var revision = revisions[repositoryName];
578 description += repositoryName + ': ' + revision.url;
580 description += revision.label;
583 var url = bugTrackers[trackerName].newBugUrl
584 .replace(/\$title/g, encodeURIComponent(title))
585 .replace(/\$description/g, encodeURIComponent(description))
586 .replace(/\$link/g, encodeURIComponent(location.href));
589 newBugUrls += ' <a href="' + url + '" target="_blank">' + trackerName + '</a>';
591 newBugUrls = 'File:' + newBugUrls;
593 tooltip.show(x, y, result.label(resultToCompare) + '<br>'
594 + buildLabelWithLinks(result.build(), resultToCompare ? resultToCompare.build() : null) + '<br>'
598 tooltip.bindClick(function () {
599 if (tooltip.currentItem)
600 toggleClickTooltip(tooltip.currentItem.dataIndex, tooltip.currentItem.pageX, tooltip.currentItem.pageY);
603 function closestItemForPageXRespectingPlotOffset(item, plot, series, pageX) {
604 if (!series || !series.data.length)
607 var offset = $(plotContainer).offset();
608 var points = series.datapoints.points;
609 var size = series.datapoints.pointsize;
610 var xInPlot = pageX - offset.left;
612 if (xInPlot < plot.getPlotOffset().left)
620 var currentPoint = plot.pointOffset({x: points[index * size], y: points[index * size + 1]});
621 if (xInPlot < currentPoint.left) {
622 if (previousPoint && xInPlot < (previousPoint.left + currentPoint.left) / 2) {
624 currentPoint = previousPoint;
628 if (index + 1 >= series.data.length)
630 previousPoint = currentPoint;
634 // Ideally we want to return a real item object but flot doesn't provide an API to obtain one
635 // so create an object that contain properties we use.
636 return {dataIndex: index, pageX: offset.left + currentPoint.left, pageY: offset.top + currentPoint.top};
639 // Return a plot generator. This function is called when we change the number of days to show.
640 this.attach = function () {
644 bounds = computeYAxisBoundsToFitLines(minTime, results, baseline, target);
646 var overviewContainer = section.find('.overviewPlot');
648 overviewPlot = $.plot(overviewContainer, plotData,
649 $.extend(true, {}, sharedPlotOptions, {
652 // The maximum number of ticks we can fit on the overflow plot is 4.
653 tickSize: [(TestBuild.now() - minTime) / 4 / 1000, 'second']
655 grid: { hoverable: false, clickable: false },
656 selection: { mode: "x" },
664 $(plotContainer).bind("plotselected", function (event, ranges) { Chart.zoom(ranges.xaxis.from, ranges.xaxis.to); });
665 $(overviewContainer).bind("plotselected", function (event, ranges) { Chart.zoom(ranges.xaxis.from, ranges.xaxis.to); });
666 $(overviewContainer).bind("plotunselected", function () { Chart.clearZoom(minTime) });
670 this.zoom(currentZoom.from, currentZoom.to);
672 this.attachMainPlot(minTime);
676 // FIXME: Crosshair should stay where it was between charts.
677 $(plotContainer).bind("plothover", function (event, pos, item) {
678 for (var i = 0; i < charts.length; i++) {
679 if (charts[i] !== self) {
680 charts[i].setCrosshair(pos);
681 charts[i].hideTooltip();
686 var data = mainPlot.getData();
687 item = closestItemForPageXRespectingPlotOffset(item, mainPlot, data[data.length - 1], pos.pageX);
691 showTooltipWithResults(tooltip, item.pageX, item.pageY, results.resultAt(item.dataIndex), results.resultAt(item.dataIndex - 1));
692 tooltip.currentItem = item;
695 $(plotContainer).bind("mouseleave", function (event) {
696 var offset = $(plotContainer).offset();
697 if (offset.left <= event.pageX && offset.top <= event.pageY && event.pageX <= offset.left + $(plotContainer).outerWidth()
698 && event.pageY <= offset.top + $(plotContainer).outerHeight())
700 for (var i = 0; i < charts.length; i++) {
701 charts[i].clearCrosshair();
702 charts[i].hideTooltip();
707 if (isDashboard) { // FIXME: This code doesn't belong here.
708 $(plotContainer).bind('click', function (event) {
709 openChart(results.platform(), results.metric());
712 $(plotContainer).bind("plotclick", function (event, pos, item) {
713 if (tooltip.currentItem)
714 toggleClickTooltip(tooltip.currentItem.dataIndex, tooltip.currentItem.pageX, tooltip.currentItem.pageY);
722 Chart.clear = function () {
726 Chart.setMinTime = function (newMinTime) {
727 minTime = newMinTime;
728 for (var i = 0; i < charts.length; i++)
732 Chart.onzoomchange = function (from, to) { };
734 Chart.zoom = function (from, to) {
735 currentZoom = {from: from, to: to};
736 for (var i = 0; i < charts.length; i++)
737 charts[i].zoom(from, to);
738 this.onzoomchange(from, to);
741 Chart.clearZoom = function (minTime) {
742 currentZoom = undefined;
743 for (var i = 0; i < charts.length; i++)
744 charts[i].clearZoom();
748 window.Chart = Chart;
751 // FIXME: We need to devise a way to fetch runs in multiple chunks so that
752 // we don't have to fetch the entire time series to just show the last 3 days.
753 // FIXME: We should do a mass-fetch where we fetch JSONs for multiple runs at once.
754 function fetchTest(repositories, builders, filename, platform, metric, callback) {
756 function createRunAndResults(rawRuns) {
760 var runs = new PerfTestRuns(metric, platform);
761 var results = rawRuns.map(function (rawRun) {
762 // FIXME: Creating PerfTestResult and keeping them alive in memory all the time seems like a terrible idea.
763 // We should create PerfTestResult on demand.
764 return new PerfTestResult(runs, rawRun, new TestBuild(repositories, builders, platform, rawRun));
766 sortedResults = results.sort(function (a, b) { return a.build().time() - b.build().time(); });
767 sortedResults.forEach(function (result) { runs.addResult(result); });
771 $.getJSON('api/runs/' + filename, function (data) {
772 callback(createRunAndResults(data.current), createRunAndResults(data.baseline), createRunAndResults(data.target));
779 var fullNameToMetric;
780 var dashboardPlatforms;
781 var runsCache = {}; // FIXME: We need to clear this cache at some point.
786 // FIXME: Show some error message when we get 404.
787 function getOrFetchTest(platform, metric, callback) {
788 var filename = fileNameFromPlatformAndTest(platform.id, metric.id);
789 var caches = runsCache[filename];
792 setTimeout(function () { callback(caches.current, caches.baseline, caches.target); }, 0);
794 fetchTest(repositories, builders, filename, platform, metric, function (current, baseline, target) {
795 runsCache[filename] = {current:current, baseline:baseline, target:target};
796 callback(current, baseline, target);
801 function showDashboard() {
802 var dashboardTable = $('<table id="dashboard"></table>');
803 $('#mainContents').html(dashboardTable);
805 // Split dashboard platforms into groups of three so that it doesn't grow horizontally too much.
806 // FIXME: Make this adaptive.
807 for (var i = 0; i < Math.ceil(dashboardPlatforms.length / 3); i++)
808 addPlatformsToDashboard(dashboardTable, dashboardPlatforms.slice(i * 3, (i + 1) * 3));
810 URLState.remove('chartList');
813 function addPlatformsToDashboard(dashboardTable, selectedPlatforms) {
814 var header = document.createElement('thead');
815 selectedPlatforms.forEach(function (platform) {
816 var cell = document.createElement('th');
817 $(cell).text(platform.name);
818 header.appendChild(cell);
820 dashboardTable.append(header);
822 var tbody = $(document.createElement('tbody'));
823 dashboardTable.append(tbody);
824 var row = document.createElement('tr');
827 selectedPlatforms.forEach(function (platform) {
828 var cell = document.createElement('td');
829 row.appendChild(cell);
831 platform.metrics.forEach(function (metric) {
832 var chart = new Chart(cell, true, platform, metric, bugTrackers);
833 getOrFetchTest(platform, metric, function (results, baseline, target) {
834 // FIXME: We shouldn't rely on the order in which XHR finishes to order plots.
835 if (dashboardTable.parent().length) {
836 chart.populate(results, baseline, target);
844 function showCharts(lists) {
845 var chartsContainer = document.createElement('section');
846 chartsContainer.id = 'charts';
847 $('#mainContents').html(chartsContainer);
849 var testPicker = document.createElement('section');
850 testPicker.id = 'testPicker';
852 function addOption(select, label, value) {
853 var option = document.createElement('option');
854 option.appendChild(document.createTextNode(label));
856 option.value = value;
857 select.appendChild(option);
860 var testList = document.createElement('select');
861 testList.id = 'testList';
862 testPicker.appendChild(testList);
863 for (var i = 0; i < tests.length; ++i) {
864 if (tests[i].parentTest)
866 addOption(testList, tests[i].fullName, tests[i].id);
869 var metricList = document.createElement('select');
870 metricList.id = 'metricList';
871 testPicker.appendChild(metricList);
873 var platformList = document.createElement('select');
874 platformList.id = 'platformList';
875 testPicker.appendChild(platformList);
876 const OPTION_VALUE_FOR_ALL = '-';
877 addOption(platformList, 'All platforms', OPTION_VALUE_FOR_ALL);
878 for (var i = 0; i < allPlatforms.length; ++i)
879 addOption(platformList, allPlatforms[i].name);
881 testList.onchange = function () {
882 while (metricList.firstChild)
883 metricList.removeChild(metricList.firstChild);
885 addOption(metricList, 'All metrics', OPTION_VALUE_FOR_ALL);
886 for (var i = 0; i < tests.length; ++i) {
887 if (tests[i].id != testList.value && (!tests[i].parentTest || tests[i].parentTest.id != testList.value))
889 var selectedTest = tests[i].id == testList.value ? tests[i] : tests[i].parentTest;
890 for (var j = 0; j < tests[i].metrics.length; ++j) {
891 var fullName = tests[i].metrics[j].fullName;
892 var relativeName = fullName.replace(selectedTest.fullName, '').replace(/^[:/]/, '');
893 addOption(metricList, relativeName, fullName);
897 metricList.onchange = function () {
898 var metric = fullNameToMetric[metricList.value];
899 var shouldAddAllMetrics = metricList.value === OPTION_VALUE_FOR_ALL;
900 for (var i = 0; i < platformList.options.length; ++i) {
901 var option = platformList.options[i];
902 if (option.value === OPTION_VALUE_FOR_ALL) // Adding all metrics for all platforms will be too slow.
903 option.disabled = shouldAddAllMetrics;
905 var platform = nameToPlatform[option.value];
906 var platformHasMetric = platform.metrics.indexOf(metric) >= 0;
907 option.disabled = !shouldAddAllMetrics && !platformHasMetric;
912 metricList.onchange();
914 $(testPicker).append(' <a href="">Add Chart</a>');
916 function removeChart(chart) {
917 for (var i = 0; i < chartList.length; i++) {
918 if (chartList[i][0] == chart.platform().name && chartList[i][1] == chart.metric().fullName) {
919 chartList.splice(i, 1);
923 URLState.set('chartList', JSON.stringify(chartList));
926 function createChartFromListPair(platformName, metricFullName) {
927 var platform = nameToPlatform[platformName];
928 var metric = fullNameToMetric[metricFullName]
929 var chart = new Chart(chartsContainer, false, platform, metric, bugTrackers, removeChart);
931 getOrFetchTest(platform, metric, function (results, baseline, target) {
932 if (!chartsContainer.parentNode)
934 chart.populate(results, baseline, target);
939 $(testPicker).children('a').bind('click', function (event) {
940 event.preventDefault();
942 var newChartList = [];
943 if (platformList.value === OPTION_VALUE_FOR_ALL) {
944 for (var i = 0; i < allPlatforms.length; ++i) {
945 createChartFromListPair(allPlatforms[i].name, metricList.value);
946 newChartList.push([allPlatforms[i].name, metricList.value]);
948 } else if (metricList.value === OPTION_VALUE_FOR_ALL) {
949 for (var i = 0; i < tests.length; ++i) {
950 if (tests[i].id != testList.value && (!tests[i].parentTest || tests[i].parentTest.id != testList.value))
952 for (var j = 0; j < tests[i].metrics.length; ++j) {
953 createChartFromListPair(platformList.value, tests[i].metrics[j].fullName);
954 newChartList.push([platformList.value, tests[i].metrics[j].fullName]);
958 createChartFromListPair(platformList.value, metricList.value);
959 newChartList.push([platformList.value, metricList.value]);
962 chartList = chartList.concat(newChartList);
963 URLState.set('chartList', JSON.stringify(chartList));
968 $('#mainContents').append(testPicker);
972 chartList = JSON.parse(URLState.get('chartList', ''));
973 // FIXME: Should we verify that platform and test names are valid here?
974 } catch (exception) {
975 // Ignore any exception thrown by parse.
978 chartList.forEach(function (item) { createChartFromListPair(item[0], item[1]); });
981 // FIXME: We should use exponential slider for charts page where we expect to have
982 // the full JSON as opposed to the dashboard where we can't afford loading really big JSON files.
983 var exponential = true;
985 var input = $('#numberOfDays')[0];
987 function updateSpanAndCall(newNumberOfDays) {
988 $('#numberOfDays').next().text(newNumberOfDays + ' days');
989 // FIXME: This is likely broken for non-PST.
990 Chart.setMinTime(Date.now() - newNumberOfDays * 24 * 3600 * 1000);
992 $('#numberOfDays').bind('change', function () {
993 var newNumberOfDays = Math.round(exponential ? Math.exp(input.value) : input.value);
994 URLState.remove('zoom');
996 URLState.set('days', newNumberOfDays);
997 updateSpanAndCall(newNumberOfDays);
999 function onchange() {
1000 var newNumberOfDays = URLState.get('days', Math.round(exponential ? Math.exp(input.defaultValue) : input.defaultValue));
1001 $('#numberOfDays').val(exponential ? Math.log(newNumberOfDays) : newNumberOfDays);
1002 updateSpanAndCall(newNumberOfDays);
1004 URLState.watch('days', onchange);
1008 URLState.watch('mode', function () { setMode(URLState.get('mode')); });
1009 URLState.watch('chartList', function (changedStates) {
1010 var modeChanged = changedStates.indexOf('mode') >= 0;
1011 if (URLState.get('mode') == 'charts' && !modeChanged)
1015 function zoomChartsIfParsedCorrectly() {
1017 zoomValues = JSON.parse(URLState.get('zoom', '[]'));
1018 if (zoomValues.length != 2)
1020 Chart.zoom(parseFloat(zoomValues[0]), parseFloat(zoomValues[1]));
1021 } catch (exception) {
1022 // Ignore all exceptions thrown by JSON.parse.
1027 URLState.watch('zoom', function (changedStates) {
1028 var modeChanged = changedStates.indexOf('mode') >= 0;
1029 if (URLState.get('mode') == 'charts' && !modeChanged) {
1030 if (!zoomChartsIfParsedCorrectly())
1031 return Chart.clearZoom();
1035 Chart.onzoomchange = function (from, to) {
1037 URLState.set('zoom', JSON.stringify([from, to]));
1039 URLState.remove('zoom');
1042 window.openChart = function (platform, metric) {
1043 URLState.set('chartList', JSON.stringify([[platform.name, metric.fullName]]));
1047 window.setMode = function (newMode) {
1048 URLState.set('mode', newMode);
1051 if (newMode == 'dashboard') {
1052 URLState.remove('zoom');
1055 } else { // FIXME: Dynamically obtain the list of tests to show.
1057 zoomChartsIfParsedCorrectly();
1061 function fullName(test) {
1064 names.push(test.name);
1065 test = test.parentTest;
1068 return names.join('/');
1071 // Perhaps we want an ordered list of platforms.
1072 $.getJSON('data/manifest.json', function (data) {
1073 var manifest = data;
1076 for (var testId in manifest.tests) {
1077 var test = manifest.tests[testId];
1078 test.parentTest = manifest.tests[manifest.tests[testId].parentId];
1083 tests.forEach(function (test) {
1084 test.fullName = fullName(test);
1085 nameToTest[test.fullName] = test;
1087 tests.sort(function (a, b) {
1088 if (a.fullName < b.fullName)
1090 if (a.fullName > b.fullName)
1095 fullNameToMetric = {};
1096 for (var metricId in manifest.metrics) {
1097 var entry = manifest.metrics[metricId];
1098 entry.id = metricId;
1099 entry.test = manifest.tests[entry.test];
1100 entry.fullName = entry.test.fullName + ':' + entry.name;
1101 if (entry.aggregator)
1102 entry.fullName += ':' + entry.aggregator;
1103 entry.test.metrics.push(entry);
1104 fullNameToMetric[entry.fullName] = entry;
1107 tests.forEach(function (test) {
1108 test.metrics.sort(function (a, b) {
1109 if (a.name < b.name)
1111 if (a.name > b.name)
1118 nameToPlatform = {};
1119 for (var platformId in manifest.all) {
1120 var platform = manifest.all[platformId];
1121 platform.id = platformId;
1122 allPlatforms.push(platform);
1123 nameToPlatform[platform.name] = platform;
1124 // FIXME: Sort tests
1125 for (var i = 0; i < platform.metrics.length; ++i)
1126 platform.metrics[i] = manifest.metrics[platform.metrics[i]];
1128 allPlatforms.sort(function (a, b) { return a.name < b.name ? -1 : (a.name > b.name ? 1 : 0); });
1130 dashboardPlatforms = [];
1131 for (var platformId in manifest.dashboard) {
1132 var platform = manifest.dashboard[platformId];
1133 platform.id = platformId;
1134 for (var i = 0; i < platform.metrics.length; ++i)
1135 platform.metrics[i] = manifest.metrics[platform.metrics[i]];
1136 platform.metrics.sort(function (a, b) { return a.fullName < b.fullName ? -1 : (a.fullName > b.fullName ? 1 : 0); });
1137 dashboardPlatforms.push(manifest.dashboard[platformId]);
1139 dashboardPlatforms.sort(function (a, b) { return a.name < b.name ? -1 : (a.name > b.name ? 1 : 0); });
1141 repositories = manifest.repositories;
1142 builders = manifest.builders;
1143 bugTrackers = manifest.bugTrackers;
1145 setMode(URLState.get('mode', 'dashboard'));
1149 window.addEventListener('DOMContentLoaded', init, false);
1156 <h1><a href="/">WebKit Perf Monitor</a></h1>
1158 <li><a href="javascript:setMode('dashboard');">Dashboard</a></li>
1159 <li><a href="javascript:setMode('charts');">Charts</a></li>
1163 <div id="numberOfDaysPicker"><input id="numberOfDays" type="range" min="1" max="5.9" step="0.001" value="2.3"><span class="output"></span></div>
1165 <div id="mainContents"></div>