diff --git a/wasm/app/app.js b/wasm/app/app.js index c7a27de..f11fe80 100644 --- a/wasm/app/app.js +++ b/wasm/app/app.js @@ -277,60 +277,52 @@ function startLiveBlock(tierName) { block.appendChild(pre); outputEl.appendChild(block); - // Each chunk gets split on \n: every completed line becomes its - // own block-level
wrapper already preserves \n as a real line break, so
+ // we don't need per-line elements. RAF coalesces chunk pile-ups
+ // so a tight (display) loop can't drown the main thread.
+ pre.style.whiteSpace = "pre";
+ const textNode = document.createTextNode("");
+ pre.appendChild(textNode);
+ let buffered = "";
+ let rafPending = false;
+ function flushBuffered() {
+ rafPending = false;
+ if (!buffered) return;
+ textNode.appendData(buffered);
+ buffered = "";
+ outputEl.scrollTop = outputEl.scrollHeight;
+ }
+ function scheduleFlush() {
+ if (rafPending) return;
+ rafPending = true;
+ requestAnimationFrame(flushBuffered);
+ }
return {
appendText(t) {
- // Pure text chunk — never contains a newline. Append to
- // the in-progress line.
if (!t) return;
- pendingText += t;
- pendingLine.textContent = pendingText;
- outputEl.scrollTop = outputEl.scrollHeight;
+ buffered += t;
+ scheduleFlush();
},
appendNewline() {
- // End-of-line event — close the current line (with a
- // single-space placeholder when empty so the row is
- // visible) and spawn a fresh pending line below.
- pendingLine.textContent = pendingText || " ";
- pendingText = "";
- pendingLine = makeLine();
- pre.appendChild(pendingLine);
- outputEl.scrollTop = outputEl.scrollHeight;
+ buffered += "\n";
+ scheduleFlush();
},
finalize(elapsedMs, kind, fullOutput) {
if (kind === "cancelled") t.textContent = ` (cancelled @ ${elapsedMs.toFixed(0)} ms)`;
else t.textContent = ` (${elapsedMs.toFixed(0)} ms)`;
if (kind === "error") pre.className = "err";
- // Reconcile against the full output if the streamed
- // per-line divs don't add up — e.g. asm-tier fallback
- // where streaming isn't wired yet. Otherwise the joined
- // per-line text should match the eval's full output.
+ // Drain any RAF-buffered chunks then reconcile against the
+ // full eval output — covers the asm-tier fallback where
+ // streaming isn't wired and the live text node stays empty
+ // until finalize lands the canonical value.
+ if (rafPending) flushBuffered();
if (fullOutput) {
- const got = Array.from(pre.children).map((d) =>
- d.textContent === " " ? "" : d.textContent).join("\n");
+ const got = textNode.nodeValue.replace(/\n$/, "");
if (got !== fullOutput.replace(/\n$/, "")) {
- pre.textContent = "";
- const lines = fullOutput.split("\n");
- for (const ln of lines) {
- const d = makeLine();
- d.textContent = ln || " ";
- pre.appendChild(d);
- }
+ textNode.nodeValue = fullOutput;
}
}
},
diff --git a/www/playground/app.js b/www/playground/app.js
index c7a27de..f11fe80 100644
--- a/www/playground/app.js
+++ b/www/playground/app.js
@@ -277,60 +277,52 @@ function startLiveBlock(tierName) {
block.appendChild(pre);
outputEl.appendChild(block);
- // Each chunk gets split on \n: every completed line becomes its
- // own block-level , the trailing fragment without a newline
- // accumulates in a pending div that's updated in place until the
- // next \n closes it. display:block + white-space:pre set INLINE
- // so a stale cached stylesheet can't suppress the fix — we kept
- // chasing what turned out to be CSS not reloading.
- const makeLine = () => {
- const d = document.createElement("div");
- d.style.display = "block";
- d.style.whiteSpace = "pre";
- return d;
- };
- let pendingLine = makeLine();
- pre.appendChild(pendingLine);
- let pendingText = "";
+ // Single Text node accumulator — appendData is ~1000x faster
+ // than appendChild(div) per line on high-volume streams. The
+ // wrapper already preserves \n as a real line break, so
+ // we don't need per-line elements. RAF coalesces chunk pile-ups
+ // so a tight (display) loop can't drown the main thread.
+ pre.style.whiteSpace = "pre";
+ const textNode = document.createTextNode("");
+ pre.appendChild(textNode);
+ let buffered = "";
+ let rafPending = false;
+ function flushBuffered() {
+ rafPending = false;
+ if (!buffered) return;
+ textNode.appendData(buffered);
+ buffered = "";
+ outputEl.scrollTop = outputEl.scrollHeight;
+ }
+ function scheduleFlush() {
+ if (rafPending) return;
+ rafPending = true;
+ requestAnimationFrame(flushBuffered);
+ }
return {
appendText(t) {
- // Pure text chunk — never contains a newline. Append to
- // the in-progress line.
if (!t) return;
- pendingText += t;
- pendingLine.textContent = pendingText;
- outputEl.scrollTop = outputEl.scrollHeight;
+ buffered += t;
+ scheduleFlush();
},
appendNewline() {
- // End-of-line event — close the current line (with a
- // single-space placeholder when empty so the row is
- // visible) and spawn a fresh pending line below.
- pendingLine.textContent = pendingText || " ";
- pendingText = "";
- pendingLine = makeLine();
- pre.appendChild(pendingLine);
- outputEl.scrollTop = outputEl.scrollHeight;
+ buffered += "\n";
+ scheduleFlush();
},
finalize(elapsedMs, kind, fullOutput) {
if (kind === "cancelled") t.textContent = ` (cancelled @ ${elapsedMs.toFixed(0)} ms)`;
else t.textContent = ` (${elapsedMs.toFixed(0)} ms)`;
if (kind === "error") pre.className = "err";
- // Reconcile against the full output if the streamed
- // per-line divs don't add up — e.g. asm-tier fallback
- // where streaming isn't wired yet. Otherwise the joined
- // per-line text should match the eval's full output.
+ // Drain any RAF-buffered chunks then reconcile against the
+ // full eval output — covers the asm-tier fallback where
+ // streaming isn't wired and the live text node stays empty
+ // until finalize lands the canonical value.
+ if (rafPending) flushBuffered();
if (fullOutput) {
- const got = Array.from(pre.children).map((d) =>
- d.textContent === " " ? "" : d.textContent).join("\n");
+ const got = textNode.nodeValue.replace(/\n$/, "");
if (got !== fullOutput.replace(/\n$/, "")) {
- pre.textContent = "";
- const lines = fullOutput.split("\n");
- for (const ln of lines) {
- const d = makeLine();
- d.textContent = ln || " ";
- pre.appendChild(d);
- }
+ textNode.nodeValue = fullOutput;
}
}
},