blob: 50c556b95cd34be4745ee1ee83deda23c1785abd [file] [log] [blame]
<!DOCTYPE html>
<html>
<head>
<title>Perf Monitor is Loading...</title>
<script>
if (location.hash.indexOf('#mode=dashboard') >= 0 || !location.hash || location.hash == '#')
location.href = '/v3/';
</script>
<script src="js/jquery.js" defer></script>
<script src="js/jquery.flot.js" defer></script>
<script src="js/jquery.flot.crosshair.js" defer></script>
<script src="js/jquery.flot.fillbetween.js" defer></script>
<script src="js/jquery.flot.resize.js" defer></script>
<script src="js/jquery.flot.selection.js" defer></script>
<script src="js/jquery.flot.time.js" defer></script>
<script src="js/helper-classes.js" defer></script>
<script src="shared/statistics.js" defer></script>
<link rel="stylesheet" href="common.css">
<style type="text/css">
#numberOfDaysPicker {
color: #666;
}
#numberOfDaysPicker input {
height: 0.9em;
margin-right: 1em;
}
td, th {
border: none;
border-collapse: collapse;
padding-top: 0.5em;
}
#dashboard > tbody > tr > td {
vertical-align: top;
}
#dashboard > thead th {
text-shadow: #bbb 1px 1px 2px;
font-size: large;
font-weight: normal;
}
.chart {
position: relative;
border: solid 1px #ccc;
border-radius: 5px;
margin: 0px 0px 10px 0px;
}
.chart.worse {
background: #fcc;
}
.chart.better {
background: #cfc;
}
#charts .pane {
position: absolute;
left: 10px;
top: 10px;
width: 230px;
}
.chart header {
height: 3em;
display: table-cell;
vertical-align: middle;
}
#dashboard header {
padding: 0px 10px;
}
.chart h2, .chart h3 {
margin: 0 0 0.3em 0;
padding: 0;
font-size: 1em;
font-weight: normal;
word-break: break-all;
}
.chart h2 {
font-size: normal;
}
.chart h3 {
font-size: normal;
}
#dashboard .chart h3 {
display: none;
}
#dashboard .chart .status {
margin: 0px 10px;
}
.chart .status {
font-size: small;
color: #666;
}
.plot {
margin: 5px 5px 10px 5px;
}
.closeButton svg {
position: absolute;
left: 8px;
bottom: 8px;
width: 15px;
height: 15px;
}
#dashboard .chart {
width: 410px;
}
#dashboard .plot {
width: 400px;
height: 100px;
cursor: pointer;
cursor: hand;
}
#charts .plot {
height: 320px;
margin-left: 250px;
}
#dashboard .overviewPlot {
display: none;
}
#charts .overviewPlot {
margin: 10px 0px 0px 0px;
padding: 0;
height: 70px;
}
.chart .summaryTable {
font-size: small;
color: #666;
border: 0;
}
.chart .meta {
position: relative;
}
.chart .meta table,
.chart .meta td,
.tooltip table,
.tooltip td {
margin: 0;
padding: 0;
}
.chart .meta th,
.tooltip th {
margin: 0;
padding: 0 0.2em 0 0;
text-align: right;
font-weight: normal;
}
.chart .meta td:not(:first-child):before,
.tooltip td:not(:first-child):before {
content: ": ";
}
#dashboard .chart .summaryTable {
position: absolute;
right: 10px;
top: 0px;
}
#charts .summaryTable {
margin-top: 0.3em;
}
#dashboard .arrow {
width: 20px;
height: 40px;
position: absolute;
bottom: 50px;
left: 5px;
}
#charts .arrow {
width: 20px;
height: 40px;
position: absolute;
top: 10px;
left: 240px;
}
.chart svg {
stroke: #ccc;
fill: #ccc;
color: #ccc;
}
.chart.worse svg {
stroke: #c99;
fill: #c99;
color: #c99;
}
.chart.better svg {
stroke: #9c9;
fill: #9c9;
color: #9c9;
}
.chart .yaxistoggler {
position: absolute;
bottom: 10px;
left: 225px;
width: 10px;
height: 25px;
}
#dashboard .yaxistoggler {
display: none;
}
.tooltip {
position: relative;
border-radius: 5px;
padding: 5px;
opacity: 0.8;
background: #333;
color: #eee;
font-size: small;
line-height: 130%;
white-space: nowrap;
}
.tooltip:before {
position: absolute;
width: 0;
height: 0;
left: 50%;
margin-left: -9px;
top: -19px;
content: "";
display: none;
border-style: solid;
border-width: 10px;
border-color: transparent transparent #333 transparent;
}
.tooltip.inverted:before {
display: block;
}
.tooltip.inverted:after {
display: none;
}
.tooltip:after {
position: absolute;
width: 0;
height: 0;
left: 50%;
margin-left: -9px;
bottom: -19px;
content: "";
display: block;
border-style: solid;
border-width: 10px;
border-color: #333 transparent transparent transparent;
}
.tooltip a {
text-decoration: underline;
color: #fff;
text-shadow: none;
}
.clickTooltip {
opacity: 0.6;
}
.hoverTooltip {
z-index: 99999;
}
#testPicker {
display: inline-block;
margin: 10px 0px;
border: solid 1px #ccc;
color: #666;
border-radius: 5px;
padding: 5px 8px;
}
</style>
<script>
(function () {
var charts = [];
var minTime;
var currentZoom;
var sharedPlotOptions = {
lines: { show: true, lineWidth: 1 },
xaxis: {
mode: "time",
timeformat: "%m/%d",
minTickSize: [1, 'day'],
max: Date.now(), // FIXME: This is likely broken for non-PST
},
yaxis: { tickLength: 0 },
series: { shadowSize: 0 },
points: { show: false },
grid: {
hoverable: true,
borderWidth: 1,
borderColor: '#999',
backgroundColor: '#fff',
}
};
function adjustedIntervalForRun(results, minTime, minRatioToFitInAdjustedInterval) {
if (!results)
return {min: Number.MAX_VALUE, max: Number.MIN_VALUE};
var degreeOfWeightingDecrease = 0.2;
var movingAverage = results.exponentialMovingArithmeticMean(minTime, degreeOfWeightingDecrease);
var resultsCount = results.countResults(minTime);
var adjustmentDelta = results.sampleStandardDeviation(minTime) / 4;
var adjustedMin = movingAverage;
var adjustedMax = movingAverage;
var adjustmentCount;
for (adjustmentCount = 0; adjustmentCount < 4 * 4; adjustmentCount++) { // Don't expand beyond 4 standard deviations.
adjustedMin -= adjustmentDelta;
adjustedMax += adjustmentDelta;
if (results.countResultsInInterval(minTime, adjustedMin, adjustedMax) / resultsCount >= minRatioToFitInAdjustedInterval)
break;
}
for (var i = 0; i < adjustmentCount; i++) {
if (results.countResultsInInterval(minTime, adjustedMin + adjustmentDelta, adjustedMax) / resultsCount < minRatioToFitInAdjustedInterval)
break;
adjustedMin += adjustmentDelta;
}
for (var i = 0; i < adjustmentCount; i++) {
if (results.countResultsInInterval(minTime, adjustedMin, adjustedMax - adjustmentDelta) / resultsCount < minRatioToFitInAdjustedInterval)
break;
adjustedMax -= adjustmentDelta;
}
return {min: adjustedMin, max: adjustedMax};
}
function computeYAxisBoundsToFitLines(minTime, results, baseline, target) {
var minOfAllRuns = results.min(minTime);
var maxOfAllRuns = results.max(minTime);
if (baseline) {
minOfAllRuns = Math.min(minOfAllRuns, baseline.min(minTime));
maxOfAllRuns = Math.max(maxOfAllRuns, baseline.max(minTime));
}
if (target) {
minOfAllRuns = Math.min(minOfAllRuns, target.min(minTime));
maxOfAllRuns = Math.max(maxOfAllRuns, target.max(minTime));
}
var marginSize = (maxOfAllRuns - minOfAllRuns) * 0.1;
var minRatioToFitInAdjustedInterval = 0.9;
var intervalForResults = adjustedIntervalForRun(results, minTime, minRatioToFitInAdjustedInterval);
var intervalForBaseline = adjustedIntervalForRun(baseline, minTime, minRatioToFitInAdjustedInterval);
var intervalForTarget = adjustedIntervalForRun(target, minTime, minRatioToFitInAdjustedInterval);
var adjustedMin = Math.min(intervalForResults.min, intervalForBaseline.min, intervalForTarget.min);
var adjustedMax = Math.max(intervalForResults.max, intervalForBaseline.max, intervalForTarget.max);
var adjsutedMarginSize = (adjustedMax - adjustedMin) * 0.1;
return {min: minOfAllRuns - marginSize, max: maxOfAllRuns + marginSize,
adjustedMin: Math.max(minOfAllRuns - marginSize, adjustedMin - adjsutedMarginSize),
adjustedMax: Math.min(maxOfAllRuns + marginSize, adjustedMax + adjsutedMarginSize)};
}
function computeStatus(smallerIsBetter, lastResult, baseline, target) {
var relativeDifferenceWithBaseline = baseline ? lastResult.relativeDifference(baseline.lastResult()) : 0;
var relativeDifferenceWithTarget = target ? lastResult.relativeDifference(target.lastResult()) : 0;
var statusText = '';
var status = '';
if (relativeDifferenceWithBaseline && relativeDifferenceWithBaseline > 0 != smallerIsBetter) {
statusText = Math.abs(relativeDifferenceWithBaseline * 100).toFixed(2) + '% ' + (smallerIsBetter ? 'above' : 'below') + ' baseline';
status = 'worse';
} else if (relativeDifferenceWithTarget && relativeDifferenceWithTarget > 0 == smallerIsBetter) {
statusText = Math.abs(relativeDifferenceWithTarget * 100).toFixed(2) + '% ' + (smallerIsBetter ? 'below' : 'above') + ' target';
status = 'better';
} else if (relativeDifferenceWithTarget)
statusText = Math.abs(relativeDifferenceWithTarget * 100).toFixed(2) + '% until target';
return {class: status, text: statusText};
}
function addPlotDataForRun(plotData, name, runs, color, interactive) {
var entry = {color: color, data: runs.meanPlotData()};
if (!interactive) {
entry.clickable = false;
entry.hoverable = false;
}
if (runs.hasConfidenceInterval()) {
var lowerName = name.toLowerCase();
var confienceEntry = $.extend(true, {}, entry, {lines: {lineWidth: 0}, clickable: false, hoverable: false});
plotData.push($.extend(true, {}, confienceEntry, {id: lowerName, data: runs.upperConfidencePlotData()}));
plotData.push($.extend(true, {}, confienceEntry,
{fillBetween: lowerName, lines: {fill: 0.3}, data: runs.lowerConfidencePlotData()}));
}
plotData.push(entry);
}
function createSummaryRowMarkup(name, runs) {
return '<tr><th>' + name + '</th><td>' + runs.lastResult().label() + '</td></tr>';
}
function buildLabelWithLinks(build, previousBuild) {
function linkifyIfNotNull(label, url) {
return url ? '<a href="' + url + '" target="_blank">' + label + '</a>' : label;
}
var formattedRevisions = build.formattedRevisions(previousBuild);
var buildInfo = {
'Commit': build.formattedTime(),
'Build': linkifyIfNotNull(build.buildNumber(), build.buildUrl()) + '(' + build.formattedBuildTime() + ')'
};
for (var repositoryName in formattedRevisions)
buildInfo[repositoryName] = linkifyIfNotNull(formattedRevisions[repositoryName].label, formattedRevisions[repositoryName].url);
var markup = '';
for (var key in buildInfo)
markup += '<tr><th>' + key + '</th><td>' + buildInfo[key] + '</td>';
return '<tr>' + markup + '</tr>';
}
function Chart(container, isDashboard, platform, metric, bugTrackers, onClose) {
var linkifiedFullName = metric.fullName;
if (metric.test.url)
linkifiedFullName = '<a href="' + metric.test.url + '">' + linkifiedFullName + '</a>';
var section = $('<section class="chart"><div class="pane"><header><h2>' + linkifiedFullName + '</h2>'
+ '<h3 class="platform">' + platform.name + '</h3></header>'
+ '<div class="meta"><table class="status"><tbody></tbody></table><table class="summaryTable"><tbody></tbody></table></div>'
+ '<div class="overviewPlot"></div></div>'
+ '<div class="plot"></div>'
+ '<div class="unit"></div>'
+ '<svg viewBox="0 0 20 100" class="arrow">'
+ '<g stroke-width="10"><line x1="10" y1="8" x2="10" y2="92" />'
+ '<polygon points="5,85 15,85 10,90" class="downwardArrowHead" />'
+ '<polygon points="5,15 15,15 10,10" class="upwardArrowHead" />'
+ '</g></svg>'
+ '<a href="#" class="toggleYAxis"><svg viewBox="0 0 40 100" class="yaxistoggler"><g stroke-width="10">'
+ '<line x1="20" y1="8" x2="20" y2="82" />'
+ '<polygon points="15,15 25,15 20,10" />'
+ '<polygon points="15,75 25,75 20,80" />'
+ '<line x1="0" y1="92" x2="40" y2="92" />'
+ '</g></svg></a>'
+ '<a href="#" class="closeButton"><svg viewBox="0 0 100 100">'
+ '<g stroke-width="10"><circle cx="50" cy="50" r="45" fill="transparent"/><polygon points="30,30 70,70" />'
+ '<polygon points="30,70 70,30" /></g></svg></a></section>');
$(container).append(section);
var self = this;
if (onClose) {
section.find('.closeButton').bind('click', function (event) {
event.preventDefault();
section.remove();
charts.splice(charts.indexOf(self), 1);
onClose(self);
return false;
});
} else
section.find('.closeButton').hide();
section.find('.yaxistoggler').bind('click', function (event) {
self.toggleYAxis();
event.preventDefault();
return false;
});
var plotData = [];
var results;
var baseline;
var target;
var tooltip = new Tooltip(container, 'tooltip hoverTooltip');
var bounds;
var plotContainer = section.find('.plot');
var mainPlot;
var overviewPlot;
var clickTooltips = [];
var shouldShowEntireYAxis = false;
this.platform = function () { return platform; }
this.metric = function () { return metric; }
this.populate = function (passedResults, passedBaseline, passedTarget) {
results = passedResults;
baseline = passedBaseline;
target = passedTarget;
var summaryRows = '';
if (target) {
addPlotDataForRun(plotData, 'Target', target, '#039');
summaryRows = createSummaryRowMarkup('Target', target) + summaryRows;
}
if (baseline) {
addPlotDataForRun(plotData, 'Baseline', baseline, '#930');
summaryRows = createSummaryRowMarkup('Baseline', baseline) + summaryRows;
}
addPlotDataForRun(plotData, 'Current', results, '#666', true);
summaryRows = createSummaryRowMarkup('Current', results) + summaryRows;
var status = computeStatus(results.smallerIsBetter(), results.lastResult(), baseline, target);
if (status.text)
summaryRows = '<tr><td colspan="2">' + status.text + '</td></tr>' + summaryRows;
section.addClass(status.class);
section.find('.status tbody').html(buildLabelWithLinks(results.lastResult().build()));
section.find('.summaryTable tbody').html(summaryRows);
if (results.smallerIsBetter()) {
section.find('.arrow .downwardArrowHead').show();
section.find('.arrow .upwardArrowHead').hide();
} else {
section.find('.arrow .downwardArrowHead').hide();
section.find('.arrow .upwardArrowHead').show();
}
}
this.attachMainPlot = function (xMin, xMax) {
if (!bounds)
return;
var mainPlotOptions = $.extend(true, {}, sharedPlotOptions, {
xaxis: {
min: xMin,
},
yaxis: {
tickLength: null,
min: shouldShowEntireYAxis ? 0 : bounds.adjustedMin,
max: shouldShowEntireYAxis ? bounds.max : bounds.adjustedMax,
},
crosshair: {'mode': 'x', color: '#c90', lineWidth: 1},
grid: {clickable: true},
});
if (xMax)
mainPlotOptions.xaxis.max = xMax;
if (isDashboard) {
mainPlotOptions.yaxis.labelWidth = 20;
mainPlotOptions.yaxis.ticks = [mainPlotOptions.yaxis.min, mainPlotOptions.yaxis.max];
mainPlotOptions.grid.autoHighlight = false;
} else {
mainPlotOptions.yaxis.labelWidth = 30;
mainPlotOptions.selection = {mode: "x"};
plotData[plotData.length - 1].points = {show: true, radius: 1};
}
mainPlot = $.plot(plotContainer, plotData, mainPlotOptions);
plotData[plotData.length - 1].points = {};
for (var i = 0; i < clickTooltips.length; i++) {
if (clickTooltips[i])
clickTooltips[i].remove();
}
clickTooltips = [];
}
this.toggleYAxis = function () {
shouldShowEntireYAxis = !shouldShowEntireYAxis;
if (currentZoom)
this.attachMainPlot(currentZoom.from, currentZoom.to);
else
this.attachMainPlot(minTime);
}
this.zoom = function (from, to) {
this.attachMainPlot(from, to);
if (overviewPlot)
overviewPlot.setSelection({xaxis: {from: from, to: to}}, true);
}
this.clearZoom = function () {
this.attachMainPlot(minTime);
if (overviewPlot)
overviewPlot.clearSelection();
}
this.setCrosshair = function (pos) {
if (mainPlot)
mainPlot.setCrosshair(pos);
}
this.clearCrosshair = function () {
if (mainPlot)
mainPlot.clearCrosshair();
}
this.hideTooltip = function() {
if (tooltip)
tooltip.hide();
}
function toggleClickTooltip(index, pageX, pageY) {
if (clickTooltips[index])
clickTooltips[index].toggle();
else {
// FIXME: Put this on URLState.
var newTooltip = new Tooltip(container, 'tooltip clickTooltip');
showTooltipWithResults(newTooltip, pageX, pageY, results.resultAt(index), results.resultAt(index - 1));
newTooltip.bindClick(function () { toggleClickTooltip(index, pageX, pageY); });
newTooltip.bindMouseEnter(function () { tooltip.hide(); });
clickTooltips[index] = newTooltip;
}
tooltip.hide();
}
function showTooltipWithResults(tooltip, x, y, result, resultToCompare) {
var newBugUrls = '';
if (resultToCompare) {
var title = (resultToCompare.isBetterThan(result) ? 'REGRESSION: ' : '') + result.metric().fullName
+ ' got ' + result.formattedProgressionOrRegression(resultToCompare)
+ ' around ' + result.build().formattedTime();
var revisions = result.build().formattedRevisions(resultToCompare.build());
for (var trackerId in bugTrackers) {
var tracker = bugTrackers[trackerId];
var repositories = tracker.repositories;
var description = 'Platform: ' + result.build().platform().name + '\n\n';
for (var i = 0; i < repositories.length; ++i) {
var repositoryName = repositories[i];
var revision = revisions[repositoryName];
if (!revision)
continue;
if (revision.url)
description += repositoryName + ': ' + revision.url;
else
description += revision.label;
description += '\n';
}
var url = tracker.newBugUrl
.replace(/\$title/g, encodeURIComponent(title))
.replace(/\$description/g, encodeURIComponent(description))
.replace(/\$link/g, encodeURIComponent(location.href));
if (newBugUrls)
newBugUrls += ',';
newBugUrls += ' <a href="' + url + '" target="_blank">' + tracker.name + '</a>';
}
newBugUrls = 'File:' + newBugUrls;
}
tooltip.show(x, y, result.label(resultToCompare) + '<table>'
+ buildLabelWithLinks(result.build(), resultToCompare ? resultToCompare.build() : null) + '</table>'
+ newBugUrls);
}
tooltip.bindClick(function () {
if (tooltip.currentItem)
toggleClickTooltip(tooltip.currentItem.dataIndex, tooltip.currentItem.pageX, tooltip.currentItem.pageY);
});
function closestItemForPageXRespectingPlotOffset(item, plot, series, pageX) {
if (!series || !series.data.length)
return null;
var offset = $(plotContainer).offset();
var points = series.datapoints.points;
var size = series.datapoints.pointsize;
var xInPlot = pageX - offset.left;
if (xInPlot < plot.getPlotOffset().left)
return null;
if (item)
return item;
var previousPoint;
var index = 0;
while (1) {
var currentPoint = plot.pointOffset({x: points[index * size], y: points[index * size + 1]});
if (xInPlot < currentPoint.left) {
if (previousPoint && xInPlot < (previousPoint.left + currentPoint.left) / 2) {
index -= 1;
currentPoint = previousPoint;
}
break;
}
if (index + 1 >= series.data.length)
break;
previousPoint = currentPoint;
index++;
}
// Ideally we want to return a real item object but flot doesn't provide an API to obtain one
// so create an object that contain properties we use.
return {dataIndex: index, pageX: offset.left + currentPoint.left, pageY: offset.top + currentPoint.top};
}
// Return a plot generator. This function is called when we change the number of days to show.
this.attach = function () {
if (!results)
return;
bounds = computeYAxisBoundsToFitLines(minTime, results, baseline, target);
var overviewContainer = section.find('.overviewPlot');
if (!isDashboard) {
overviewPlot = $.plot(overviewContainer, plotData,
$.extend(true, {}, sharedPlotOptions, {
xaxis: {
min: minTime,
// The maximum number of ticks we can fit on the overflow plot is 4.
tickSize: [(TestBuild.now() - minTime) / 4 / 1000, 'second']
},
grid: { hoverable: false, clickable: false },
selection: { mode: "x" },
yaxis: {
show: false,
min: bounds.min,
max: bounds.max,
}
}));
$(plotContainer).bind("plotselected", function (event, ranges) { Chart.zoom(ranges.xaxis.from, ranges.xaxis.to); });
$(overviewContainer).bind("plotselected", function (event, ranges) { Chart.zoom(ranges.xaxis.from, ranges.xaxis.to); });
$(overviewContainer).bind("plotunselected", function () { Chart.clearZoom(minTime) });
}
if (currentZoom)
this.zoom(currentZoom.from, currentZoom.to);
else
this.attachMainPlot(minTime);
if (bindPlotEventHandlers)
bindPlotEventHandlers(this);
bindPlotEventHandlers = null;
};
function bindPlotEventHandlers(chart) {
// FIXME: Crosshair should stay where it was between charts.
$(plotContainer).bind("plothover", function (event, pos, item) {
for (var i = 0; i < charts.length; i++) {
if (charts[i] !== chart) {
charts[i].setCrosshair(pos);
charts[i].hideTooltip();
}
}
if (isDashboard)
return;
var data = mainPlot.getData();
item = closestItemForPageXRespectingPlotOffset(item, mainPlot, data[data.length - 1], pos.pageX);
if (!item)
return;
showTooltipWithResults(tooltip, item.pageX, item.pageY, results.resultAt(item.dataIndex), results.resultAt(item.dataIndex - 1));
tooltip.currentItem = item;
});
$(plotContainer).bind("mouseleave", function (event) {
var offset = $(plotContainer).offset();
if (offset.left <= event.pageX && offset.top <= event.pageY && event.pageX <= offset.left + $(plotContainer).outerWidth()
&& event.pageY <= offset.top + $(plotContainer).outerHeight())
return 0;
for (var i = 0; i < charts.length; i++) {
charts[i].clearCrosshair();
charts[i].hideTooltip();
}
});
if (isDashboard) { // FIXME: This code doesn't belong here.
$(plotContainer).bind('click', function (event) {
openChart(results.platform(), results.metric());
});
} else {
$(plotContainer).bind("plotclick", function (event, pos, item) {
if (tooltip.currentItem)
toggleClickTooltip(tooltip.currentItem.dataIndex, tooltip.currentItem.pageX, tooltip.currentItem.pageY);
});
}
}
charts.push(this);
}
Chart.clear = function () {
charts = [];
}
Chart.setMinTime = function (newMinTime) {
minTime = newMinTime;
for (var i = 0; i < charts.length; i++)
charts[i].attach();
}
Chart.onzoomchange = function (from, to) { };
Chart.zoom = function (from, to) {
currentZoom = {from: from, to: to};
for (var i = 0; i < charts.length; i++)
charts[i].zoom(from, to);
this.onzoomchange(from, to);
}
Chart.clearZoom = function (minTime) {
currentZoom = undefined;
for (var i = 0; i < charts.length; i++)
charts[i].clearZoom();
this.onzoomchange();
}
window.Chart = Chart;
})();
// FIXME: We need to devise a way to fetch runs in multiple chunks so that
// we don't have to fetch the entire time series to just show the last 3 days.
// FIXME: We should do a mass-fetch where we fetch JSONs for multiple runs at once.
function fetchTest(repositories, builders, filename, platform, metric, callback) {
function createRunAndResults(rawRuns) {
if (!rawRuns)
return null;
var runs = new PerfTestRuns(metric, platform);
var results = rawRuns.map(function (rawRun) {
// FIXME: Creating PerfTestResult and keeping them alive in memory all the time seems like a terrible idea.
// We should create PerfTestResult on demand.
return new PerfTestResult(runs, rawRun, new TestBuild(repositories, builders, platform, rawRun));
});
runs.setResults(results.sort(function (a, b) { return a.build().time() - b.build().time(); }));
return runs;
}
$.getJSON('api/runs/' + filename + '?cache=true', function (response) {
var data = response.configurations;
callback(createRunAndResults(data.current), createRunAndResults(data.baseline), createRunAndResults(data.target));
});
}
function fileNameFromPlatformAndTest(platformId, testId) {
return platformId + '-' + testId + '.json';
}
function init() {
var allPlatforms;
var tests = [];
var fullNameToMetric;
var dashboardPlatforms;
var runsCache = {}; // FIXME: We need to clear this cache at some point.
var repositories;
var builders;
var bugTrackers;
// FIXME: Show some error message when we get 404.
function getOrFetchTest(platform, metric, callback) {
var filename = fileNameFromPlatformAndTest(platform.id, metric.id);
var caches = runsCache[filename];
if (caches)
setTimeout(function () { callback(caches.current, caches.baseline, caches.target); }, 0);
else {
fetchTest(repositories, builders, filename, platform, metric, function (current, baseline, target) {
runsCache[filename] = {current:current, baseline:baseline, target:target};
callback(current, baseline, target);
});
}
}
function showDashboard() {
var dashboardTable = $('<table id="dashboard"></table>');
$('#mainContents').html(dashboardTable);
// Split dashboard platforms into groups of three so that it doesn't grow horizontally too much.
// FIXME: Make this adaptive.
for (var i = 0; i < Math.ceil(dashboardPlatforms.length / 3); i++)
addPlatformsToDashboard(dashboardTable, dashboardPlatforms.slice(i * 3, (i + 1) * 3));
URLState.remove('chartList');
}
function addPlatformsToDashboard(dashboardTable, selectedPlatforms) {
var header = document.createElement('thead');
selectedPlatforms.forEach(function (platform) {
var cell = document.createElement('th');
$(cell).text(platform.name);
header.appendChild(cell);
});
dashboardTable.append(header);
var tbody = $(document.createElement('tbody'));
dashboardTable.append(tbody);
var row = document.createElement('tr');
tbody.append(row);
selectedPlatforms.forEach(function (platform) {
var cell = document.createElement('td');
row.appendChild(cell);
platform.metrics.forEach(function (metric) {
var chart = new Chart(cell, true, platform, metric, bugTrackers);
getOrFetchTest(platform, metric, function (results, baseline, target) {
// FIXME: We shouldn't rely on the order in which XHR finishes to order plots.
if (dashboardTable.parent().length) {
chart.populate(results, baseline, target);
chart.attach();
}
});
});
});
}
function showCharts(lists) {
var chartsContainer = document.createElement('section');
chartsContainer.id = 'charts';
$('#mainContents').html(chartsContainer);
var testPicker = document.createElement('section');
testPicker.id = 'testPicker';
function addOption(select, label, value) {
var option = document.createElement('option');
option.appendChild(document.createTextNode(label));
if (value)
option.value = value;
select.appendChild(option);
}
var testList = document.createElement('select');
testList.id = 'testList';
testPicker.appendChild(testList);
for (var i = 0; i < tests.length; ++i) {
if (tests[i].parentTest)
continue;
addOption(testList, tests[i].fullName, tests[i].id);
}
var metricList = document.createElement('select');
metricList.id = 'metricList';
testPicker.appendChild(metricList);
var platformList = document.createElement('select');
platformList.id = 'platformList';
testPicker.appendChild(platformList);
const OPTION_VALUE_FOR_ALL = '-';
addOption(platformList, 'All platforms', OPTION_VALUE_FOR_ALL);
for (var i = 0; i < allPlatforms.length; ++i)
addOption(platformList, allPlatforms[i].name);
testList.onchange = function () {
while (metricList.firstChild)
metricList.removeChild(metricList.firstChild);
var metricsGroup = document.createElement('optgroup');
metricsGroup.label = 'Metrics';
metricList.appendChild(metricsGroup);
addOption(metricsGroup, 'All metrics', OPTION_VALUE_FOR_ALL);
for (var i = 0; i < tests.length; ++i) {
if (tests[i].id == testList.value) {
var selectedTest = tests[i];
for (var j = 0; j < selectedTest.metrics.length; ++j) {
var fullName = selectedTest.metrics[j].fullName;
var relativeName = fullName.replace(selectedTest.fullName, '').replace(/^[:/]/, '');
addOption(metricsGroup, relativeName, fullName);
}
}
}
var subtestsGroup = document.createElement('optgroup');
subtestsGroup.label = 'Tests';
metricList.appendChild(subtestsGroup);
addOption(subtestsGroup, 'All subtests', OPTION_VALUE_FOR_ALL);
for (var i = 0; i < tests.length; ++i) {
if (!tests[i].parentTest || tests[i].parentTest.id != testList.value)
continue;
var subtest = tests[i];
var selectedTest = subtest.parentTest;
for (var j = 0; j < subtest.metrics.length; ++j) {
var fullName = subtest.metrics[j].fullName;
var relativeName = fullName.replace(selectedTest.fullName, '').replace(/^[:/]/, '');
addOption(subtestsGroup, relativeName, fullName);
}
}
}
metricList.onchange = function () {
var metric = fullNameToMetric[metricList.value];
var shouldAddAllMetrics = metricList.value === OPTION_VALUE_FOR_ALL;
for (var i = 0; i < platformList.options.length; ++i) {
var option = platformList.options[i];
if (option.value === OPTION_VALUE_FOR_ALL) // Adding all metrics for all platforms will be too slow.
option.disabled = shouldAddAllMetrics;
else {
var platform = nameToPlatform[option.value];
var platformHasMetric = platform.metrics.indexOf(metric) >= 0;
option.disabled = !shouldAddAllMetrics && !platformHasMetric;
}
}
}
testList.onchange();
metricList.onchange();
$(testPicker).append(' <a href="">Add Chart</a>');
function removeChart(chart) {
for (var i = 0; i < chartList.length; i++) {
if (chartList[i][0] == chart.platform().name && chartList[i][1] == chart.metric().fullName) {
chartList.splice(i, 1);
break;
}
}
URLState.set('chartList', JSON.stringify(chartList));
}
function createChartFromListPair(platformName, metricFullName) {
var platform = nameToPlatform[platformName];
var metric = fullNameToMetric[metricFullName]
var chart = new Chart(chartsContainer, false, platform, metric, bugTrackers, removeChart);
getOrFetchTest(platform, metric, function (results, baseline, target) {
if (!chartsContainer.parentNode)
return;
chart.populate(results, baseline, target);
chart.attach();
});
}
$(testPicker).children('a').bind('click', function (event) {
event.preventDefault();
var newChartList = [];
if (platformList.value === OPTION_VALUE_FOR_ALL) {
for (var i = 0; i < allPlatforms.length; ++i) {
createChartFromListPair(allPlatforms[i].name, metricList.value);
newChartList.push([allPlatforms[i].name, metricList.value]);
}
} else if (metricList.value === OPTION_VALUE_FOR_ALL) {
var group = metricList.selectedOptions[0].parentNode;
var metricsToAdd = [];
for (var i = 0; i < group.children.length; i++) {
var metric = group.children[i].value;
if (metric == OPTION_VALUE_FOR_ALL)
continue;
createChartFromListPair(platformList.value, metric);
newChartList.push([platformList.value, metric]);
}
} else {
createChartFromListPair(platformList.value, metricList.value);
newChartList.push([platformList.value, metricList.value]);
}
chartList = chartList.concat(newChartList);
URLState.set('chartList', JSON.stringify(chartList));
return false;
});
$('#mainContents').append(testPicker);
var chartList = [];
try {
chartList = JSON.parse(URLState.get('chartList', ''));
// FIXME: Should we verify that platform and test names are valid here?
} catch (exception) {
// Ignore any exception thrown by parse.
}
redirectChartsToV3(chartList);
chartList.forEach(function (item) { createChartFromListPair(item[0], item[1]); });
}
function redirectChartsToV3(chartList)
{
var v3URLParams = [];
var numberOfDays = URLState.get('days');
if (parseInt(numberOfDays) == numberOfDays)
v3URLParams.push('since=' + (+Date.now() - numberOfDays * 24 * 3600 * 1000));
var v3PaneList = [];
for (var item of chartList) {
var platform = nameToPlatform[item[0]];
var metric = fullNameToMetric[item[1]];
if (platform && metric)
v3PaneList.push(`(${platform.id}-${metric.id})`);
}
v3URLParams.push(`paneList=(${v3PaneList.join('-')})`);
try {
var zoomValues = JSON.parse(URLState.get('zoom', '[]'));
if (zoomValues.length == 2)
v3URLParams.push(`zoom=(${zoomValues[0]}-${zoomValues[1]})`);
} catch (error) { }
location.href = '/v3/#/charts?' + v3URLParams.join('&');
}
// FIXME: We should use exponential slider for charts page where we expect to have
// the full JSON as opposed to the dashboard where we can't afford loading really big JSON files.
var exponential = true;
(function () {
var input = $('#numberOfDays')[0];
var updating = 0;
function updateSpanAndCall(newNumberOfDays) {
$('#numberOfDays').next().text(newNumberOfDays + ' days');
// FIXME: This is likely broken for non-PST.
Chart.setMinTime(Date.now() - newNumberOfDays * 24 * 3600 * 1000);
}
$('#numberOfDays').bind('change', function () {
var newNumberOfDays = Math.round(exponential ? Math.exp(input.value) : input.value);
URLState.remove('zoom');
Chart.clearZoom();
URLState.set('days', newNumberOfDays);
updateSpanAndCall(newNumberOfDays);
});
function onchange() {
var newNumberOfDays = URLState.get('days', Math.round(exponential ? Math.exp(input.defaultValue) : input.defaultValue));
$('#numberOfDays').val(exponential ? Math.log(newNumberOfDays) : newNumberOfDays);
updateSpanAndCall(newNumberOfDays);
}
URLState.watch('days', onchange);
onchange();
})();
URLState.watch('mode', function () { setMode(URLState.get('mode')); });
URLState.watch('chartList', function (changedStates) {
var modeChanged = changedStates.indexOf('mode') >= 0;
if (URLState.get('mode') == 'charts' && !modeChanged)
setMode('charts');
});
function zoomChartsIfParsedCorrectly() {
try {
zoomValues = JSON.parse(URLState.get('zoom', '[]'));
if (zoomValues.length != 2)
return false;
Chart.zoom(parseFloat(zoomValues[0]), parseFloat(zoomValues[1]));
} catch (exception) {
// Ignore all exceptions thrown by JSON.parse.
}
return true;
}
URLState.watch('zoom', function (changedStates) {
var modeChanged = changedStates.indexOf('mode') >= 0;
if (URLState.get('mode') == 'charts' && !modeChanged) {
if (!zoomChartsIfParsedCorrectly())
return Chart.clearZoom();
}
});
Chart.onzoomchange = function (from, to) {
if (from && to)
URLState.set('zoom', JSON.stringify([from, to]));
else
URLState.remove('zoom');
}
window.openChart = function (platform, metric) {
URLState.set('chartList', JSON.stringify([[platform.name, metric.fullName]]));
setMode('charts');
}
window.setMode = function (newMode) {
URLState.set('mode', newMode);
Chart.clear();
if (newMode == 'dashboard') {
URLState.remove('zoom');
Chart.clearZoom();
showDashboard();
} else { // FIXME: Dynamically obtain the list of tests to show.
showCharts();
zoomChartsIfParsedCorrectly();
}
}
function fullName(test) {
var names = [];
do {
names.push(test.name);
test = test.parentTest;
} while (test);
names.reverse();
return names.join('/');
}
// Perhaps we want an ordered list of platforms.
$.getJSON('data/manifest.json', function (data) {
var manifest = data;
nameToTest = {};
for (var testId in manifest.tests) {
var test = manifest.tests[testId];
test.parentTest = manifest.tests[manifest.tests[testId].parentId];
test.id = testId;
test.metrics = [];
tests.push(test);
}
tests.forEach(function (test) {
test.fullName = fullName(test);
nameToTest[test.fullName] = test;
});
tests.sort(function (a, b) {
if (a.fullName < b.fullName)
return -1;
if (a.fullName > b.fullName)
return 1;
return 0;
});
fullNameToMetric = {};
for (var metricId in manifest.metrics) {
var entry = manifest.metrics[metricId];
entry.id = metricId;
entry.test = manifest.tests[entry.test];
entry.fullName = entry.test.fullName + ':' + entry.name;
if (entry.aggregator)
entry.fullName += ':' + entry.aggregator;
entry.test.metrics.push(entry);
fullNameToMetric[entry.fullName] = entry;
}
tests.forEach(function (test) {
test.metrics.sort(function (a, b) {
if (a.name < b.name)
return -1;
if (a.name > b.name)
return 1;
return 0;
});
});
allPlatforms = [];
nameToPlatform = {};
for (var platformId in manifest.all) {
var platform = manifest.all[platformId];
platform.id = platformId;
allPlatforms.push(platform);
nameToPlatform[platform.name] = platform;
// FIXME: Sort tests
for (var i = 0; i < platform.metrics.length; ++i)
platform.metrics[i] = manifest.metrics[platform.metrics[i]];
}
allPlatforms.sort(function (a, b) { return a.name < b.name ? -1 : (a.name > b.name ? 1 : 0); });
dashboardPlatforms = [];
for (var platformId in manifest.dashboard) {
var platform = manifest.dashboard[platformId];
platform.id = platformId;
for (var i = 0; i < platform.metrics.length; ++i)
platform.metrics[i] = manifest.metrics[platform.metrics[i]];
platform.metrics.sort(function (a, b) { return a.fullName < b.fullName ? -1 : (a.fullName > b.fullName ? 1 : 0); });
dashboardPlatforms.push(manifest.dashboard[platformId]);
}
dashboardPlatforms.sort(function (a, b) { return a.name < b.name ? -1 : (a.name > b.name ? 1 : 0); });
repositories = manifest.repositories;
builders = manifest.builders;
bugTrackers = manifest.bugTrackers;
document.title = manifest.siteTitle;
document.getElementById('siteTitle').textContent = manifest.siteTitle;
setMode(URLState.get('mode', 'dashboard'));
});
}
window.addEventListener('DOMContentLoaded', init, false);
</script>
</head>
<body>
<header id="title">
<h1><a id="siteTitle" href="/">Perf Monitor</a></h1>
<ul>
<li id="numberOfDaysPicker"><input id="numberOfDays" type="range" min="1" max="5.9" step="0.001" value="2.3"><span class="output"></span></li>
<li><a href="javascript:setMode('dashboard');">Dashboard</a></li>
<li><a href="javascript:setMode('charts');">Charts</a></li>
</ul>
</header>
<div id="mainContents"></div>
</body>
</html>