Measured 2026-09-03 on the verified cc build (macOS, main 8d27f7b964), three-arm PTY experiment (idle → 400 KB paste-churn → 4-min settle), footprint sampled every 10 s. Raw data /tmp/gc_arms.csv on the dev box.
| arm |
idle sawtooth trough |
post-churn (4 min) |
| default |
449 MB |
425 MB, flat — zero collections |
MAJOR_PACING_GROWTH=1 FLOOR_MB=8 |
~365 MB |
564 MB, flat |
+ OLD_DEFRAG=1 |
~365 MB |
458 MB, flat |
Finding 1 — no idle-time collection (the structural gap)
Collections trigger on allocation. An idle TUI barely allocates ⇒ after a burst, the heap freezes at whatever the last mid-burst cycle left — the post-churn column is last-collection timing luck (564 > 425 under more aggressive pacing proves it), and it persists indefinitely. A live user session sat pinned at 680 MB through minutes of idle for exactly this reason. Node idles the same bundle at ~150–200 MB because V8 runs an idle memory-reducer; perry has no equivalent. Fix shape: an idle hook (the event loop knows it is parking — the #9540 body_check site is where "about to idle" is known) that after N quiet seconds runs a major + page return, budgeted so a keystroke never lands mid-cycle (incremental or bounded pause).
Finding 2 — default pacing + defrag leave ~200 MB combined
Aggressive pacing lowers the idle trough 449→365 (~85 MB). OLD_DEFRAG=1 reclaimed ~105 MB of post-burst residue. Both knobs work today; the defaults are tuned for throughput benchmarks, not resident TUIs. Proposal: idle-reducer runs with the aggressive profile (collect-to-live + defrag) regardless of the throughput defaults, since by definition nobody is waiting.
Finding 3 — bounded by a separate census
Fresh-idle live set is ~330 MB GC (vs node's whole process 150–200) — that remainder is representation/side-tables territory, being censused separately; this issue is only the two collector-behaviour gaps above.
Verification bar
The three-arm PTY harness is the regression test shape: post-churn line must descend within N idle seconds instead of staying flat, without keystroke-latency regression (assert input-echo timing under the reducer). The 2.1 GB transient peak during heavy ops should also shrink under pacing review — assert peak in the harness too.
Measured 2026-09-03 on the verified cc build (macOS, main
8d27f7b964), three-arm PTY experiment (idle → 400 KB paste-churn → 4-min settle), footprint sampled every 10 s. Raw data/tmp/gc_arms.csvon the dev box.MAJOR_PACING_GROWTH=1 FLOOR_MB=8OLD_DEFRAG=1Finding 1 — no idle-time collection (the structural gap)
Collections trigger on allocation. An idle TUI barely allocates ⇒ after a burst, the heap freezes at whatever the last mid-burst cycle left — the post-churn column is last-collection timing luck (564 > 425 under more aggressive pacing proves it), and it persists indefinitely. A live user session sat pinned at 680 MB through minutes of idle for exactly this reason. Node idles the same bundle at ~150–200 MB because V8 runs an idle memory-reducer; perry has no equivalent. Fix shape: an idle hook (the event loop knows it is parking — the #9540
body_checksite is where "about to idle" is known) that after N quiet seconds runs a major + page return, budgeted so a keystroke never lands mid-cycle (incremental or bounded pause).Finding 2 — default pacing + defrag leave ~200 MB combined
Aggressive pacing lowers the idle trough 449→365 (~85 MB).
OLD_DEFRAG=1reclaimed ~105 MB of post-burst residue. Both knobs work today; the defaults are tuned for throughput benchmarks, not resident TUIs. Proposal: idle-reducer runs with the aggressive profile (collect-to-live + defrag) regardless of the throughput defaults, since by definition nobody is waiting.Finding 3 — bounded by a separate census
Fresh-idle live set is ~330 MB GC (vs node's whole process 150–200) — that remainder is representation/side-tables territory, being censused separately; this issue is only the two collector-behaviour gaps above.
Verification bar
The three-arm PTY harness is the regression test shape: post-churn line must descend within N idle seconds instead of staying flat, without keystroke-latency regression (assert input-echo timing under the reducer). The 2.1 GB transient peak during heavy ops should also shrink under pacing review — assert peak in the harness too.