mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-02 13:39:41 +02:00
fix: cap terminal writes at 48KB/frame to prevent page unresponsive freezes
Root cause was NOT WebGL — breadcrumbs showed 141KB single-frame terminal.write() calls freezing Chrome for 2+ minutes even with the canvas renderer. During heavy Ink output, multiple SSE terminal events accumulate between animation frames and flush all at once. Fix: split flushPendingWrites at DEC 2026 sync segment boundaries with a 48KB per-frame budget. Each segment is a complete Ink redraw, so splitting between them preserves atomicity (no flicker). Excess segments are deferred to the next requestAnimationFrame. Also re-enable WebGL since it was not the cause — the flush cap protects both renderers equally. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+41
-12
@@ -1381,21 +1381,50 @@ class CodemanApp {
|
||||
|
||||
const segments = extractSyncSegments(joined);
|
||||
|
||||
// Write all segments in a single batch (atomic within this frame).
|
||||
// xterm.js internally batches multiple write() calls within same frame.
|
||||
// Never discard content from incomplete sync blocks — xterm.js doesn't support
|
||||
// DEC 2026 natively anyway, so strip the marker and write content regardless.
|
||||
// Discarding causes real data loss (including Ink's erase-line escapes).
|
||||
for (const segment of segments) {
|
||||
if (segment) {
|
||||
const content = segment.startsWith(DEC_SYNC_START)
|
||||
? segment.slice(DEC_SYNC_START.length)
|
||||
: segment;
|
||||
if (content) this.terminal.write(content);
|
||||
// Write segments respecting a per-frame byte budget.
|
||||
// Each DEC 2026 sync segment is a complete Ink redraw — writing whole segments
|
||||
// preserves atomicity (no flicker). But when total data exceeds 48KB, defer
|
||||
// remaining segments to the next frame to prevent terminal.write() from blocking
|
||||
// the main thread. 141KB single-frame writes have been observed to freeze Chrome
|
||||
// for 2+ minutes even with the canvas renderer.
|
||||
const MAX_FRAME_BYTES = 49152; // 48KB budget per frame
|
||||
let bytesThisFrame = 0;
|
||||
let deferred = false;
|
||||
|
||||
for (let i = 0; i < segments.length; i++) {
|
||||
const segment = segments[i];
|
||||
if (!segment) continue;
|
||||
const content = segment.startsWith(DEC_SYNC_START)
|
||||
? segment.slice(DEC_SYNC_START.length)
|
||||
: segment;
|
||||
if (!content) continue;
|
||||
|
||||
// If we'd exceed the budget, defer this and all remaining segments
|
||||
if (bytesThisFrame > 0 && bytesThisFrame + content.length > MAX_FRAME_BYTES) {
|
||||
// Re-queue remaining segments as raw content for next flush
|
||||
const remaining = segments.slice(i).map(s => {
|
||||
if (!s) return '';
|
||||
return s.startsWith(DEC_SYNC_START) ? s.slice(DEC_SYNC_START.length) : s;
|
||||
}).filter(Boolean).join('');
|
||||
if (remaining) {
|
||||
this.pendingWrites.push(remaining);
|
||||
if (!this.writeFrameScheduled) {
|
||||
this.writeFrameScheduled = true;
|
||||
requestAnimationFrame(() => {
|
||||
this.flushPendingWrites();
|
||||
this.writeFrameScheduled = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
deferred = true;
|
||||
break;
|
||||
}
|
||||
|
||||
this.terminal.write(content);
|
||||
bytesThisFrame += content.length;
|
||||
}
|
||||
const _dt = performance.now() - _t0;
|
||||
if (_dt > 100) console.warn(`[CRASH-DIAG] flushPendingWrites took ${_dt.toFixed(0)}ms (${_joinedLen} bytes)`);
|
||||
if (_dt > 100 || deferred) console.warn(`[CRASH-DIAG] flushPendingWrites: ${_dt.toFixed(0)}ms, ${(bytesThisFrame/1024).toFixed(0)}KB written${deferred ? ', rest deferred' : ''} (total ${(_joinedLen/1024).toFixed(0)}KB)`);
|
||||
|
||||
// Sticky scroll: if user was at bottom, keep them there after new output
|
||||
if (this._wasAtBottomBeforeWrite) {
|
||||
|
||||
Reference in New Issue
Block a user