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">
22 #numberOfDaysPicker input {
29 border-collapse: collapse;
33 #dashboard > tbody > tr > td {
37 #dashboard > thead th {
38 text-shadow: #bbb 1px 1px 2px;
45 border: solid 1px #ccc;
47 margin: 0px 0px 10px 0px;
68 vertical-align: middle;
75 .chart h2, .chart h3 {
80 word-break: break-all;
91 #dashboard .chart h3 {
95 #dashboard .chart .status {
105 margin: 5px 5px 10px 5px;
132 #dashboard .overviewPlot {
136 #charts .overviewPlot {
137 margin: 10px 0px 0px 0px;
142 .chart .summaryTable {
163 padding: 0 0.2em 0 0;
168 .chart .meta td:not(:first-child):before,
169 .tooltip td:not(:first-child):before {
173 #dashboard .chart .summaryTable {
179 #charts .summaryTable {
230 .chart .yaxistoggler {
238 #dashboard .yaxistoggler {
265 border-color: transparent transparent #333 transparent;
268 .tooltip.inverted:before {
272 .tooltip.inverted:after {
287 border-color: #333 transparent transparent transparent;
291 text-decoration: underline;
305 display: inline-block;
307 border: solid 1px #ccc;
320 var sharedPlotOptions = {
321 lines: { show: true, lineWidth: 1 },
325 minTickSize: [1, 'day'],
326 max: Date.now(), // FIXME: This is likely broken for non-PST
328 yaxis: { tickLength: 0 },
329 series: { shadowSize: 0 },
330 points: { show: false },
335 backgroundColor: '#fff',
339 function adjustedIntervalForRun(results, minTime, minRatioToFitInAdjustedInterval) {
341 return {min: Number.MAX_VALUE, max: Number.MIN_VALUE};
342 var degreeOfWeightingDecrease = 0.2;
343 var movingAverage = results.exponentialMovingArithmeticMean(minTime, degreeOfWeightingDecrease);
344 var resultsCount = results.countResults(minTime);
345 var adjustmentDelta = results.sampleStandardDeviation(minTime) / 4;
346 var adjustedMin = movingAverage;
347 var adjustedMax = movingAverage;
349 for (adjustmentCount = 0; adjustmentCount < 4 * 4; adjustmentCount++) { // Don't expand beyond 4 standard deviations.
350 adjustedMin -= adjustmentDelta;
351 adjustedMax += adjustmentDelta;
352 if (results.countResultsInInterval(minTime, adjustedMin, adjustedMax) / resultsCount >= minRatioToFitInAdjustedInterval)
355 for (var i = 0; i < adjustmentCount; i++) {
356 if (results.countResultsInInterval(minTime, adjustedMin + adjustmentDelta, adjustedMax) / resultsCount < minRatioToFitInAdjustedInterval)
358 adjustedMin += adjustmentDelta;
360 for (var i = 0; i < adjustmentCount; i++) {
361 if (results.countResultsInInterval(minTime, adjustedMin, adjustedMax - adjustmentDelta) / resultsCount < minRatioToFitInAdjustedInterval)
363 adjustedMax -= adjustmentDelta;
365 return {min: adjustedMin, max: adjustedMax};
368 function computeYAxisBoundsToFitLines(minTime, results, baseline, target) {
369 var minOfAllRuns = results.min(minTime);
370 var maxOfAllRuns = results.max(minTime);
372 minOfAllRuns = Math.min(minOfAllRuns, baseline.min(minTime));
373 maxOfAllRuns = Math.max(maxOfAllRuns, baseline.max(minTime));
376 minOfAllRuns = Math.min(minOfAllRuns, target.min(minTime));
377 maxOfAllRuns = Math.max(maxOfAllRuns, target.max(minTime));
379 var marginSize = (maxOfAllRuns - minOfAllRuns) * 0.1;
381 var minRatioToFitInAdjustedInterval = 0.9;
382 var intervalForResults = adjustedIntervalForRun(results, minTime, minRatioToFitInAdjustedInterval);
383 var intervalForBaseline = adjustedIntervalForRun(baseline, minTime, minRatioToFitInAdjustedInterval);
384 var intervalForTarget = adjustedIntervalForRun(target, minTime, minRatioToFitInAdjustedInterval);
385 var adjustedMin = Math.min(intervalForResults.min, intervalForBaseline.min, intervalForTarget.min);
386 var adjustedMax = Math.max(intervalForResults.max, intervalForBaseline.max, intervalForTarget.max);
387 var adjsutedMarginSize = (adjustedMax - adjustedMin) * 0.1;
388 return {min: minOfAllRuns - marginSize, max: maxOfAllRuns + marginSize,
389 adjustedMin: Math.max(minOfAllRuns - marginSize, adjustedMin - adjsutedMarginSize),
390 adjustedMax: Math.min(maxOfAllRuns + marginSize, adjustedMax + adjsutedMarginSize)};
393 function computeStatus(smallerIsBetter, lastResult, baseline, target) {
394 var relativeDifferenceWithBaseline = baseline ? lastResult.relativeDifference(baseline.lastResult()) : 0;
395 var relativeDifferenceWithTarget = target ? lastResult.relativeDifference(target.lastResult()) : 0;
399 if (relativeDifferenceWithBaseline && relativeDifferenceWithBaseline > 0 != smallerIsBetter) {
400 statusText = Math.abs(relativeDifferenceWithBaseline * 100).toFixed(2) + '% ' + (smallerIsBetter ? 'above' : 'below') + ' baseline';
402 } else if (relativeDifferenceWithTarget && relativeDifferenceWithTarget > 0 == smallerIsBetter) {
403 statusText = Math.abs(relativeDifferenceWithTarget * 100).toFixed(2) + '% ' + (smallerIsBetter ? 'below' : 'above') + ' target';
405 } else if (relativeDifferenceWithTarget)
406 statusText = Math.abs(relativeDifferenceWithTarget * 100).toFixed(2) + '% until target';
408 return {class: status, text: statusText};
411 function addPlotDataForRun(plotData, name, runs, color, interactive) {
412 var entry = {color: color, data: runs.meanPlotData()};
414 entry.clickable = false;
415 entry.hoverable = false;
417 if (runs.hasConfidenceInterval()) {
418 var lowerName = name.toLowerCase();
419 var confienceEntry = $.extend(true, {}, entry, {lines: {lineWidth: 0}, clickable: false, hoverable: false});
420 plotData.push($.extend(true, {}, confienceEntry, {id: lowerName, data: runs.upperConfidencePlotData()}));
421 plotData.push($.extend(true, {}, confienceEntry,
422 {fillBetween: lowerName, lines: {fill: 0.3}, data: runs.lowerConfidencePlotData()}));
424 plotData.push(entry);
427 function createSummaryRowMarkup(name, runs) {
428 return '<tr><th>' + name + '</th><td>' + runs.lastResult().label() + '</td></tr>';
431 function buildLabelWithLinks(build, previousBuild) {
432 function linkifyIfNotNull(label, url) {
433 return url ? '<a href="' + url + '" target="_blank">' + label + '</a>' : label;
436 var formattedRevisions = build.formattedRevisions(previousBuild);
438 'Commit': build.formattedTime(),
439 'Build': linkifyIfNotNull(build.buildNumber(), build.buildUrl()) + '(' + build.formattedBuildTime() + ')'
441 for (var repositoryName in formattedRevisions)
442 buildInfo[repositoryName] = linkifyIfNotNull(formattedRevisions[repositoryName].label, formattedRevisions[repositoryName].url);
444 for (var key in buildInfo)
445 markup += '<tr><th>' + key + '</th><td>' + buildInfo[key] + '</td>';
446 return '<tr>' + markup + '</tr>';
449 function Chart(container, isDashboard, platform, metric, bugTrackers, onClose) {
450 var linkifiedFullName = metric.fullName;
452 linkifiedFullName = '<a href="' + metric.test.url + '">' + linkifiedFullName + '</a>';
453 var section = $('<section class="chart"><div class="pane"><header><h2>' + linkifiedFullName + '</h2>'
454 + '<h3 class="platform">' + platform.name + '</h3></header>'
455 + '<div class="meta"><table class="status"><tbody></tbody></table><table class="summaryTable"><tbody></tbody></table></div>'
456 + '<div class="overviewPlot"></div></div>'
457 + '<div class="plot"></div>'
458 + '<div class="unit"></div>'
459 + '<svg viewBox="0 0 20 100" class="arrow">'
460 + '<g stroke-width="10"><line x1="10" y1="8" x2="10" y2="92" />'
461 + '<polygon points="5,85 15,85 10,90" class="downwardArrowHead" />'
462 + '<polygon points="5,15 15,15 10,10" class="upwardArrowHead" />'
464 + '<a href="#" class="toggleYAxis"><svg viewBox="0 0 40 100" class="yaxistoggler"><g stroke-width="10">'
465 + '<line x1="20" y1="8" x2="20" y2="82" />'
466 + '<polygon points="15,15 25,15 20,10" />'
467 + '<polygon points="15,75 25,75 20,80" />'
468 + '<line x1="0" y1="92" x2="40" y2="92" />'
470 + '<a href="#" class="closeButton"><svg viewBox="0 0 100 100">'
471 + '<g stroke-width="10"><circle cx="50" cy="50" r="45" fill="transparent"/><polygon points="30,30 70,70" />'
472 + '<polygon points="30,70 70,30" /></g></svg></a></section>');
474 $(container).append(section);
478 section.find('.closeButton').bind('click', function (event) {
479 event.preventDefault();
481 charts.splice(charts.indexOf(self), 1);
486 section.find('.closeButton').hide();
488 section.find('.yaxistoggler').bind('click', function (event) {
490 event.preventDefault();
499 var tooltip = new Tooltip(container, 'tooltip hoverTooltip');
501 var plotContainer = section.find('.plot');
504 var clickTooltips = [];
505 var shouldShowEntireYAxis = false;
507 this.platform = function () { return platform; }
508 this.metric = function () { return metric; }
510 this.populate = function (passedResults, passedBaseline, passedTarget) {
511 results = passedResults;
512 baseline = passedBaseline;
513 target = passedTarget;
515 var summaryRows = '';
517 addPlotDataForRun(plotData, 'Target', target, '#039');
518 summaryRows = createSummaryRowMarkup('Target', target) + summaryRows;
521 addPlotDataForRun(plotData, 'Baseline', baseline, '#930');
522 summaryRows = createSummaryRowMarkup('Baseline', baseline) + summaryRows;
524 addPlotDataForRun(plotData, 'Current', results, '#666', true);
525 summaryRows = createSummaryRowMarkup('Current', results) + summaryRows;
527 var status = computeStatus(results.smallerIsBetter(), results.lastResult(), baseline, target);
529 summaryRows = '<tr><td colspan="2">' + status.text + '</td></tr>' + summaryRows;
530 section.addClass(status.class);
531 section.find('.status tbody').html(buildLabelWithLinks(results.lastResult().build()));
532 section.find('.summaryTable tbody').html(summaryRows);
533 section.find('.unit').html(results.unit());
534 if (results.smallerIsBetter()) {
535 section.find('.arrow .downwardArrowHead').show();
536 section.find('.arrow .upwardArrowHead').hide();
538 section.find('.arrow .downwardArrowHead').hide();
539 section.find('.arrow .upwardArrowHead').show();
543 this.attachMainPlot = function (xMin, xMax) {
547 var mainPlotOptions = $.extend(true, {}, sharedPlotOptions, {
553 min: shouldShowEntireYAxis ? 0 : bounds.adjustedMin,
554 max: shouldShowEntireYAxis ? bounds.max : bounds.adjustedMax,
556 crosshair: {'mode': 'x', color: '#c90', lineWidth: 1},
557 grid: {clickable: true},
560 mainPlotOptions.xaxis.max = xMax;
562 mainPlotOptions.yaxis.labelWidth = 20;
563 mainPlotOptions.yaxis.ticks = [mainPlotOptions.yaxis.min, mainPlotOptions.yaxis.max];
564 mainPlotOptions.grid.autoHighlight = false;
566 mainPlotOptions.yaxis.labelWidth = 30;
567 mainPlotOptions.selection = {mode: "x"};
568 plotData[plotData.length - 1].points = {show: true, radius: 1};
570 mainPlot = $.plot(plotContainer, plotData, mainPlotOptions);
571 plotData[plotData.length - 1].points = {};
573 for (var i = 0; i < clickTooltips.length; i++) {
574 if (clickTooltips[i])
575 clickTooltips[i].remove();
580 this.toggleYAxis = function () {
581 shouldShowEntireYAxis = !shouldShowEntireYAxis;
583 this.attachMainPlot(currentZoom.from, currentZoom.to);
585 this.attachMainPlot(minTime);
588 this.zoom = function (from, to) {
589 this.attachMainPlot(from, to);
591 overviewPlot.setSelection({xaxis: {from: from, to: to}}, true);
594 this.clearZoom = function () {
595 this.attachMainPlot(minTime);
597 overviewPlot.clearSelection();
600 this.setCrosshair = function (pos) {
602 mainPlot.setCrosshair(pos);
605 this.clearCrosshair = function () {
607 mainPlot.clearCrosshair();
610 this.hideTooltip = function() {
615 function toggleClickTooltip(index, pageX, pageY) {
616 if (clickTooltips[index])
617 clickTooltips[index].toggle();
619 // FIXME: Put this on URLState.
620 var newTooltip = new Tooltip(container, 'tooltip clickTooltip');
621 showTooltipWithResults(newTooltip, pageX, pageY, results.resultAt(index), results.resultAt(index - 1));
622 newTooltip.bindClick(function () { toggleClickTooltip(index, pageX, pageY); });
623 newTooltip.bindMouseEnter(function () { tooltip.hide(); });
624 clickTooltips[index] = newTooltip;
629 function showTooltipWithResults(tooltip, x, y, result, resultToCompare) {
631 if (resultToCompare) {
632 var title = (resultToCompare.isBetterThan(result) ? 'REGRESSION: ' : '') + result.metric().fullName
633 + ' got ' + result.formattedProgressionOrRegression(resultToCompare)
634 + ' around ' + result.build().formattedTime();
635 var revisions = result.build().formattedRevisions(resultToCompare.build());
637 for (var trackerId in bugTrackers) {
638 var tracker = bugTrackers[trackerId];
639 var repositories = tracker.repositories;
640 var description = 'Platform: ' + result.build().platform().name + '\n\n';
641 for (var i = 0; i < repositories.length; ++i) {
642 var repositoryName = repositories[i];
643 var revision = revisions[repositoryName];
647 description += repositoryName + ': ' + revision.url;
649 description += revision.label;
652 var url = tracker.newBugUrl
653 .replace(/\$title/g, encodeURIComponent(title))
654 .replace(/\$description/g, encodeURIComponent(description))
655 .replace(/\$link/g, encodeURIComponent(location.href));
658 newBugUrls += ' <a href="' + url + '" target="_blank">' + tracker.name + '</a>';
660 newBugUrls = 'File:' + newBugUrls;
662 tooltip.show(x, y, result.label(resultToCompare) + '<table>'
663 + buildLabelWithLinks(result.build(), resultToCompare ? resultToCompare.build() : null) + '</table>'
667 tooltip.bindClick(function () {
668 if (tooltip.currentItem)
669 toggleClickTooltip(tooltip.currentItem.dataIndex, tooltip.currentItem.pageX, tooltip.currentItem.pageY);
672 function closestItemForPageXRespectingPlotOffset(item, plot, series, pageX) {
673 if (!series || !series.data.length)
676 var offset = $(plotContainer).offset();
677 var points = series.datapoints.points;
678 var size = series.datapoints.pointsize;
679 var xInPlot = pageX - offset.left;
681 if (xInPlot < plot.getPlotOffset().left)
689 var currentPoint = plot.pointOffset({x: points[index * size], y: points[index * size + 1]});
690 if (xInPlot < currentPoint.left) {
691 if (previousPoint && xInPlot < (previousPoint.left + currentPoint.left) / 2) {
693 currentPoint = previousPoint;
697 if (index + 1 >= series.data.length)
699 previousPoint = currentPoint;
703 // Ideally we want to return a real item object but flot doesn't provide an API to obtain one
704 // so create an object that contain properties we use.
705 return {dataIndex: index, pageX: offset.left + currentPoint.left, pageY: offset.top + currentPoint.top};
708 // Return a plot generator. This function is called when we change the number of days to show.
709 this.attach = function () {
713 bounds = computeYAxisBoundsToFitLines(minTime, results, baseline, target);
715 var overviewContainer = section.find('.overviewPlot');
717 overviewPlot = $.plot(overviewContainer, plotData,
718 $.extend(true, {}, sharedPlotOptions, {
721 // The maximum number of ticks we can fit on the overflow plot is 4.
722 tickSize: [(TestBuild.now() - minTime) / 4 / 1000, 'second']
724 grid: { hoverable: false, clickable: false },
725 selection: { mode: "x" },
733 $(plotContainer).bind("plotselected", function (event, ranges) { Chart.zoom(ranges.xaxis.from, ranges.xaxis.to); });
734 $(overviewContainer).bind("plotselected", function (event, ranges) { Chart.zoom(ranges.xaxis.from, ranges.xaxis.to); });
735 $(overviewContainer).bind("plotunselected", function () { Chart.clearZoom(minTime) });
739 this.zoom(currentZoom.from, currentZoom.to);
741 this.attachMainPlot(minTime);
743 if (bindPlotEventHandlers)
744 bindPlotEventHandlers(this);
745 bindPlotEventHandlers = null;
748 function bindPlotEventHandlers(chart) {
749 // FIXME: Crosshair should stay where it was between charts.
750 $(plotContainer).bind("plothover", function (event, pos, item) {
751 for (var i = 0; i < charts.length; i++) {
752 if (charts[i] !== chart) {
753 charts[i].setCrosshair(pos);
754 charts[i].hideTooltip();
759 var data = mainPlot.getData();
760 item = closestItemForPageXRespectingPlotOffset(item, mainPlot, data[data.length - 1], pos.pageX);
764 showTooltipWithResults(tooltip, item.pageX, item.pageY, results.resultAt(item.dataIndex), results.resultAt(item.dataIndex - 1));
765 tooltip.currentItem = item;
768 $(plotContainer).bind("mouseleave", function (event) {
769 var offset = $(plotContainer).offset();
770 if (offset.left <= event.pageX && offset.top <= event.pageY && event.pageX <= offset.left + $(plotContainer).outerWidth()
771 && event.pageY <= offset.top + $(plotContainer).outerHeight())
773 for (var i = 0; i < charts.length; i++) {
774 charts[i].clearCrosshair();
775 charts[i].hideTooltip();
780 if (isDashboard) { // FIXME: This code doesn't belong here.
781 $(plotContainer).bind('click', function (event) {
782 openChart(results.platform(), results.metric());
785 $(plotContainer).bind("plotclick", function (event, pos, item) {
786 if (tooltip.currentItem)
787 toggleClickTooltip(tooltip.currentItem.dataIndex, tooltip.currentItem.pageX, tooltip.currentItem.pageY);
795 Chart.clear = function () {
799 Chart.setMinTime = function (newMinTime) {
800 minTime = newMinTime;
801 for (var i = 0; i < charts.length; i++)
805 Chart.onzoomchange = function (from, to) { };
807 Chart.zoom = function (from, to) {
808 currentZoom = {from: from, to: to};
809 for (var i = 0; i < charts.length; i++)
810 charts[i].zoom(from, to);
811 this.onzoomchange(from, to);
814 Chart.clearZoom = function (minTime) {
815 currentZoom = undefined;
816 for (var i = 0; i < charts.length; i++)
817 charts[i].clearZoom();
821 window.Chart = Chart;
824 // FIXME: We need to devise a way to fetch runs in multiple chunks so that
825 // we don't have to fetch the entire time series to just show the last 3 days.
826 // FIXME: We should do a mass-fetch where we fetch JSONs for multiple runs at once.
827 function fetchTest(repositories, builders, filename, platform, metric, callback) {
829 function createRunAndResults(rawRuns) {
833 var runs = new PerfTestRuns(metric, platform);
834 var results = rawRuns.map(function (rawRun) {
835 // FIXME: Creating PerfTestResult and keeping them alive in memory all the time seems like a terrible idea.
836 // We should create PerfTestResult on demand.
837 return new PerfTestResult(runs, rawRun, new TestBuild(repositories, builders, platform, rawRun));
839 runs.setResults(results.sort(function (a, b) { return a.build().time() - b.build().time(); }));
843 $.getJSON('api/runs/' + filename, function (data) {
844 callback(createRunAndResults(data.current), createRunAndResults(data.baseline), createRunAndResults(data.target));
851 var fullNameToMetric;
852 var dashboardPlatforms;
853 var runsCache = {}; // FIXME: We need to clear this cache at some point.
858 // FIXME: Show some error message when we get 404.
859 function getOrFetchTest(platform, metric, callback) {
860 var filename = fileNameFromPlatformAndTest(platform.id, metric.id);
861 var caches = runsCache[filename];
864 setTimeout(function () { callback(caches.current, caches.baseline, caches.target); }, 0);
866 fetchTest(repositories, builders, filename, platform, metric, function (current, baseline, target) {
867 runsCache[filename] = {current:current, baseline:baseline, target:target};
868 callback(current, baseline, target);
873 function showDashboard() {
874 var dashboardTable = $('<table id="dashboard"></table>');
875 $('#mainContents').html(dashboardTable);
877 // Split dashboard platforms into groups of three so that it doesn't grow horizontally too much.
878 // FIXME: Make this adaptive.
879 for (var i = 0; i < Math.ceil(dashboardPlatforms.length / 3); i++)
880 addPlatformsToDashboard(dashboardTable, dashboardPlatforms.slice(i * 3, (i + 1) * 3));
882 URLState.remove('chartList');
885 function addPlatformsToDashboard(dashboardTable, selectedPlatforms) {
886 var header = document.createElement('thead');
887 selectedPlatforms.forEach(function (platform) {
888 var cell = document.createElement('th');
889 $(cell).text(platform.name);
890 header.appendChild(cell);
892 dashboardTable.append(header);
894 var tbody = $(document.createElement('tbody'));
895 dashboardTable.append(tbody);
896 var row = document.createElement('tr');
899 selectedPlatforms.forEach(function (platform) {
900 var cell = document.createElement('td');
901 row.appendChild(cell);
903 platform.metrics.forEach(function (metric) {
904 var chart = new Chart(cell, true, platform, metric, bugTrackers);
905 getOrFetchTest(platform, metric, function (results, baseline, target) {
906 // FIXME: We shouldn't rely on the order in which XHR finishes to order plots.
907 if (dashboardTable.parent().length) {
908 chart.populate(results, baseline, target);
916 function showCharts(lists) {
917 var chartsContainer = document.createElement('section');
918 chartsContainer.id = 'charts';
919 $('#mainContents').html(chartsContainer);
921 var testPicker = document.createElement('section');
922 testPicker.id = 'testPicker';
924 function addOption(select, label, value) {
925 var option = document.createElement('option');
926 option.appendChild(document.createTextNode(label));
928 option.value = value;
929 select.appendChild(option);
932 var testList = document.createElement('select');
933 testList.id = 'testList';
934 testPicker.appendChild(testList);
935 for (var i = 0; i < tests.length; ++i) {
936 if (tests[i].parentTest)
938 addOption(testList, tests[i].fullName, tests[i].id);
941 var metricList = document.createElement('select');
942 metricList.id = 'metricList';
943 testPicker.appendChild(metricList);
945 var platformList = document.createElement('select');
946 platformList.id = 'platformList';
947 testPicker.appendChild(platformList);
948 const OPTION_VALUE_FOR_ALL = '-';
949 addOption(platformList, 'All platforms', OPTION_VALUE_FOR_ALL);
950 for (var i = 0; i < allPlatforms.length; ++i)
951 addOption(platformList, allPlatforms[i].name);
953 testList.onchange = function () {
954 while (metricList.firstChild)
955 metricList.removeChild(metricList.firstChild);
957 var metricsGroup = document.createElement('optgroup');
958 metricsGroup.label = 'Metrics';
959 metricList.appendChild(metricsGroup);
960 addOption(metricsGroup, 'All metrics', OPTION_VALUE_FOR_ALL);
961 for (var i = 0; i < tests.length; ++i) {
962 if (tests[i].id == testList.value) {
963 var selectedTest = tests[i];
964 for (var j = 0; j < selectedTest.metrics.length; ++j) {
965 var fullName = selectedTest.metrics[j].fullName;
966 var relativeName = fullName.replace(selectedTest.fullName, '').replace(/^[:/]/, '');
967 addOption(metricsGroup, relativeName, fullName);
971 var subtestsGroup = document.createElement('optgroup');
972 subtestsGroup.label = 'Tests';
973 metricList.appendChild(subtestsGroup);
974 addOption(subtestsGroup, 'All subtests', OPTION_VALUE_FOR_ALL);
975 for (var i = 0; i < tests.length; ++i) {
976 if (!tests[i].parentTest || tests[i].parentTest.id != testList.value)
978 var subtest = tests[i];
979 var selectedTest = subtest.parentTest;
980 for (var j = 0; j < subtest.metrics.length; ++j) {
981 var fullName = subtest.metrics[j].fullName;
982 var relativeName = fullName.replace(selectedTest.fullName, '').replace(/^[:/]/, '');
983 addOption(subtestsGroup, relativeName, fullName);
987 metricList.onchange = function () {
988 var metric = fullNameToMetric[metricList.value];
989 var shouldAddAllMetrics = metricList.value === OPTION_VALUE_FOR_ALL;
990 for (var i = 0; i < platformList.options.length; ++i) {
991 var option = platformList.options[i];
992 if (option.value === OPTION_VALUE_FOR_ALL) // Adding all metrics for all platforms will be too slow.
993 option.disabled = shouldAddAllMetrics;
995 var platform = nameToPlatform[option.value];
996 var platformHasMetric = platform.metrics.indexOf(metric) >= 0;
997 option.disabled = !shouldAddAllMetrics && !platformHasMetric;
1001 testList.onchange();
1002 metricList.onchange();
1004 $(testPicker).append(' <a href="">Add Chart</a>');
1006 function removeChart(chart) {
1007 for (var i = 0; i < chartList.length; i++) {
1008 if (chartList[i][0] == chart.platform().name && chartList[i][1] == chart.metric().fullName) {
1009 chartList.splice(i, 1);
1013 URLState.set('chartList', JSON.stringify(chartList));
1016 function createChartFromListPair(platformName, metricFullName) {
1017 var platform = nameToPlatform[platformName];
1018 var metric = fullNameToMetric[metricFullName]
1019 var chart = new Chart(chartsContainer, false, platform, metric, bugTrackers, removeChart);
1021 getOrFetchTest(platform, metric, function (results, baseline, target) {
1022 if (!chartsContainer.parentNode)
1024 chart.populate(results, baseline, target);
1029 $(testPicker).children('a').bind('click', function (event) {
1030 event.preventDefault();
1032 var newChartList = [];
1033 if (platformList.value === OPTION_VALUE_FOR_ALL) {
1034 for (var i = 0; i < allPlatforms.length; ++i) {
1035 createChartFromListPair(allPlatforms[i].name, metricList.value);
1036 newChartList.push([allPlatforms[i].name, metricList.value]);
1038 } else if (metricList.value === OPTION_VALUE_FOR_ALL) {
1039 var group = metricList.selectedOptions[0].parentNode;
1040 var metricsToAdd = [];
1041 for (var i = 0; i < group.children.length; i++) {
1042 var metric = group.children[i].value;
1043 if (metric == OPTION_VALUE_FOR_ALL)
1045 createChartFromListPair(platformList.value, metric);
1046 newChartList.push([platformList.value, metric]);
1049 createChartFromListPair(platformList.value, metricList.value);
1050 newChartList.push([platformList.value, metricList.value]);
1053 chartList = chartList.concat(newChartList);
1054 URLState.set('chartList', JSON.stringify(chartList));
1059 $('#mainContents').append(testPicker);
1063 chartList = JSON.parse(URLState.get('chartList', ''));
1064 // FIXME: Should we verify that platform and test names are valid here?
1065 } catch (exception) {
1066 // Ignore any exception thrown by parse.
1069 chartList.forEach(function (item) { createChartFromListPair(item[0], item[1]); });
1072 // FIXME: We should use exponential slider for charts page where we expect to have
1073 // the full JSON as opposed to the dashboard where we can't afford loading really big JSON files.
1074 var exponential = true;
1076 var input = $('#numberOfDays')[0];
1078 function updateSpanAndCall(newNumberOfDays) {
1079 $('#numberOfDays').next().text(newNumberOfDays + ' days');
1080 // FIXME: This is likely broken for non-PST.
1081 Chart.setMinTime(Date.now() - newNumberOfDays * 24 * 3600 * 1000);
1083 $('#numberOfDays').bind('change', function () {
1084 var newNumberOfDays = Math.round(exponential ? Math.exp(input.value) : input.value);
1085 URLState.remove('zoom');
1087 URLState.set('days', newNumberOfDays);
1088 updateSpanAndCall(newNumberOfDays);
1090 function onchange() {
1091 var newNumberOfDays = URLState.get('days', Math.round(exponential ? Math.exp(input.defaultValue) : input.defaultValue));
1092 $('#numberOfDays').val(exponential ? Math.log(newNumberOfDays) : newNumberOfDays);
1093 updateSpanAndCall(newNumberOfDays);
1095 URLState.watch('days', onchange);
1099 URLState.watch('mode', function () { setMode(URLState.get('mode')); });
1100 URLState.watch('chartList', function (changedStates) {
1101 var modeChanged = changedStates.indexOf('mode') >= 0;
1102 if (URLState.get('mode') == 'charts' && !modeChanged)
1106 function zoomChartsIfParsedCorrectly() {
1108 zoomValues = JSON.parse(URLState.get('zoom', '[]'));
1109 if (zoomValues.length != 2)
1111 Chart.zoom(parseFloat(zoomValues[0]), parseFloat(zoomValues[1]));
1112 } catch (exception) {
1113 // Ignore all exceptions thrown by JSON.parse.
1118 URLState.watch('zoom', function (changedStates) {
1119 var modeChanged = changedStates.indexOf('mode') >= 0;
1120 if (URLState.get('mode') == 'charts' && !modeChanged) {
1121 if (!zoomChartsIfParsedCorrectly())
1122 return Chart.clearZoom();
1126 Chart.onzoomchange = function (from, to) {
1128 URLState.set('zoom', JSON.stringify([from, to]));
1130 URLState.remove('zoom');
1133 window.openChart = function (platform, metric) {
1134 URLState.set('chartList', JSON.stringify([[platform.name, metric.fullName]]));
1138 window.setMode = function (newMode) {
1139 URLState.set('mode', newMode);
1142 if (newMode == 'dashboard') {
1143 URLState.remove('zoom');
1146 } else { // FIXME: Dynamically obtain the list of tests to show.
1148 zoomChartsIfParsedCorrectly();
1152 function fullName(test) {
1155 names.push(test.name);
1156 test = test.parentTest;
1159 return names.join('/');
1162 // Perhaps we want an ordered list of platforms.
1163 $.getJSON('data/manifest.json', function (data) {
1164 var manifest = data;
1167 for (var testId in manifest.tests) {
1168 var test = manifest.tests[testId];
1169 test.parentTest = manifest.tests[manifest.tests[testId].parentId];
1174 tests.forEach(function (test) {
1175 test.fullName = fullName(test);
1176 nameToTest[test.fullName] = test;
1178 tests.sort(function (a, b) {
1179 if (a.fullName < b.fullName)
1181 if (a.fullName > b.fullName)
1186 fullNameToMetric = {};
1187 for (var metricId in manifest.metrics) {
1188 var entry = manifest.metrics[metricId];
1189 entry.id = metricId;
1190 entry.test = manifest.tests[entry.test];
1191 entry.fullName = entry.test.fullName + ':' + entry.name;
1192 if (entry.aggregator)
1193 entry.fullName += ':' + entry.aggregator;
1194 entry.test.metrics.push(entry);
1195 fullNameToMetric[entry.fullName] = entry;
1198 tests.forEach(function (test) {
1199 test.metrics.sort(function (a, b) {
1200 if (a.name < b.name)
1202 if (a.name > b.name)
1209 nameToPlatform = {};
1210 for (var platformId in manifest.all) {
1211 var platform = manifest.all[platformId];
1212 platform.id = platformId;
1213 allPlatforms.push(platform);
1214 nameToPlatform[platform.name] = platform;
1215 // FIXME: Sort tests
1216 for (var i = 0; i < platform.metrics.length; ++i)
1217 platform.metrics[i] = manifest.metrics[platform.metrics[i]];
1219 allPlatforms.sort(function (a, b) { return a.name < b.name ? -1 : (a.name > b.name ? 1 : 0); });
1221 dashboardPlatforms = [];
1222 for (var platformId in manifest.dashboard) {
1223 var platform = manifest.dashboard[platformId];
1224 platform.id = platformId;
1225 for (var i = 0; i < platform.metrics.length; ++i)
1226 platform.metrics[i] = manifest.metrics[platform.metrics[i]];
1227 platform.metrics.sort(function (a, b) { return a.fullName < b.fullName ? -1 : (a.fullName > b.fullName ? 1 : 0); });
1228 dashboardPlatforms.push(manifest.dashboard[platformId]);
1230 dashboardPlatforms.sort(function (a, b) { return a.name < b.name ? -1 : (a.name > b.name ? 1 : 0); });
1232 repositories = manifest.repositories;
1233 builders = manifest.builders;
1234 bugTrackers = manifest.bugTrackers;
1236 setMode(URLState.get('mode', 'dashboard'));
1240 window.addEventListener('DOMContentLoaded', init, false);
1247 <h1><a href="/">WebKit Perf Monitor</a></h1>
1249 <li id="numberOfDaysPicker"><input id="numberOfDays" type="range" min="1" max="5.9" step="0.001" value="2.3"><span class="output"></span></li>
1250 <li><a href="javascript:setMode('dashboard');">Dashboard</a></li>
1251 <li><a href="javascript:setMode('charts');">Charts</a></li>
1255 <div id="mainContents"></div>