blob: d1116973421a32c5efec8172255b9981b9c6b0c5 [file] [log] [blame]
<!--
Copyright 2022 The Go Authors. All rights reserved.
Use of this source code is governed by a BSD-style
license that can be found in the LICENSE file.
-->
<!DOCTYPE html>
<html lang="en">
<head>
<title>Go Performance Dashboard</title>
<link rel="icon" href="https://go.dev/favicon.ico"/>
<link rel="stylesheet" href="./static/style.css"/>
<script src="https://ajax.googleapis.com/ajax/libs/d3js/7.4.2/d3.min.js"></script>
<script src="./third_party/bandchart/bandchart.js"></script>
<script src="./static/range.js"></script>
</head>
<body class="Dashboard">
<!-- header class="Dashboard-topbar" style="background: antiquewhite;">
<div>
A banner isn't actively displayed at this time. This serves as a placeholder
that can be used if a banner does need to be displayed.
</div>
</header -->
<header class="Dashboard-topbar">
<h1>
<a href="https://farmer.golang.org/">Go Build Coordinator</a>
</h1>
<nav>
<ul>
<li><a href="https://build.golang.org/">Build Dashboard</a></li>
<li><a href="/dashboard">Performance Dashboard</a></li>
<li><a href="https://farmer.golang.org/builders">Builders</a></li>
<span class="left-separator"></span>
<li><a class="alt-nav-button" href="?benchmark=all">Go Benchmark List</a>
<li><a class="alt-nav-button" href="?benchmark=all&regressions=true">Go Regressions</a>
<li><a class="alt-nav-button" href="?repository=tools&branch=latest-release">x/tools Dashboard</a>
<li><a class="alt-nav-button" href="?repository=tools&benchmark=all">x/tools Benchmark List</a>
</ul>
</nav>
</header>
<nav class="Dashboard-controls">
<form autocomplete="off" action="./">
<ul>
<li>
<div class="Dashboard-search-benchmark">
<input id="benchmark-input" type="text" name="benchmark" placeholder="Search benchmarks (case sensitive)..." />
</div>
<div class="Dashboard-search-unit">
<input id="unit-input" type="text" name="unit" placeholder="Unit (optional)" />
</div>
</li>
<li>
Repository:
<select id="repository-select" name="repository">
<option>go</option>
<option>tools</option>
</select>
Go branch:
<select id="branch-select" name="branch"></select>
Platform:
<select id="platform-select" name="platform">
<option>all</option>
<option>linux/amd64</option>
<option>linux/arm64</option>
</select>
Duration (days):
<div class="Dashboard-duration">
<input id="days-input" type="number" name="days" value="30" />
</div>
End (UTC): <input id="end-input" type="datetime-local" name="end" />
<input type="submit">
</li>
</ul>
</form>
</nav>
<div class="Dashboard-documentation">
<p>
Each graph displays benchmark results relative to its baseline
commit, which is the latest stable release (e.g., 1.18.3) at
the time of testing. The 95% confidence interval is displayed
in light gray, if available. On hover, the graph displays the
benchmarked commit at that point. Click that commit to go to
a more detailed page highlighting the results at that commit
in context. You may also reach this page by clicking the name
of the unit right above each graph.
</p>
<p>
Note that some commits are not tested, so there could be
multiple commits (not shown) between two points on the graph.
See the columns with titles ending in <code>perf_vs_release</code>
on the
<a href="https://ci.chromium.org/p/golang/g/go-gotip/console">build dashboard</a>.
Individually tested commits have their own green box. A single
box that covers multiple commits indicates that only the latest
commit in the range was tested.
</p>
<p>
The 'Go branch' selection above is the Go branch that benchmarking
ran against on
<a href="https://ci.chromium.org/ui/p/golang">the build dashboard</a>.
</p>
<p>
Further documentation is available on the
<a href=https://go.dev/wiki/PerformanceMonitoring>wiki</a>.
</p>
</div>
<grid id="dashboard">
<grid id="loading" class="Dashboard-section Dashboard-section-expand">
<h2 class="Dashboard-title" id="loading">Loading...</h2>
</grid>
</grid>
<script>
// minViewPercentDelta represents the minimum range we're willing to
// let the Y axis have for charts and X axis for ranges in the per-unit
// view. In both cases the unit of this value is a delta percent, hence
// the name.
//
// This constant exists because allowing the axis' range to be arbitrarily
// small produces results that are really noisy visually, even though they
// represent virtually no change. For instance, a relatively low-noise
// series of benchmark results, with a min delta of -0.05% and +0.05%
// might appear really noisy if we "zoom" in too far, when in actuality
// the amount of noise is incredibly low.
const minViewDeltaPercent = 0.025;
// HTML to inject when there's no data found.
const noDataHTML = `
<grid class="Dashboard-section Dashboard-section-expand">
<h2 class="Dashboard-title">No data</h2>
<p class="Dashboard-documentation">
Found no data. Consider trying a different time range. Note also that subrepositories like tools have
no data against the Go master branch, in which case, try picking a release branch.
</p>
</grid>
`
function removeLoadingMessage() {
let loading = document.getElementById("loading");
if (loading) {
loading.parentNode.removeChild(loading);
}
}
let allBenchmarkChartSections = [];
let allBenchmarkChartSectionsByGroup = new Map();
function addCharts(benchmarks, repository, goBranch, commits, regressions) {
let dashboard = document.getElementById("dashboard");
removeLoadingMessage();
for (const b in benchmarks) {
const bench = benchmarks[b];
let sec = null;
const groupKey = bench.Name+'+'+bench.Platform;
if (allBenchmarkChartSectionsByGroup.has(groupKey)) {
sec = allBenchmarkChartSectionsByGroup.get(groupKey)
} else {
let section = document.createElement("grid");
section.classList.add("Dashboard-section");
let link = document.createElement("a");
link.href = "?benchmark=" + bench.Name + "&platform=" + bench.Platform + "&repository=" + repository + "&branch=" + goBranch;
link.textContent = bench.Name + " (" + bench.Platform + ")";
let title = document.createElement("h2");
title.classList.add("Dashboard-title");
title.appendChild(link);
section.appendChild(title);
grid = document.createElement("grid");
grid.classList.add("Dashboard-grid");
section.appendChild(grid);
sec = {
Section: section,
Grid: grid,
Items: [],
Name: bench.Name,
Platform: bench.Platform,
Regression: bench.Regression,
};
allBenchmarkChartSections.push(sec);
allBenchmarkChartSectionsByGroup.set(groupKey, sec);
}
let item = document.createElement("div");
item.classList.add("Dashboard-grid-item");
if (bench.Regression) {
const p = document.createElement("p");
p.classList.add("Dashboard-regression-description");
const r = bench.Regression;
if (r.DeltaIndex >= 0) {
// Generate some text indicating the regression.
const rd = bench.Values[r.DeltaIndex];
const regression = (Math.abs(r.Change)*100).toFixed(2);
const shortCommit = rd.CommitHash.slice(0, 7);
let diffText = "regression";
let isRegression = true;
if (r.Change < 0) {
// Note: r.Change already has its sign flipped for HigherIsBetter.
// Positive always means regression, negative always means improvement.
diffText = "improvement";
isRegression = false;
}
p.innerHTML = `${regression}% ${diffText}, ${(r.Delta*100).toFixed(2)}%-point change at <a href="?benchmark=${bench.Name}&unit=${bench.Unit}&platform=${bench.Platform}#${commitToId(rd.CommitHash)}">${shortCommit}</a>.`;
// Add a link to file a bug.
if (isRegression) {
const title = `affected/package: ${regression}% regression in ${bench.Name} ${bench.Unit} on ${bench.Platform} at ${shortCommit}`;
const body = `Discovered a regression in ${bench.Unit} of ${regression}% for benchmark ${bench.Name} at ${shortCommit}.\n\n<ADD MORE DETAILS>.`
let query = `?title=${encodeURIComponent(title)}&body=${encodeURIComponent(body)}&labels=Performance`;
p.innerHTML += ` <a href="https://github.com/golang/go/issues/new${query}">File an issue</a>.`;
} else {
// Include a grinning emoji if it's an improvement.
p.innerHTML += ` <span style="font-style: normal;">&#128513;</span>`;
}
} else {
continue;
}
item.appendChild(p);
}
item.appendChild(BandChart(bench.Values, {
benchmark: bench.Name,
unit: bench.Unit,
platform: bench.Platform,
repository: repository,
minViewDeltaPercent: minViewDeltaPercent,
higherIsBetter: bench.HigherIsBetter,
history: commits,
}));
sec.Items.push({Unit: bench.Unit, Item: item});
}
if (allBenchmarkChartSections.length === 0) {
dashboard.innerHTML = noDataHTML;
} else {
allBenchmarkChartSections.sort(function(a, b) {
if (a.Regression) {
let ra = a.Regression;
let rb = b.Regression;
// Regressions w/ a delta index come first
if ((ra.DeltaIndex < 0) != (rb.DeltaIndex < 0)) {
if (rb.DeltaIndex < 0) {
return -1;
} else {
return 1;
}
}
// Put larger regressions first.
if (ra.Change > rb.Change) {
return -1;
} else {
return 1;
}
}
if (a.Name === b.Name) {
return a.Platform < b.Platform ? -1 : 1;
}
return a.Name < b.Name ? -1 : 1;
});
// N.B. Re-appending moves a child into the right location.
for (const s in allBenchmarkChartSections) {
const sec = allBenchmarkChartSections[s];
sec.Items.sort(function(a, b) {
return a.Unit < b.Unit ? -1 : 1;
});
for (const i in sec.Items) {
sec.Grid.appendChild(sec.Items[i].Item);
}
if (sec.Items.length > 0) {
dashboard.appendChild(sec.Section);
}
}
}
}
function commitToId(commitHash) {
return "commit" + commitHash;
}
function idToCommit(id) {
if (id && id.startsWith("commit")) {
return id.slice(6);
}
return null;
}
function addTable(bench, unit, platform, repository, goBranch, commits) {
let commitSelected = idToCommit(window.location.hash.slice(1));
let dashboard = document.getElementById("dashboard");
removeLoadingMessage();
let section = document.createElement("grid");
section.classList.add("Dashboard-section");
section.classList.add("Dashboard-section-expand");
dashboard.appendChild(section);
let link = document.createElement("a");
link.href = "?benchmark=" + bench.Name + "&unit=" + unit + "&platform=" + platform + "&repository=" + repository + "&branch=" + goBranch;
link.textContent = bench.Name + " (" + unit + ", " + platform + ")";
let title = document.createElement("h2");
title.classList.add("Dashboard-title");
title.appendChild(link);
section.appendChild(title);
const table = document.createElement("table");
table.classList.add("Dashboard-table");
section.appendChild(table);
const createCell = function(text, header) {
let elemType = "td";
if (header) {
elemType = "th";
}
const elem = document.createElement(elemType);
elem.textContent = text;
return elem;
}
const createCommitCell = function(commit, repository) {
const commitHash = createCell("", false);
const commitLink = document.createElement("a");
commitLink.href = "https://go.googlesource.com/" + repository + "/+show/" + commit;
commitLink.textContent = commit.slice(0, 7);
commitHash.appendChild(commitLink);
commitHash.classList.add("Dashboard-table-commit");
return commitHash;
}
// Create the header.
const header = document.createElement("tr");
header.appendChild(createCell("Date", true));
header.appendChild(createCell("Experiment commit", true));
header.appendChild(createCell("Delta", true));
header.appendChild(createCell("Baseline commit", true));
header.appendChild(createCell("x/benchmarks commit", true));
table.appendChild(header);
// Find the min and max.
let min = bench.Values[0].Low;
let max = bench.Values[0].High;
for (let i = 1; i < bench.Values.length; i++) {
if (bench.Values[i].Low < min) {
min = bench.Values[i].Low;
}
if (bench.Values[i].High > max) {
max = bench.Values[i].High;
}
}
// Clamp for presentation.
if (min < 0 && min > -minViewDeltaPercent) {
min = -minViewDeltaPercent
}
if (max > 0 && max < minViewDeltaPercent) {
max = minViewDeltaPercent
}
if (max-min < 2*minViewDeltaPercent) {
const amt = (2*minViewDeltaPercent-(max-min))/2;
max += amt;
min -= amt;
}
// Create a map of hashes to values.
let valuesByCommit = new Map();
for (let i = 0; i < bench.Values.length; i++) {
valuesByCommit.set(bench.Values[i].CommitHash, bench.Values[i])
}
// Iterate backwards, showing the most recent first.
for (let i = commits.length-1; i >= 0; i--) {
const c = commits[i];
// Create a row per value.
const row = document.createElement("tr");
if (commitSelected && commitSelected === c.Hash) {
row.classList.add("selected");
}
// Commit date.
row.appendChild(createCell(Intl.DateTimeFormat([], {
dateStyle: "long",
timeStyle: "short",
}).format(c.Date), false));
// Commit hash.
const commitCell = createCommitCell(c.Hash, repository);
commitCell.id = commitToId(c.Hash);
row.appendChild(commitCell);
if (valuesByCommit.has(c.Hash)) {
const v = valuesByCommit.get(c.Hash);
// Range visualization.
const range = createCell("", false);
range.appendChild(Range(v.Low, v.Center, v.High, min, max, 640, 48, bench.Unit, bench.HigherIsBetter));
range.classList.add("Dashboard-table-range")
row.appendChild(range);
// Baseline commit hash.
row.appendChild(createCommitCell(v.BaselineCommitHash, repository));
// Benchmarks commit hash.
row.appendChild(createCommitCell(v.BenchmarksCommitHash, "benchmarks"));
} else {
// Row without info.
const range = createCell("", false);
range.appendChild(NoDataRange(min, max, 640, 48));
range.classList.add("Dashboard-table-range");
row.appendChild(range);
const baselineNA = createCell("N/A", false);
baselineNA.classList.add("Dashboard-table-commit");
row.appendChild(baselineNA);
const benchmarksNA = createCell("N/A", false);
benchmarksNA.classList.add("Dashboard-table-commit");
row.appendChild(benchmarksNA);
}
table.appendChild(row);
}
if (commitSelected) {
// Now that we've generated anchors for every commit, let's scroll to the
// right one. The browser won't do this automatically because the anchors
// don't exist when the page is loaded.
const anchor = document.querySelector("#" + commitToId(commitSelected));
window.scrollTo({
top: anchor.getBoundingClientRect().top + window.pageYOffset - 20,
})
}
}
function addBenchmarkList(benchmarks) {
let dashboard = document.getElementById("dashboard");
removeLoadingMessage();
let section = document.createElement("grid");
section.classList.add("Dashboard-section");
section.classList.add("Dashboard-section-expand");
dashboard.appendChild(section);
let title = document.createElement("h2");
title.classList.add("Dashboard-title");
title.textContent = "All " + benchmarks[0].Repository + " benchmarks";
section.appendChild(title);
const list = document.createElement("ul");
for (let i = 0; i < benchmarks.length; i++) {
const bench = benchmarks[i];
const li = document.createElement("li");
let link = document.createElement("a");
let goBranch = "master";
if (bench.Repository != "go") {
goBranch = "latest-release";
}
link.href = "?benchmark=" + bench.Name + "&repository=" + bench.Repository + "&branch=" + goBranch;
link.textContent = bench.Name;
li.appendChild(link);
list.appendChild(li);
}
section.appendChild(list);
}
function failure(name, response) {
let dashboard = document.getElementById("dashboard");
removeLoadingMessage();
let title = document.createElement("h2");
title.classList.add("Dashboard-title");
title.textContent = "Benchmark \"" + name + "\" not found.";
dashboard.appendChild(title);
let message = document.createElement("p");
message.classList.add("Dashboard-documentation");
response.text().then(function(error) {
message.textContent = error;
});
dashboard.appendChild(message);
}
let now = new Date();
// Fill search boxes from query params.
function prefillSearch(formFields) {
let params = new URLSearchParams(window.location.search);
let benchmark = params.get('benchmark');
if (benchmark) {
let input = document.getElementById('benchmark-input');
input.value = benchmark;
}
let unit = params.get('unit');
if (unit) {
let input = document.getElementById('unit-input');
input.value = unit;
}
let repository = params.get('repository');
if (repository) {
let select = document.getElementById('repository-select');
select.value = repository;
}
let platform = params.get('platform');
if (platform) {
let select = document.getElementById('platform-select');
select.value = platform;
}
let branch = params.get('branch');
if (branch) {
let select = document.getElementById('branch-select');
if (branch === "latest-release") {
select.value = formFields.LatestReleaseBranch;
} else {
select.value = branch;
}
}
let days = params.get('days');
if (days) {
let input = document.getElementById('days-input');
input.value = days;
}
let end = params.get('end');
let input = document.getElementById('end-input');
if (end) {
input.value = end;
} else {
// toISOString always uses UTC, then we just chop off the end
// of string to get the datetime-local format of
// 2000-12-31T15:00.
//
// Yes, this is really the suggested approach...
input.value = now.toISOString().slice(0, 16);
}
}
fetch('./formfields.json')
.then(response => {
if (!response.ok) {
throw new Error("Form fields fetch failed");
}
return response.json();
})
.then(function(formFields) {
let select = document.getElementById('branch-select');
for (const i in formFields.Branches) {
var opt = document.createElement('option');
opt.value = formFields.Branches[i];
opt.innerText = formFields.Branches[i];
select.appendChild(opt);
}
var opt = document.createElement('option');
opt.value = "latest-release";
opt.innerText = "Latest release branch";
select.appendChild(opt);
prefillSearch(formFields);
});
// Grab the repository so we can plumb it into UI elements.
let repository = (new URLSearchParams(window.location.search)).get('repository');
if (!repository) {
repository = "go";
}
// Fetch content.
let benchmark = (new URLSearchParams(window.location.search)).get('benchmark');
let unit = (new URLSearchParams(window.location.search)).get('unit');
let platform = (new URLSearchParams(window.location.search)).get('platform');
let goBranch = (new URLSearchParams(window.location.search)).get('branch');
let regressions = (new URLSearchParams(window.location.search)).get('regressions');
if (!goBranch) {
// Assume the same thing the server does when no branch is specified.
goBranch = "master";
}
function fetchData(params, callback) {
let dataURL = './data.json?' + params;
fetch(dataURL)
.then(response => {
if (!response.ok) {
failure(benchmark, response);
throw new Error("Data fetch failed");
}
return response.json();
})
.then(function(data) {
let benchmarks = data.Benchmarks;
// Convert CommitDate to a proper dates.
benchmarks.forEach(function(b) {
b.Values.forEach(function(v) {
v.CommitDate = new Date(v.CommitDate);
});
});
callback(benchmarks);
});
}
let benchmarksURL = './benchmarks.json' + window.location.search; // Pass through all URL params.
fetch(benchmarksURL)
.then(response => {
if (!response.ok) {
failure(benchmark, response);
throw new Error("Data fetch failed");
}
return response.json();
})
.then(function(benchmarks) {
let benchmarkKeys = benchmarks.Benchmarks;
let commits = benchmarks.Commits;
// Render a list of benchmarks if the user asks for all of them.
if (benchmark === 'all' && regressions !== 'true') {
addBenchmarkList(benchmarkKeys);
return;
}
// Convert Date to a proper dates.
commits.forEach(function(c) {
c.Date = new Date(c.Date);
})
// If we have an explicit unit, then there should be just one result.
let callback = null;
if (benchmarkKeys.length === 1 && unit && platform && platform != "all") {
callback = (benchmarks) => addTable(benchmarks[0], unit, platform, repository, goBranch, commits);
} else {
callback = (benchmarks) => addCharts(benchmarks, repository, goBranch, commits);
}
// Customize the home page.
if (!benchmark && repository === "go") {
for (const k in benchmarkKeys) {
let params = new URLSearchParams(window.location.search);
params.set('benchmark', benchmarkKeys[k].Name); // Pass through all URL params, but be specific about the benchmark name.
params.set('unit', 'sec/op');
fetchData(params, callback);
params.set('unit', 'average-RSS-bytes');
fetchData(params, callback);
params.set('unit', 'peak-RSS-bytes');
fetchData(params, callback);
}
} else {
let loadWord = "Loading";
if (regressions === 'true') {
loadWord = "Checking";
}
const popup = document.createElement("div");
popup.classList.add("Dashboard-popup");
popup.textContent = loadWord+" benchmarks 0/"+benchmarkKeys.length.toString();
document.body.appendChild(popup);
// Load all the charts.
let k = 0;
let n = 0;
const max = 4;
function dispatch() {
while (k < benchmarkKeys.length && n < max) {
let params = new URLSearchParams(window.location.search);
params.set('benchmark', benchmarkKeys[k].Name); // Pass through all URL params, but be specific about the benchmark name.
fetchData(params, function(benchmarks) {
n--;
callback(benchmarks);
});
k++;
n++;
popup.textContent = loadWord+" benchmarks "+k.toString()+"/"+benchmarkKeys.length.toString();
}
if (k < benchmarkKeys.length) {
setTimeout(dispatch, 100);
} else {
document.body.removeChild(popup);
}
}
dispatch();
}
});
</script>
</body>
</html>