diff --git a/.github/workflows/wasm-site.yml b/.github/workflows/wasm-site.yml
index d93a9b5..947af09 100644
--- a/.github/workflows/wasm-site.yml
+++ b/.github/workflows/wasm-site.yml
@@ -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
diff --git a/README.md b/README.md
index 05b1ae0..96adb53 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/Sources/PureYAMLStreamingWasmSmoke/main.swift b/Sources/PureYAMLStreamingWasmSmoke/main.swift
index c176e28..8502d98 100644
--- a/Sources/PureYAMLStreamingWasmSmoke/main.swift
+++ b/Sources/PureYAMLStreamingWasmSmoke/main.swift
@@ -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,
diff --git a/WasmSite/index.html b/WasmSite/index.html
index fb395c1..c764d83 100644
--- a/WasmSite/index.html
+++ b/WasmSite/index.html
@@ -3,26 +3,56 @@
- PureYAMLStreaming WASM Smoke
+ PureYAMLStreaming WASM Benchmark
PureYAMLStreaming
- WASM Streaming Smoke Test
+ Live WASM YAML Benchmark
- 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.
- Run WASM Test
+ Public YAML example
+
+ URL
+
+ Run Benchmark
Idle
+
+ Timing
+
+
+
Bytes
+ -
+
+
+
Fetch
+ -
+
+
+
Swift parse
+ -
+
+
+
Throughput
+ -
+
+
+
Documents
+ -
+
+
+
+
Output
- Click "Run WASM Test".
+ Pick a YAML file and click "Run Benchmark".
diff --git a/WasmSite/main.js b/WasmSite/main.js
index 6f549d7..fa4a7d0 100644
--- a/WasmSite/main.js
+++ b/WasmSite/main.js
@@ -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`;
+}
diff --git a/WasmSite/style.css b/WasmSite/style.css
index dc2028d..6ff88e2 100644
--- a/WasmSite/style.css
+++ b/WasmSite/style.css
@@ -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;