feat: benchmark public YAML in browser WASM

Add a live public YAML chooser and JavaScript timing for fetch, Swift WASM parse, throughput, and document counts.
This commit is contained in:
Mihaela Mihaljevic 2026-06-06 22:41:40 +02:00
parent 36065fe572
commit bc888d950f
6 changed files with 255 additions and 29 deletions

View File

@ -76,6 +76,7 @@ jobs:
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
await page.goto('http://127.0.0.1:8766/?v=' + Date.now());
await page.selectOption('#sampleSelect', { index: 4 });
await page.click('#runButton');
await page.waitForFunction(
() => document.querySelector('#status')?.textContent === 'Passed',
@ -83,9 +84,13 @@ jobs:
{ timeout: 60000 }
);
const output = await page.locator('#output').textContent();
const throughput = await page.locator('#throughputMetric').textContent();
if (!output.includes('"ok" : true') && !output.includes('"ok":true')) {
throw new Error(output);
}
if (!throughput.includes('/s')) {
throw new Error(`Expected throughput metric, got: ${throughput}`);
}
await browser.close();
})();
NODE

View File

@ -49,7 +49,7 @@ try parser.parseDocuments(from: url) { document in
}
```
## WASM Browser Smoke Site
## WASM Browser Benchmark Site
Live site:
@ -68,8 +68,10 @@ bash scripts/serve-wasm-site.sh
```
Then open `http://localhost:8080` and click **Run WASM Test**. The page loads
`pureyaml-streaming-wasm-smoke.wasm`, runs a chunked document-stream parse in
the browser through a WASI shim, captures stdout, and renders pass/fail.
`pureyaml-streaming-wasm-smoke.wasm`, lets you choose from 10+ public real-world
YAML files, fetches the selected file in JavaScript, feeds the bytes to Swift
through WASI stdin, and reports fetch time, parse time, throughput, document
count, and stdout JSON.
## Relationship To PureYAML

View File

@ -4,13 +4,14 @@ import PureYAMLStreaming
struct SmokeResult: Encodable {
var ok: Bool
var inputByteCount: Int
var documentCount: Int
var firstName: String?
var lastName: String?
var message: String
}
let yaml = Data(
let fallbackYAML = Data(
"""
---
name: browser-0
@ -31,25 +32,29 @@ let yaml = Data(
)
do {
let parser = PureYAMLStreaming.Parser(chunkSize: 7)
let input = CommandLine.arguments.count > 1
? FileHandle.standardInput.readDataToEndOfFile()
: Data()
let yaml = input.isEmpty ? fallbackYAML : input
let parser = PureYAMLStreaming.Parser(chunkSize: 31)
let documents = try parser.collectDocuments(data: yaml)
let names = documents.compactMap { document in
stringValue("name", in: document.value)
}
let ok = documents.count == 3
&& names.first == "browser-0"
&& names.last == "browser-2"
let ok = !documents.isEmpty
let result = SmokeResult(
ok: ok,
inputByteCount: yaml.count,
documentCount: documents.count,
firstName: names.first,
lastName: names.last,
message: ok ? "PureYAMLStreaming WASM smoke passed" : "PureYAMLStreaming WASM smoke failed",
message: ok ? "PureYAMLStreaming WASM parse passed" : "PureYAMLStreaming WASM parse found no documents",
)
print(try renderJSON(result))
} catch {
let result = SmokeResult(
ok: false,
inputByteCount: 0,
documentCount: 0,
firstName: nil,
lastName: nil,

View File

@ -3,26 +3,56 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>PureYAMLStreaming WASM Smoke</title>
<title>PureYAMLStreaming WASM Benchmark</title>
<link rel="stylesheet" href="./style.css" />
</head>
<body>
<main>
<section class="card">
<p class="eyebrow">PureYAMLStreaming</p>
<h1>WASM Streaming Smoke Test</h1>
<h1>Live WASM YAML Benchmark</h1>
<p>
This page loads the Swift WASI build in the browser, runs a
document-streaming parse, captures stdout, and reports whether the
smoke test passed.
Pick a public real-world YAML file. The browser fetches it, feeds the
bytes to the Swift WASI executable through stdin, and times the
document-streaming parse live.
</p>
<button id="runButton" type="button">Run WASM Test</button>
<label for="sampleSelect">Public YAML example</label>
<select id="sampleSelect"></select>
<label for="urlInput">URL</label>
<input id="urlInput" type="url" autocomplete="off" />
<button id="runButton" type="button">Run Benchmark</button>
<div id="status" class="status idle">Idle</div>
</section>
<section class="card">
<h2>Timing</h2>
<dl class="metrics">
<div>
<dt>Bytes</dt>
<dd id="bytesMetric">-</dd>
</div>
<div>
<dt>Fetch</dt>
<dd id="fetchMetric">-</dd>
</div>
<div>
<dt>Swift parse</dt>
<dd id="parseMetric">-</dd>
</div>
<div>
<dt>Throughput</dt>
<dd id="throughputMetric">-</dd>
</div>
<div>
<dt>Documents</dt>
<dd id="documentsMetric">-</dd>
</div>
</dl>
</section>
<section class="card">
<h2>Output</h2>
<pre id="output">Click "Run WASM Test".</pre>
<pre id="output">Pick a YAML file and click "Run Benchmark".</pre>
</section>
</main>

View File

@ -6,29 +6,116 @@ import {
} from "https://esm.sh/@bjorn3/browser_wasi_shim@0.3.0";
const runButton = document.querySelector("#runButton");
const sampleSelect = document.querySelector("#sampleSelect");
const urlInput = document.querySelector("#urlInput");
const statusElement = document.querySelector("#status");
const outputElement = document.querySelector("#output");
const bytesMetric = document.querySelector("#bytesMetric");
const fetchMetric = document.querySelector("#fetchMetric");
const parseMetric = document.querySelector("#parseMetric");
const throughputMetric = document.querySelector("#throughputMetric");
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;
const samples = [
{
name: "GitHub REST API OpenAPI (9.5 MB)",
url: "https://raw.githubusercontent.com/mihaelamj/PureYAMLGeekbench/main/Fixtures/real-yaml/github-rest-api.yaml",
},
{
name: "Zoom OpenAPI (5.0 MB)",
url: "https://raw.githubusercontent.com/mihaelamj/PureYAMLGeekbench/main/Fixtures/real-yaml/api-guru-zoom-openapi.yaml",
},
{
name: "Stripe OpenAPI (3.7 MB)",
url: "https://raw.githubusercontent.com/mihaelamj/PureYAMLGeekbench/main/Fixtures/real-yaml/api-guru-stripe-openapi.yaml",
},
{
name: "cert-manager Helm values (65 KB)",
url: "https://raw.githubusercontent.com/mihaelamj/PureYAMLGeekbench/main/Fixtures/real-yaml/cert-manager-values.yaml",
},
{
name: "cert-manager CRD (44 KB)",
url: "https://raw.githubusercontent.com/mihaelamj/PureYAMLGeekbench/main/Fixtures/real-yaml/cert-manager-certificate-crd.yaml",
},
{
name: "Kubernetes Prow presubmits (14 KB)",
url: "https://raw.githubusercontent.com/mihaelamj/PureYAMLGeekbench/main/Fixtures/real-yaml/prow-cluster-api-presubmits.yaml",
},
{
name: "Prometheus config (12 KB)",
url: "https://raw.githubusercontent.com/mihaelamj/PureYAMLGeekbench/main/Fixtures/real-yaml/prometheus-conf-good.yml",
},
{
name: "USPTO OpenAPI",
url: "https://raw.githubusercontent.com/mihaelamj/PureYAMLGeekbench/main/Fixtures/real-yaml/openapi-uspto.yaml",
},
{
name: "OpenAPI petstore expanded",
url: "https://raw.githubusercontent.com/mihaelamj/PureYAMLGeekbench/main/Fixtures/real-yaml/openapi-petstore-expanded.yaml",
},
{
name: "Bitnami Apache values",
url: "https://raw.githubusercontent.com/mihaelamj/PureYAMLGeekbench/main/Fixtures/real-yaml/bitnami-apache-values.yaml",
},
{
name: "GitHub Actions Swift format",
url: "https://raw.githubusercontent.com/mihaelamj/PureYAMLGeekbench/main/Fixtures/real-yaml/github-actions-swift-format.yml",
},
{
name: "Docker Compose Prometheus/Grafana",
url: "https://raw.githubusercontent.com/mihaelamj/PureYAMLGeekbench/main/Fixtures/real-yaml/awesome-compose-prometheus-grafana-compose-yaml.yaml",
},
];
for (const sample of samples) {
const option = document.createElement("option");
option.value = sample.url;
option.textContent = sample.name;
sampleSelect.append(option);
}
urlInput.value = samples[0].url;
runButton.addEventListener("click", () => {
runSmokeTest().catch((error) => {
runBenchmark().catch((error) => {
renderFailure(error);
});
});
async function runSmokeTest() {
sampleSelect.addEventListener("change", () => {
urlInput.value = sampleSelect.value;
});
async function runBenchmark() {
const sourceURL = urlInput.value.trim();
if (!sourceURL) {
throw new Error("Choose or enter a YAML URL first.");
}
runButton.disabled = true;
statusElement.className = "status idle";
statusElement.textContent = "Running...";
outputElement.textContent = "Loading WASM...";
statusElement.textContent = "Fetching YAML...";
outputElement.textContent = "";
resetMetrics();
const wasmBytes = await loadWasmBytes(wasmURL);
const moduleLoadStart = performance.now();
const module = await loadWasmModule(wasmURL);
const moduleLoadMS = performance.now() - moduleLoadStart;
const fetchStart = performance.now();
const response = await fetch(cacheBustedURL(sourceURL), { cache: "no-store" });
if (!response.ok) {
throw new Error(`Failed to fetch YAML: HTTP ${response.status}`);
}
const yamlBytes = new Uint8Array(await response.arrayBuffer());
const fetchMS = performance.now() - fetchStart;
let stdoutText = "";
let stderrText = "";
const fds = [
new OpenFile(new File([])),
new OpenFile(new File(yamlBytes)),
ConsoleStdout.lineBuffered((line) => {
stdoutText += `${line}\n`;
}),
@ -36,20 +123,33 @@ async function runSmokeTest() {
stderrText += `${line}\n`;
}),
];
const wasi = new WASI([], [], fds);
const wasm = await WebAssembly.compile(wasmBytes);
const instance = await WebAssembly.instantiate(wasm, {
const wasi = new WASI(["pureyaml-streaming-wasm-smoke", sourceURL], [], fds);
statusElement.textContent = "Running Swift parser...";
const parseStart = performance.now();
const instance = await WebAssembly.instantiate(module, {
wasi_snapshot_preview1: wasi.wasiImport,
});
wasi.start(instance);
const parseMS = performance.now() - parseStart;
const parsed = parseSmokeOutput(stdoutText);
const ok = parsed?.ok === true;
const throughput = yamlBytes.byteLength / (parseMS / 1000);
statusElement.className = ok ? "status pass" : "status fail";
statusElement.textContent = ok ? "Passed" : "Failed";
bytesMetric.textContent = formatBytes(yamlBytes.byteLength);
fetchMetric.textContent = formatDuration(fetchMS);
parseMetric.textContent = formatDuration(parseMS);
throughputMetric.textContent = `${formatBytes(throughput)}/s`;
documentsMetric.textContent = parsed?.documentCount ?? "-";
outputElement.textContent = [
`url: ${sourceURL}`,
`moduleLoad: ${formatDuration(moduleLoadMS)}`,
`fetch: ${formatDuration(fetchMS)}`,
`parse: ${formatDuration(parseMS)}`,
`throughput: ${formatBytes(throughput)}/s`,
"",
"stdout:",
stdoutText || "(empty)",
"",
@ -59,6 +159,13 @@ async function runSmokeTest() {
runButton.disabled = false;
}
async function loadWasmModule(url) {
if (!modulePromise) {
modulePromise = loadWasmBytes(url).then((bytes) => WebAssembly.compile(bytes));
}
return modulePromise;
}
async function loadWasmBytes(url) {
const response = await fetch(url);
if (!response.ok) {
@ -75,6 +182,12 @@ async function loadWasmBytes(url) {
return new Response(stream).arrayBuffer();
}
function cacheBustedURL(url) {
const parsedURL = new URL(url, location.href);
parsedURL.searchParams.set("_pureyaml_bench", `${Date.now()}-${Math.random().toString(36).slice(2)}`);
return parsedURL.href;
}
function parseSmokeOutput(text) {
try {
return JSON.parse(text);
@ -90,3 +203,27 @@ function renderFailure(error) {
runButton.disabled = false;
}
function resetMetrics() {
bytesMetric.textContent = "-";
fetchMetric.textContent = "-";
parseMetric.textContent = "-";
throughputMetric.textContent = "-";
documentsMetric.textContent = "-";
}
function formatDuration(milliseconds) {
if (milliseconds < 1000) {
return `${milliseconds.toFixed(1)} ms`;
}
return `${(milliseconds / 1000).toFixed(3)} s`;
}
function formatBytes(byteCount) {
if (byteCount < 1024) {
return `${byteCount.toFixed(0)} B`;
}
if (byteCount < 1024 * 1024) {
return `${(byteCount / 1024).toFixed(1)} KiB`;
}
return `${(byteCount / 1024 / 1024).toFixed(2)} MiB`;
}

View File

@ -10,12 +10,12 @@
body {
margin: 0;
min-height: 100vh;
display: grid;
place-items: center;
padding: 28px 0;
}
main {
width: min(920px, calc(100vw - 32px));
width: min(1040px, calc(100vw - 32px));
margin: 0 auto;
display: grid;
gap: 20px;
}
@ -52,10 +52,29 @@ p {
opacity: 0.82;
}
label {
display: block;
margin: 18px 0 8px;
font-weight: 800;
}
select,
input {
box-sizing: border-box;
width: 100%;
border: 1px solid color-mix(in srgb, CanvasText 18%, transparent);
border-radius: 14px;
padding: 12px 14px;
background: Canvas;
color: CanvasText;
font: inherit;
}
button {
appearance: none;
border: 0;
border-radius: 999px;
margin-top: 18px;
padding: 12px 18px;
font: inherit;
font-weight: 700;
@ -86,6 +105,34 @@ button:disabled {
background: color-mix(in srgb, #c21f39 30%, transparent);
}
.metrics {
margin: 0;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 14px;
}
.metrics div {
border: 1px solid color-mix(in srgb, CanvasText 14%, transparent);
border-radius: 16px;
padding: 16px;
background: color-mix(in srgb, Canvas 96%, CanvasText 4%);
}
dt {
margin: 0 0 8px;
font-size: 12px;
font-weight: 800;
letter-spacing: 0.08em;
text-transform: uppercase;
opacity: 0.65;
}
dd {
margin: 0;
font: 700 24px/1.1 ui-monospace, SFMono-Regular, Menlo, monospace;
}
pre {
margin: 0;
white-space: pre-wrap;