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); + } + } } }, };