diff --git a/wasm/app/app.js b/wasm/app/app.js
index 1f9d71e..67a172e 100644
--- a/wasm/app/app.js
+++ b/wasm/app/app.js
@@ -261,27 +261,63 @@ function startLiveBlock(tierName) {
pre.textContent = "";
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 = "";
+
return {
append(chunk) {
- // Append a TextNode rather than `pre.textContent += chunk`
- // — the latter re-reads + restringifies + re-sets all
- // existing children on every chunk, which under a fast
- // C-tier loop (one chunk per (newline) printf) was
- // dropping line breaks and rendering "tick 0tick 10000…"
- // horizontally instead of stacking vertically. TextNode
- // append preserves every byte verbatim.
- pre.appendChild(document.createTextNode(chunk));
+ if (!chunk) return;
+ let start = 0;
+ for (let i = 0; i < chunk.length; i++) {
+ if (chunk.charCodeAt(i) === 10) {
+ pendingText += chunk.slice(start, i);
+ pendingLine.textContent = pendingText || " ";
+ pendingText = "";
+ pendingLine = makeLine();
+ pre.appendChild(pendingLine);
+ start = i + 1;
+ }
+ }
+ if (start < chunk.length) {
+ pendingText += chunk.slice(start);
+ pendingLine.textContent = pendingText;
+ }
outputEl.scrollTop = outputEl.scrollHeight;
},
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";
- // If the streamed chunks miss anything (e.g. asm tier
- // which doesn't stream yet), reconcile with the full
- // output. No-op when streaming captured everything.
- if (fullOutput && pre.textContent !== fullOutput) {
- pre.textContent = fullOutput;
+ // 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.
+ if (fullOutput) {
+ const got = Array.from(pre.children).map((d) =>
+ d.textContent === " " ? "" : d.textContent).join("\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);
+ }
+ }
}
},
};
diff --git a/www/playground/app.js b/www/playground/app.js
index 1f9d71e..67a172e 100644
--- a/www/playground/app.js
+++ b/www/playground/app.js
@@ -261,27 +261,63 @@ function startLiveBlock(tierName) {
pre.textContent = "";
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 = "";
+
return {
append(chunk) {
- // Append a TextNode rather than `pre.textContent += chunk`
- // — the latter re-reads + restringifies + re-sets all
- // existing children on every chunk, which under a fast
- // C-tier loop (one chunk per (newline) printf) was
- // dropping line breaks and rendering "tick 0tick 10000…"
- // horizontally instead of stacking vertically. TextNode
- // append preserves every byte verbatim.
- pre.appendChild(document.createTextNode(chunk));
+ if (!chunk) return;
+ let start = 0;
+ for (let i = 0; i < chunk.length; i++) {
+ if (chunk.charCodeAt(i) === 10) {
+ pendingText += chunk.slice(start, i);
+ pendingLine.textContent = pendingText || " ";
+ pendingText = "";
+ pendingLine = makeLine();
+ pre.appendChild(pendingLine);
+ start = i + 1;
+ }
+ }
+ if (start < chunk.length) {
+ pendingText += chunk.slice(start);
+ pendingLine.textContent = pendingText;
+ }
outputEl.scrollTop = outputEl.scrollHeight;
},
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";
- // If the streamed chunks miss anything (e.g. asm tier
- // which doesn't stream yet), reconcile with the full
- // output. No-op when streaming captured everything.
- if (fullOutput && pre.textContent !== fullOutput) {
- pre.textContent = fullOutput;
+ // 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.
+ if (fullOutput) {
+ const got = Array.from(pre.children).map((d) =>
+ d.textContent === " " ? "" : d.textContent).join("\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);
+ }
+ }
}
},
};