feat: show streaming document preview

This commit is contained in:
Mihaela Mihaljevic 2026-06-07 01:10:38 +02:00
parent 4a8978a1ef
commit 442e84d94a
4 changed files with 246 additions and 2 deletions

View File

@ -136,6 +136,7 @@ jobs:
const throughput = await page.locator('#throughputMetric').textContent();
const documents = await page.locator('#documentsMetric').textContent();
const runLog = await page.locator('#runLog').textContent();
const streamDocument = await page.locator('#streamDocument').textContent();
const parsed = parsedStdout(output);
if (parsed.ok !== true) throw new Error(output);
if (parsed.transport !== 'SwiftWASIHTTPClient.HostHTTPClient') throw new Error(output);
@ -170,6 +171,18 @@ jobs:
if (!runLog.includes('Fetching YAML bytes') || !runLog.includes('SwiftWASIHTTPClient.HostHTTPClient') || !runLog.includes('Starting Swift parser.') || !runLog.includes('ChunkedUTF8Reader consumed') || !runLog.includes('Benchmark completed successfully.')) {
throw new Error(`Expected detailed run log, got: ${runLog}`);
}
if (!streamDocument.includes('name: ci-doc-0') || !streamDocument.includes('name: ci-doc-2999')) {
throw new Error('Expected buffered document panel to show generated YAML');
}
if (await page.locator('#streamDocument .yaml-key').count() < 3) {
throw new Error('Expected buffered document panel syntax highlighting');
}
await page.click('#copyRunLogButton');
await page.waitForFunction(
() => document.querySelector('#copyRunLogButton')?.textContent === 'Copied',
null,
{ timeout: 5000 }
);
await page.click('#streamButton');
await page.waitForFunction(
@ -179,6 +192,7 @@ jobs:
);
const streamingOutput = await page.locator('#output').textContent();
const streamingRunLog = await page.locator('#runLog').textContent();
const streamingDocument = await page.locator('#streamDocument').textContent();
const streamingParsed = parsedStdout(streamingOutput);
if (streamingParsed.ok !== true) throw new Error(streamingOutput);
if (streamingParsed.transport !== 'SharedArrayBuffer fd_read stdin') throw new Error(streamingOutput);
@ -195,6 +209,12 @@ jobs:
if (!streamingRunLog.includes('Streaming HTTP 200 response body into SharedArrayBuffer') || !streamingRunLog.includes('Network chunk') || !streamingRunLog.includes('Worker fd_read calls') || !streamingRunLog.includes('SharedArrayBuffer fd_read stdin')) {
throw new Error(`Expected true streaming run log, got: ${streamingRunLog}`);
}
if (!streamingDocument.includes('name: ci-doc-0') || !streamingDocument.includes('name: ci-doc-2999')) {
throw new Error('Expected streaming document panel to append generated YAML');
}
if (await page.locator('#streamDocument .yaml-key').count() < 3) {
throw new Error('Expected streaming document panel syntax highlighting');
}
await browser.close();
})();
NODE
@ -271,6 +291,7 @@ jobs:
const output = await page.locator('#output').textContent();
const throughput = await page.locator('#throughputMetric').textContent();
const runLog = await page.locator('#runLog').textContent();
const streamDocument = await page.locator('#streamDocument').textContent();
const parsed = parsedStdout(output);
if (parsed.transport !== 'SwiftWASIHTTPClient.HostHTTPClient') throw new Error(output);
if (parsed.httpStatusCode !== 200) throw new Error(output);
@ -298,6 +319,12 @@ jobs:
if (!runLog.includes('Fetching YAML bytes') || !runLog.includes('SwiftWASIHTTPClient.HostHTTPClient') || !runLog.includes('Starting Swift parser.') || !runLog.includes('ChunkedUTF8Reader consumed') || !runLog.includes('Benchmark completed successfully.')) {
throw new Error(`Expected live detailed run log, got: ${runLog}`);
}
if (!streamDocument.includes('name: ci-doc-0') || !streamDocument.includes('name: ci-doc-2999')) {
throw new Error('Expected live buffered document panel to show generated YAML');
}
if (await page.locator('#streamDocument .yaml-key').count() < 3) {
throw new Error('Expected live buffered document panel syntax highlighting');
}
await page.click('#streamButton');
await page.waitForFunction(
() => document.querySelector('#status')?.textContent === 'Passed',
@ -306,6 +333,7 @@ jobs:
);
const streamingOutput = await page.locator('#output').textContent();
const streamingRunLog = await page.locator('#runLog').textContent();
const streamingDocument = await page.locator('#streamDocument').textContent();
const streamingParsed = parsedStdout(streamingOutput);
if (streamingParsed.transport !== 'SharedArrayBuffer fd_read stdin') throw new Error(streamingOutput);
if (streamingParsed.httpStatusCode !== 200) throw new Error(streamingOutput);
@ -318,6 +346,12 @@ jobs:
if (!streamingRunLog.includes('Streaming HTTP 200 response body into SharedArrayBuffer') || !streamingRunLog.includes('Network chunk') || !streamingRunLog.includes('Worker fd_read calls') || !streamingRunLog.includes('SharedArrayBuffer fd_read stdin')) {
throw new Error(`Expected live true streaming run log, got: ${streamingRunLog}`);
}
if (!streamingDocument.includes('name: ci-doc-0') || !streamingDocument.includes('name: ci-doc-2999')) {
throw new Error('Expected live streaming document panel to append generated YAML');
}
if (await page.locator('#streamDocument .yaml-key').count() < 3) {
throw new Error('Expected live streaming document panel syntax highlighting');
}
await browser.close();
})();
NODE

View File

@ -76,15 +76,28 @@
</section>
<section class="card">
<h2>Run Log</h2>
<div class="section-header">
<h2>Run Log</h2>
<button id="copyRunLogButton" class="secondary-button" type="button">Copy Run Log</button>
</div>
<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>Document Stream</h2>
<p>
In true streaming mode this panel appends decoded YAML chunks as the
browser receives them. In buffered mode it shows the fetched body after
the browser buffer is complete.
</p>
<pre id="streamDocument" class="stream-document">Waiting for YAML bytes.</pre>
</section>
<section class="card">
<h2>Output</h2>
<pre id="output">Pick a YAML file and click "Run Benchmark".</pre>
<pre id="output">Pick a YAML file and choose a benchmark mode.</pre>
</section>
</main>

View File

@ -12,6 +12,8 @@ const urlInput = document.querySelector("#urlInput");
const statusElement = document.querySelector("#status");
const outputElement = document.querySelector("#output");
const runLogElement = document.querySelector("#runLog");
const copyRunLogButton = document.querySelector("#copyRunLogButton");
const streamDocumentElement = document.querySelector("#streamDocument");
const isolationMetric = document.querySelector("#isolationMetric");
const sabMetric = document.querySelector("#sabMetric");
const workerMetric = document.querySelector("#workerMetric");
@ -110,6 +112,12 @@ streamButton.addEventListener("click", () => {
});
});
copyRunLogButton.addEventListener("click", () => {
copyRunLog().catch((error) => {
logStep(`Unable to copy run log: ${String(error?.message || error)}`, "error");
});
});
sampleSelect.addEventListener("change", () => {
urlInput.value = sampleSelect.value;
});
@ -129,6 +137,7 @@ async function runBenchmark() {
outputElement.textContent = "";
resetMetrics();
resetLog();
resetStreamDocument("Waiting for buffered YAML fetch to complete.");
logStep("Queued benchmark run.");
logStep(`Selected source: ${sourceURL}`);
@ -147,6 +156,7 @@ async function runBenchmark() {
}
const yamlBytes = new Uint8Array(await response.arrayBuffer());
const fetchMS = performance.now() - fetchStart;
replaceStreamDocument(decodeYAMLBytes(yamlBytes));
logStep(`Fetched HTTP ${response.status}, ${formatBytes(yamlBytes.byteLength)} in ${formatDuration(fetchMS)}.`);
logStep("Handing buffered response to SwiftWASIHTTPClient.HostHTTPClient through WASI stdin.");
@ -239,6 +249,7 @@ async function runStreamingBenchmark() {
outputElement.textContent = "";
resetMetrics();
resetLog();
resetStreamDocument("Waiting for streaming YAML chunks.");
logStep("Queued true browser-to-WASI streaming run.");
logStep(`Selected source: ${sourceURL}`);
@ -295,6 +306,7 @@ async function runStreamingBenchmark() {
logStep(`Streaming HTTP ${response.status} response body into SharedArrayBuffer.`);
const reader = response.body.getReader();
const documentDecoder = new TextDecoder();
let networkChunkCount = 0;
let fetchedByteCount = 0;
try {
@ -305,6 +317,7 @@ async function runStreamingBenchmark() {
}
networkChunkCount += 1;
fetchedByteCount += value.byteLength;
appendStreamDocumentChunk(documentDecoder.decode(value, { stream: true }));
await writeToRingBuffer(value, control, bytes);
logStep(`Network chunk ${networkChunkCount}: wrote ${formatBytes(value.byteLength)} to ring buffer.`);
}
@ -315,6 +328,7 @@ async function runStreamingBenchmark() {
worker?.terminate();
throw error;
}
appendStreamDocumentChunk(documentDecoder.decode());
Atomics.store(control, streamControlIndex.closed, 1);
wakeRingBuffer(control);
const fetchMS = performance.now() - fetchStart;
@ -504,6 +518,131 @@ function resetMetrics() {
documentsMetric.textContent = "-";
}
async function copyRunLog() {
const text = runLogElement.innerText.trim();
if (!text) {
return;
}
if ("clipboard" in navigator && window.isSecureContext) {
await navigator.clipboard.writeText(text);
} else {
const textarea = document.createElement("textarea");
textarea.value = text;
textarea.setAttribute("readonly", "");
textarea.style.position = "fixed";
textarea.style.opacity = "0";
document.body.append(textarea);
textarea.select();
document.execCommand("copy");
textarea.remove();
}
const previousLabel = copyRunLogButton.textContent;
copyRunLogButton.textContent = "Copied";
window.setTimeout(() => {
copyRunLogButton.textContent = previousLabel;
}, 1500);
}
function resetStreamDocument(message) {
streamDocumentElement.replaceChildren(document.createTextNode(message));
streamDocumentElement.scrollTop = 0;
}
function replaceStreamDocument(text) {
streamDocumentElement.replaceChildren(renderYAMLSyntax(text || "(empty document)"));
streamDocumentElement.scrollTop = 0;
}
function appendStreamDocumentChunk(text) {
if (!text) {
return;
}
if (streamDocumentElement.childNodes.length === 1 && streamDocumentElement.textContent.startsWith("Waiting for")) {
streamDocumentElement.replaceChildren();
}
streamDocumentElement.append(renderYAMLSyntax(text));
streamDocumentElement.scrollTop = streamDocumentElement.scrollHeight;
}
function decodeYAMLBytes(bytes) {
return new TextDecoder().decode(bytes);
}
function renderYAMLSyntax(text) {
const fragment = document.createDocumentFragment();
for (const segment of text.split(/(\r?\n)/)) {
if (segment === "\n" || segment === "\r\n") {
fragment.append(document.createTextNode(segment));
} else {
renderYAMLLine(segment, fragment);
}
}
return fragment;
}
function renderYAMLLine(line, fragment) {
const commentIndex = findYAMLCommentIndex(line);
const source = commentIndex >= 0 ? line.slice(0, commentIndex) : line;
const comment = commentIndex >= 0 ? line.slice(commentIndex) : "";
const keyMatch = source.match(/^(\s*(?:-\s*)?)([A-Za-z0-9_.-]+)(\s*:)(.*)$/);
if (source.trim() === "---" || source.trim() === "...") {
appendSyntaxSpan(fragment, source, "yaml-marker");
} else if (keyMatch) {
fragment.append(document.createTextNode(keyMatch[1]));
appendSyntaxSpan(fragment, keyMatch[2], "yaml-key");
appendSyntaxSpan(fragment, keyMatch[3], "yaml-punctuation");
appendYAMLValue(fragment, keyMatch[4]);
} else {
appendYAMLValue(fragment, source);
}
if (comment) {
appendSyntaxSpan(fragment, comment, "yaml-comment");
}
}
function appendYAMLValue(fragment, value) {
const trimmed = value.trim();
const leading = value.slice(0, value.length - value.trimStart().length);
const trailingStart = value.length - value.trimEnd().length;
const trailing = trailingStart > 0 ? value.slice(value.length - trailingStart) : "";
if (leading) {
fragment.append(document.createTextNode(leading));
}
if (/^(true|false|null|~)$/i.test(trimmed)) {
appendSyntaxSpan(fragment, trimmed, "yaml-literal");
} else if (/^[+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?$/i.test(trimmed)) {
appendSyntaxSpan(fragment, trimmed, "yaml-number");
} else if (/^(['"]).*\1$/.test(trimmed)) {
appendSyntaxSpan(fragment, trimmed, "yaml-string");
} else {
fragment.append(document.createTextNode(trimmed));
}
if (trailing) {
fragment.append(document.createTextNode(trailing));
}
}
function appendSyntaxSpan(fragment, text, className) {
const span = document.createElement("span");
span.className = className;
span.textContent = text;
fragment.append(span);
}
function findYAMLCommentIndex(line) {
let quote = null;
for (let index = 0; index < line.length; index += 1) {
const character = line[index];
if ((character === "'" || character === "\"") && line[index - 1] !== "\\") {
quote = quote === character ? null : quote || character;
}
if (character === "#" && quote === null && (index === 0 || /\s/.test(line[index - 1]))) {
return index;
}
}
return -1;
}
function formatDuration(milliseconds) {
if (milliseconds < 1000) {
return `${milliseconds.toFixed(1)} ms`;

View File

@ -90,11 +90,30 @@ button {
color: CanvasText;
}
.secondary-button {
margin-top: 0;
border: 1px solid CanvasText;
background: Canvas;
color: CanvasText;
}
button:disabled {
cursor: wait;
opacity: 0.55;
}
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 12px;
}
.section-header h2 {
margin-bottom: 0;
}
.status {
margin-top: 18px;
display: inline-flex;
@ -157,6 +176,45 @@ dd {
font: 13px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace;
}
.stream-document {
height: min(420px, 55vh);
overflow: 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;
}
.yaml-key {
color: #4f8cff;
font-weight: 700;
}
.yaml-punctuation,
.yaml-marker {
color: #b66cff;
font-weight: 700;
}
.yaml-comment {
color: #708090;
font-style: italic;
}
.yaml-string {
color: #138a36;
}
.yaml-number {
color: #b36b00;
}
.yaml-literal {
color: #c21f39;
font-weight: 700;
}
.log-entry {
padding: 6px 0;
border-bottom: 1px solid color-mix(in srgb, CanvasText 8%, transparent);