mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-09 08:59:40 +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);
|
const segments = extractSyncSegments(joined);
|
||||||
|
|
||||||
// Write all segments in a single batch (atomic within this frame).
|
// Write segments respecting a per-frame byte budget.
|
||||||
// xterm.js internally batches multiple write() calls within same frame.
|
// Each DEC 2026 sync segment is a complete Ink redraw — writing whole segments
|
||||||
// Never discard content from incomplete sync blocks — xterm.js doesn't support
|
// preserves atomicity (no flicker). But when total data exceeds 48KB, defer
|
||||||
// DEC 2026 natively anyway, so strip the marker and write content regardless.
|
// remaining segments to the next frame to prevent terminal.write() from blocking
|
||||||
// Discarding causes real data loss (including Ink's erase-line escapes).
|
// the main thread. 141KB single-frame writes have been observed to freeze Chrome
|
||||||
for (const segment of segments) {
|
// for 2+ minutes even with the canvas renderer.
|
||||||
if (segment) {
|
const MAX_FRAME_BYTES = 49152; // 48KB budget per frame
|
||||||
const content = segment.startsWith(DEC_SYNC_START)
|
let bytesThisFrame = 0;
|
||||||
? segment.slice(DEC_SYNC_START.length)
|
let deferred = false;
|
||||||
: segment;
|
|
||||||
if (content) this.terminal.write(content);
|
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;
|
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
|
// Sticky scroll: if user was at bottom, keep them there after new output
|
||||||
if (this._wasAtBottomBeforeWrite) {
|
if (this._wasAtBottomBeforeWrite) {
|
||||||
|
|||||||
Reference in New Issue
Block a user