Merge upstream PR #167: Conditionally show more metric details

See https://github.com/WebKit/JetStream/pull/167

- Collect and report subTimes like subScores
- Only show times if the global .details CSS class is set (the default view is unaffected)
- Also use .details for displaying displayCategoryScores
- Change uiFriendlyDuration to only report 2 significant digits
- Add Benchmark.prototype.wallTime accessor and report that as well
-  Use # as unit symbol for console printing

Change-Id: Ibc603b29c516060823c39c738069466cff57170a
Reviewed-on: https://chromium-review.googlesource.com/c/external/github.com/WebKit/JetStream/+/6976231
Reviewed-by: Victor Gomes <victorgomes@chromium.org>
This commit is contained in:
Camillo Bruni 2025-09-23 16:49:21 +02:00
parent 8270d839bf
commit 515acee6e1
2 changed files with 214 additions and 81 deletions

View file

@ -348,6 +348,13 @@ a.button {
scroll-margin-bottom: 20vh;
}
.benchmark .result.detail {
display: none;
}
.details .benchmark .result.detail {
display: inline-block;
}
.benchmark h4,
.benchmark .result,
.benchmark label,

View file

@ -37,25 +37,8 @@ if (!JetStreamParams.prefetchResources)
this.currentResolve = null;
this.currentReject = null;
let showScoreDetails = false;
let categoryScores = null;
function displayCategoryScores() {
if (!categoryScores)
return;
let scoreDetails = `<div class="benchmark benchmark-done">`;
for (let [category, scores] of categoryScores) {
scoreDetails += `<span class="result">
<span>${uiFriendlyScore(geomeanScore(scores))}</span>
<label>${category}</label>
</span>`;
}
scoreDetails += "</div>";
let summaryElement = document.getElementById("result-summary");
summaryElement.innerHTML += scoreDetails;
categoryScores = null;
document.body.classList.add("details");
}
function getIterationCount(plan) {
@ -81,19 +64,22 @@ function getWorstCaseCount(plan) {
if (isInBrowser) {
document.onkeydown = (keyboardEvent) => {
const key = keyboardEvent.key;
if (key === "d" || key === "D") {
showScoreDetails = true;
if (key === "d" || key === "D")
displayCategoryScores();
}
};
}
function mean(values) {
function sum(values) {
console.assert(values instanceof Array);
let sum = 0;
for (let x of values)
sum += x;
return sum / values.length;
return sum;
}
function mean(values) {
const totalSum = sum(values)
return totalSum / values.length;
}
function geomeanScore(values) {
@ -135,9 +121,25 @@ function uiFriendlyScore(num) {
}
function uiFriendlyDuration(time) {
return `${time.toFixed(3)} ms`;
return `${time.toFixed(2)} ms`;
}
function shellFriendlyLabel(label) {
const namePadding = 40;
return `${label}:`.padEnd(namePadding);
}
const valuePadding = 10;
function shellFriendlyDuration(time) {
return `${uiFriendlyDuration(time)}`.padStart(valuePadding);
}
function shellFriendlyScore(time) {
return `${uiFriendlyScore(time)} # `.padStart(valuePadding);
}
// TODO: Cleanup / remove / merge. This is only used for caching loads in the
// non-browser setting. In the browser we use exclusively `loadCache`,
// `loadBlob`, `doLoadBlob`, `prefetchResourcesForBrowser` etc., see below.
@ -226,7 +228,7 @@ class Driver {
if (isInBrowser)
document.getElementById("benchmark-total-time-score").innerHTML = uiFriendlyNumber(totalTime);
else if (!JetStreamParams.dumpJSONResults)
console.log("Total time:", uiFriendlyNumber(totalTime));
console.log("Total-Time:", uiFriendlyNumber(totalTime));
allScores.push(totalTime);
}
@ -237,10 +239,13 @@ class Driver {
allScores.push(score);
}
categoryScores = new Map;
const categoryScores = new Map();
const categoryTimes = new Map();
for (const benchmark of this.benchmarks) {
for (let category of Object.keys(benchmark.subScores()))
categoryScores.set(category, []);
for (let category of Object.keys(benchmark.subTimes()))
categoryTimes.set(category, []);
}
for (const benchmark of this.benchmarks) {
@ -249,25 +254,55 @@ class Driver {
console.assert(value > 0, `Invalid ${benchmark.name} ${category} score: ${value}`);
arr.push(value);
}
for (let [category, value] of Object.entries(benchmark.subTimes())) {
const arr = categoryTimes.get(category);
console.assert(value > 0, `Invalid ${benchmark.name} ${category} time: ${value}`);
arr.push(value);
}
}
const totalScore = geomeanScore(allScores);
console.assert(totalScore > 0, `Invalid total score: ${totalScore}`);
if (isInBrowser) {
let summaryHtml = `<div class="score">${uiFriendlyScore(totalScore)}</div>
<label>Score</label>`;
summaryHtml += `<div class="benchmark benchmark-done">`;
for (let [category, scores] of categoryScores) {
summaryHtml += `<span class="result detail">
<span>${uiFriendlyScore(geomeanScore(scores))}</span>
<label>${category}</label>
</span>`;
}
summaryHtml += "<br/>";
for (let [category, times] of categoryTimes) {
summaryHtml += `<span class="result detail">
<span>${uiFriendlyDuration(geomeanScore(times))}</span>
<label>${category}</label>
</span>`;
}
summaryHtml += "</div>";
const summaryElement = document.getElementById("result-summary");
summaryElement.classList.add("done");
summaryElement.innerHTML = `<div class="score">${uiFriendlyScore(totalScore)}</div>
<label>Score</label>`;
summaryElement.innerHTML = summaryHtml;
summaryElement.onclick = displayCategoryScores;
if (showScoreDetails)
displayCategoryScores();
statusElement.innerHTML = "";
} else if (!JetStreamParams.dumpJSONResults) {
console.log("\n");
for (let [category, scores] of categoryScores)
console.log(`${category}: ${uiFriendlyScore(geomeanScore(scores))}`);
console.log("\nTotal Score: ", uiFriendlyScore(totalScore), "\n");
console.log("Total:");
for (let [category, scores] of categoryScores) {
console.log(
shellFriendlyLabel(`${category}-Score`),
shellFriendlyScore(geomeanScore(scores)));
}
for (let [category, times] of categoryTimes) {
console.log(
shellFriendlyLabel(`${category}-Time`),
shellFriendlyDuration(geomeanScore(times)));
}
console.log("");
console.log(shellFriendlyLabel("Total-Score"), shellFriendlyScore(totalScore));
console.log(shellFriendlyLabel("Total-Time"), shellFriendlyDuration(totalTime));
console.log("");
}
this.reportScoreToRunBenchmarkRunner();
@ -284,33 +319,38 @@ class Driver {
prepareToRun() {
this.benchmarks.sort((a, b) => a.plan.name.toLowerCase() < b.plan.name.toLowerCase() ? 1 : -1);
let text = "";
for (const benchmark of this.benchmarks) {
const description = Object.keys(benchmark.subScores());
description.push("Score");
const scoreIds = benchmark.scoreIdentifiers();
const overallScoreId = scoreIds.pop();
if (isInBrowser) {
text +=
`<div class="benchmark" id="benchmark-${benchmark.name}">
<h3 class="benchmark-name">${benchmark.name} <a class="info" href="in-depth.html#${benchmark.name}">i</a></h3>
<h4 class="score" id="${overallScoreId}">&nbsp;</h4>
<h4 class="plot" id="plot-${benchmark.name}">&nbsp;</h4>
<p>`;
for (let i = 0; i < scoreIds.length; i++) {
const scoreId = scoreIds[i];
const label = description[i];
text += `<span class="result"><span id="${scoreId}">&nbsp;</span><label>${label}</label></span>`
}
text += `</p></div>`;
}
}
if (!isInBrowser)
return;
let text = "";
for (const benchmark of this.benchmarks) {
const scoreDescription = Object.keys(benchmark.allScores());
const timeDescription = Object.keys(benchmark.allTimes());
const scoreIds = benchmark.allScoreIdentifiers();
const overallScoreId = scoreIds.pop();
const timeIds = benchmark.allTimeIdentifiers();
text +=
`<div class="benchmark" id="benchmark-${benchmark.name}">
<h3 class="benchmark-name">${benchmark.name} <a class="info" href="in-depth.html#${benchmark.name}">i</a></h3>
<h4 class="score" id="${overallScoreId}">&nbsp;</h4>
<h4 class="plot" id="plot-${benchmark.name}">&nbsp;</h4>
<p>`;
for (let i = 0; i < scoreIds.length; i++) {
const scoreId = scoreIds[i];
const label = scoreDescription[i];
text += `<span class="result score"><span id="${scoreId}">&nbsp;</span><label>${label}</label></span>`
}
text += "<br/>";
for (let i = 0; i < timeIds.length; i++) {
const timeId = timeIds[i];
const label = timeDescription[i];
text += `<span class="result detail"><span id="${timeId}">&nbsp;</span><label>${label}</label></span>`
}
text += `</p></div>`;
}
const timestamp = performance.now();
document.getElementById('jetstreams').style.backgroundImage = `url('jetstreams.svg?${timestamp}')`;
const resultsTable = document.getElementById("results");
@ -329,12 +369,13 @@ class Driver {
if (!isInBrowser)
return;
for (const id of benchmark.scoreIdentifiers()) {
for (const id of benchmark.allScoreIdentifiers())
document.getElementById(id).innerHTML = "error";
const benchmarkResultsUI = document.getElementById(`benchmark-${benchmark.name}`);
benchmarkResultsUI.classList.remove("benchmark-running");
benchmarkResultsUI.classList.add("benchmark-error");
}
for (const id of benchmark.allTimeIdentifiers())
document.getElementById(id).innerHTML = "error";
const benchmarkResultsUI = document.getElementById(`benchmark-${benchmark.name}`);
benchmarkResultsUI.classList.remove("benchmark-running");
benchmarkResultsUI.classList.add("benchmark-error");
}
pushError(name, error) {
@ -731,16 +772,36 @@ class Benchmark {
return geomeanScore(subScores);
}
get totalTime() {
const subTimes = Object.values(this.subTimes());
return sum(subTimes);
}
get wallTime() {
return this.endTime - this.startTime;
}
subScores() {
throw new Error("Subclasses need to implement this");
}
subTimes() {
throw new Error("Subclasses need to implement this");
}
allScores() {
const allScores = this.subScores();
allScores["Score"] = this.score;
return allScores;
}
allTimes() {
const allTimes = this.subTimes();
allTimes["Wall"] = this.wallTime;
allTimes["Total"] = this.totalTime;
return allTimes;
}
get prerunCode() { return null; }
get preIterationCode() {
@ -1001,7 +1062,7 @@ class Benchmark {
this.preloads = Object.entries(this.plan.preload ?? {});
}
scoreIdentifiers() {
allScoreIdentifiers() {
const ids = Object.keys(this.allScores()).map(name => this.scoreIdentifier(name));
return ids;
}
@ -1010,6 +1071,15 @@ class Benchmark {
return `results-cell-${this.name}-${scoreName}`;
}
allTimeIdentifiers() {
const ids = Object.keys(this.allTimes()).map(name => this.timeIdentifier(name));
return ids;
}
timeIdentifier(scoreName) {
return `results-cell-${this.name}-${scoreName}-time`;
}
updateUIBeforeRun() {
if (!JetStreamParams.dumpJSONResults)
console.log(`Running ${this.name}:`);
@ -1022,30 +1092,56 @@ class Benchmark {
resultsBenchmarkUI.classList.add("benchmark-running");
resultsBenchmarkUI.scrollIntoView({ block: "nearest" });
for (const id of this.scoreIdentifiers())
for (const id of this.allScoreIdentifiers())
document.getElementById(id).innerHTML = "...";
for (const id of this.allTimeIdentifiers())
document.getElementById(id).innerHTML = "...";
}
updateUIAfterRun() {
const scoreEntries = Object.entries(this.allScores());
if (isInBrowser)
this.updateUIAfterRunInBrowser(scoreEntries);
this.updateUIAfterRunInBrowser();
if (JetStreamParams.dumpJSONResults)
return;
this.updateConsoleAfterRun(scoreEntries);
this.updateConsoleAfterRun();
}
updateUIAfterRunInBrowser(scoreEntries) {
updateUIAfterRunInBrowser() {
const benchmarkResultsUI = document.getElementById(`benchmark-${this.name}`);
benchmarkResultsUI.classList.remove("benchmark-running");
benchmarkResultsUI.classList.add("benchmark-done");
for (const [name, value] of scoreEntries)
for (const [name, value] of Object.entries(this.allScores()))
document.getElementById(this.scoreIdentifier(name)).innerHTML = uiFriendlyScore(value);
for (const [name, value] of Object.entries(this.allTimes()))
document.getElementById(this.timeIdentifier(name)).innerHTML = uiFriendlyDuration(value);
this.renderScatterPlot();
}
updateConsoleAfterRun() {
for (let [name, value] of Object.entries(this.allScores())) {
if (!name.endsWith("Score"))
name = `${name}-Score`;
this.logMetric(name, shellFriendlyScore(value));
}
for (let [name, value] of Object.entries(this.allTimes())) {
this.logMetric(`${name}-Time`, shellFriendlyDuration(value));
}
if (JetStreamParams.RAMification) {
this.logMetric("Current Footprint", uiFriendlyNumber(this.currentFootprint));
this.logMetric("Peak Footprint", uiFriendlyNumber(this.peakFootprint));
}
console.log("");
}
logMetric(name, value) {
console.log(
shellFriendlyLabel(`${this.name} ${name}`),
value);
}
renderScatterPlot() {
const plotContainer = document.getElementById(`plot-${this.name}`);
if (!plotContainer || !this.results || this.results.length === 0)
@ -1073,17 +1169,6 @@ class Benchmark {
}
plotContainer.innerHTML = `<svg width="${width}px" height="${height}px">${circlesSVG}</svg>`;
}
updateConsoleAfterRun(scoreEntries) {
for (let [name, value] of scoreEntries) {
console.log(` ${name}:`, uiFriendlyScore(value));
}
if (JetStreamParams.RAMification) {
console.log(" Current Footprint:", uiFriendlyNumber(this.currentFootprint));
console.log(" Peak Footprint:", uiFriendlyNumber(this.peakFootprint));
}
console.log(" Wall-Time:", uiFriendlyDuration(this.endTime - this.startTime));
}
};
class GroupedBenchmark extends Benchmark {
@ -1167,6 +1252,22 @@ class GroupedBenchmark extends Benchmark {
results[subScore] = geomeanScore(results[subScore]);
return results;
}
subTimes() {
const results = {};
for (const benchmark of this.benchmarks) {
let times = benchmark.subTimes();
for (let subTime in times) {
results[subTime] ??= [];
results[subTime].push(times[subTime]);
}
}
for (let subTimes in results)
results[subTimes] = sum(results[subTimes]);
return results;
}
};
class DefaultBenchmark extends Benchmark {
@ -1215,6 +1316,17 @@ class DefaultBenchmark extends Benchmark {
scores["Average"] = this.averageScore;
return scores;
}
subTimes() {
const times = {
"First": this.firstIterationTime,
};
if (this.worstCaseCount)
times["Worst"] = this.worstTime;
if (this.iterations > 1)
times["Average"] = this.averageTime;
return times;
}
}
class AsyncBenchmark extends DefaultBenchmark {
@ -1379,6 +1491,13 @@ class WSLBenchmark extends Benchmark {
}`;
}
subTimes() {
return {
"Stdlib": this.stdlibTime,
"MainRun": this.mainRunTime,
};
}
subScores() {
return {
"Stdlib": this.stdlibScore,
@ -1516,6 +1635,13 @@ class WasmLegacyBenchmark extends Benchmark {
"Runtime": this.runScore,
};
}
subTimes() {
return {
"Startup": this.startupTime,
"Runtime": this.runTime,
};
}
};
function dotnetPreloads(type)