feat: show streaming document preview
This commit is contained in:
parent
4a8978a1ef
commit
442e84d94a
34
.github/workflows/wasm-site.yml
vendored
34
.github/workflows/wasm-site.yml
vendored
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
139
WasmSite/main.js
139
WasmSite/main.js
@ -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`;
|
||||
|
||||
@ -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);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user