feat: show live benchmark run log

Add a scrollable browser log so long YAML benchmark runs expose each fetch, WASI, parse, and render step while they execute.
This commit is contained in:
Mihaela Mihaljevic 2026-06-06 23:32:21 +02:00
parent 9fd46932d1
commit 23d4c77cf4
4 changed files with 90 additions and 1 deletions

View File

@ -113,6 +113,7 @@ jobs:
const parse = await page.locator('#parseMetric').textContent();
const throughput = await page.locator('#throughputMetric').textContent();
const documents = await page.locator('#documentsMetric').textContent();
const runLog = await page.locator('#runLog').textContent();
const stdoutMatch = output.match(/stdout:\n([\s\S]*?)\n\nstderr:/);
const parsed = JSON.parse(stdoutMatch?.[1] || '{}');
if (parsed.ok !== true) throw new Error(output);
@ -131,6 +132,9 @@ jobs:
if (!throughput.includes('/s')) {
throw new Error(`Expected throughput metric, got: ${throughput}`);
}
if (!runLog.includes('Fetching YAML bytes.') || !runLog.includes('Starting Swift parser.') || !runLog.includes('Benchmark completed successfully.')) {
throw new Error(`Expected detailed run log, got: ${runLog}`);
}
await browser.close();
})();
NODE
@ -189,6 +193,7 @@ jobs:
);
const output = await page.locator('#output').textContent();
const throughput = await page.locator('#throughputMetric').textContent();
const runLog = await page.locator('#runLog').textContent();
const stdoutMatch = output.match(/stdout:\n([\s\S]*?)\n\nstderr:/);
const parsed = JSON.parse(stdoutMatch?.[1] || '{}');
if (parsed.documentCount !== 3000) throw new Error(output);
@ -200,6 +205,9 @@ jobs:
if (!throughput.includes('/s')) {
throw new Error(`Expected live throughput metric, got: ${throughput}`);
}
if (!runLog.includes('Fetching YAML bytes.') || !runLog.includes('Starting Swift parser.') || !runLog.includes('Benchmark completed successfully.')) {
throw new Error(`Expected live detailed run log, got: ${runLog}`);
}
await browser.close();
})();
NODE

View File

@ -50,6 +50,13 @@
</dl>
</section>
<section class="card">
<h2>Run Log</h2>
<div id="runLog" class="run-log" role="log" aria-live="polite">
<div class="log-entry muted">Waiting for benchmark run.</div>
</div>
</section>
<section class="card">
<h2>Output</h2>
<pre id="output">Pick a YAML file and click "Run Benchmark".</pre>

View File

@ -10,6 +10,7 @@ const sampleSelect = document.querySelector("#sampleSelect");
const urlInput = document.querySelector("#urlInput");
const statusElement = document.querySelector("#status");
const outputElement = document.querySelector("#output");
const runLogElement = document.querySelector("#runLog");
const bytesMetric = document.querySelector("#bytesMetric");
const fetchMetric = document.querySelector("#fetchMetric");
const parseMetric = document.querySelector("#parseMetric");
@ -18,6 +19,7 @@ const documentsMetric = document.querySelector("#documentsMetric");
const params = new URLSearchParams(location.search);
const wasmURL = params.get("wasm") || "./pureyaml-streaming-wasm-smoke.wasm.gz";
let modulePromise = null;
let runStartedAt = performance.now();
const samples = [
{
@ -96,14 +98,21 @@ async function runBenchmark() {
runButton.disabled = true;
statusElement.className = "status idle";
statusElement.textContent = "Fetching YAML...";
statusElement.textContent = "Preparing...";
outputElement.textContent = "";
resetMetrics();
resetLog();
logStep("Queued benchmark run.");
logStep(`Selected source: ${sourceURL}`);
logStep(`Loading WASM module: ${wasmURL}`);
const moduleLoadStart = performance.now();
const module = await loadWasmModule(wasmURL);
const moduleLoadMS = performance.now() - moduleLoadStart;
logStep(`WASM module ready in ${formatDuration(moduleLoadMS)}.`);
statusElement.textContent = "Fetching YAML...";
logStep("Fetching YAML bytes.");
const fetchStart = performance.now();
const response = await fetch(sourceURL, { cache: "no-store" });
if (!response.ok) {
@ -111,7 +120,9 @@ async function runBenchmark() {
}
const yamlBytes = new Uint8Array(await response.arrayBuffer());
const fetchMS = performance.now() - fetchStart;
logStep(`Fetched ${formatBytes(yamlBytes.byteLength)} in ${formatDuration(fetchMS)}.`);
logStep("Preparing WASI file descriptors.");
let stdoutText = "";
let stderrText = "";
const fds = [
@ -123,15 +134,22 @@ async function runBenchmark() {
stderrText += `${line}\n`;
}),
];
logStep("Creating WASI instance.");
const wasi = new WASI(["pureyaml-streaming-wasm-smoke", sourceURL], [], fds);
statusElement.textContent = "Running Swift parser...";
logStep("Starting Swift parser. The browser tab can pause while WebAssembly is executing.");
await nextPaint();
const parseStart = performance.now();
const instance = await WebAssembly.instantiate(module, {
wasi_snapshot_preview1: wasi.wasiImport,
});
logStep("WebAssembly instance created; entering WASI _start.");
await nextPaint();
wasi.start(instance);
const wallClockParseMS = performance.now() - parseStart;
logStep(`WASI process returned in ${formatDuration(wallClockParseMS)}.`);
logStep("Parsing Swift JSON result from stdout.");
const parsed = parseSmokeOutput(stdoutText);
const ok = parsed?.ok === true;
if (!Number.isFinite(parsed?.parseMilliseconds)) {
@ -139,6 +157,9 @@ async function runBenchmark() {
}
const parseMS = parsed.parseMilliseconds;
const throughput = parseMS > 0 ? yamlBytes.byteLength / (parseMS / 1000) : 0;
logStep(`Swift parse reported ${formatDuration(parseMS)}.`);
logStep(`Parsed ${parsed.documentCount ?? 0} document(s).`);
logStep(`Throughput: ${formatBytes(throughput)}/s.`);
statusElement.className = ok ? "status pass" : "status fail";
statusElement.textContent = ok ? "Passed" : "Failed";
@ -161,6 +182,7 @@ async function runBenchmark() {
"stderr:",
stderrText || "(empty)",
].join("\n");
logStep(ok ? "Benchmark completed successfully." : "Benchmark finished with parser failure.");
runButton.disabled = false;
}
@ -199,6 +221,7 @@ function renderFailure(error) {
statusElement.className = "status fail";
statusElement.textContent = "Failed";
outputElement.textContent = String(error?.stack || error);
logStep(`Failed: ${String(error?.message || error)}`, "error");
runButton.disabled = false;
}
@ -226,3 +249,25 @@ function formatBytes(byteCount) {
}
return `${(byteCount / 1024 / 1024).toFixed(2)} MiB`;
}
function resetLog() {
runStartedAt = performance.now();
runLogElement.replaceChildren();
}
function logStep(message, level = "info") {
const entry = document.createElement("div");
entry.className = `log-entry ${level}`;
const elapsed = (performance.now() - runStartedAt) / 1000;
entry.textContent = `[+${elapsed.toFixed(3)}s] ${message}`;
runLogElement.append(entry);
runLogElement.scrollTop = runLogElement.scrollHeight;
}
function nextPaint() {
return new Promise((resolve) => {
requestAnimationFrame(() => {
requestAnimationFrame(resolve);
});
});
}

View File

@ -133,6 +133,35 @@ dd {
font: 700 24px/1.1 ui-monospace, SFMono-Regular, Menlo, monospace;
}
.run-log {
height: min(320px, 45vh);
overflow-y: auto;
border: 1px solid color-mix(in srgb, CanvasText 14%, transparent);
border-radius: 16px;
padding: 14px;
background: color-mix(in srgb, Canvas 96%, CanvasText 4%);
font: 13px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace;
}
.log-entry {
padding: 6px 0;
border-bottom: 1px solid color-mix(in srgb, CanvasText 8%, transparent);
overflow-wrap: anywhere;
}
.log-entry:last-child {
border-bottom: 0;
}
.log-entry.muted {
opacity: 0.65;
}
.log-entry.error {
color: #c21f39;
font-weight: 700;
}
pre {
margin: 0;
white-space: pre-wrap;