You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
perf(main): PTY replay buffers copy the whole cap on every chunk and retain sliced-string parents #726
Every PTY chunk from every live session costs the main process a copy of the whole replay buffer, and the retained buffer can pin up to twice its nominal cap.
src/main/sessionManager.ts keeps two per-session replay buffers as plain strings — terminalBuffers (cap 256 KiB) and agentPtyBuffers (cap 512 KiB) — and appends with appendCappedBuffer (~L404):
letnext=prev+data// ConsString
...
constfirstCode=next.charCodeAt(startIdx)// forces a flatten: fresh (cap + chunk) copynext=next.slice(startIdx)// SlicedString that RETAINS the flattened parent
Once a buffer is full (which every agent TUI reaches within minutes — full-screen repaints), each chunk allocates ~cap + chunk bytes and the stored value is a slice whose parent is that whole allocation. With 20 sessions emitting repaint chunks continuously this is on the order of 100+ MB/s of short-lived string allocation on the main thread, i.e. steady GC pressure of the #390 class, and the retained per-session buffer is cap + last chunk, not cap.
Evidence
The 2026-09-03 main-process heap snapshot (--owners / string survey, see #720 for the run) contains six identical 1.8 MB strings beginning with a docker [+] Building ANSI repaint and ~10 identical ~1 MB Claude screen-frame strings: PTY chunks larger than the cap become the flattened parent that the stored slice keeps alive, one per session that saw them. [debug-retention] and heartbeat records for the run show main eventLoop.meanMs up to 1591 during heavy multi-session output.
Expected
Appending a chunk should be amortised O(chunk), not O(cap); the retained size should never exceed the cap; and a chunk larger than the cap should be stored as a real copy of its tail, not a slice over the oversized chunk (#321 lesson: V8 slices keep their parent alive).
Proposed shape
Replace the string-per-session with a small chunk-list buffer (append pushes the chunk and drops whole oldest chunks while over the cap; read joins on attach, which is rare). Both buffers keep their exact contract: bytes accumulate from process start, attachTerminal / attachAgentPty return the current tail atomically, and the caps are unchanged.
Impact
Main-process GC churn proportional to (sessions × chunk rate × cap); up to 2× cap retained per session; oversized chunks retained in full.
Activity
perf(sessions): cut oversized chunks once and pin piece boundaries in…