gc: the ArenaBytes productivity backoff is computed and then discarded above the trigger ceiling #9831
Description
Summary
gc_finish_arena_trigger_collection maintains an adaptive step that is meant
to back the ArenaBytes threshold off when collections stop being productive.
Above gc_trigger_absolute_ceiling_bytes() that signal is computed, stored,
doubled, and then discarded — the threshold arithmetic cannot see it, so the
arm re-arms at a fixed new_total + 16 MB no matter how little the last
collection freed.
This is the shape of #9589, where the idle reducer scored every full
unproductive because old_gen_in_use_bytes is a sum of bump offsets a sweep
cannot lower. There the fix was to price the cycle by its own freed bytes. Here
the price is already computed and thrown away.
Mechanism
let stepped = new_total.saturating_add(step);
let capped = stepped.min(gc_trigger_absolute_ceiling_bytes());
let floor = new_total.saturating_add(gc_trigger_headroom_floor_bytes());
let next_trigger = std::cmp::max(capped, floor) + credit;gc_trigger_absolute_ceiling_bytes()is a quarter of the device budget capped
at 128 MB.stepstarts at 128 MB (GC_STEP_INITIAL) and doubles toward
GC_THRESHOLD_MAX_BYTES(1 GB) wheneverpct_freedfalls outside 10–84 %.
So new_total + step clears the ceiling on the first collection and capped
is pinned at the ceiling for the rest of the process. Once new_total passes
ceiling − headroom_floor, max(capped, floor) selects floor every time, and
step stops affecting the result at all.
Measured
Compiled claude-code TUI, 3300-character streamed reply, PERRY_GC_DIAG=1,
66 [gc-step] lines:
median pct_freed |
0 % |
median sweep_freed |
203 KB |
median block_reclaim |
65 KB |
median step |
1,073,741,824 (saturated at the 1 GB maximum) |
pre_in_use range |
19 MB → 297 MB |
The backoff was at its maximum for essentially the whole turn and had no effect.
The arm fired 51 times in one reply, each freeing a median of 131 KB
against MallocCount's 10.0 MB in the same run.
Tests
Branch perf/arena-trigger-pricing on proggeramlug/perry extracts the
computation as a pure function — the bug is invisible in the fused expression —
and adds gc::tests::arena_trigger_pricing:
fn arena_trigger_headroom_bytes(step: usize) -> usize {
gc_trigger_headroom_floor_bytes().max(step.min(gc_trigger_absolute_ceiling_bytes()))
}a_productive_collection_keeps_the_headroom_flooran_unproductive_collection_earns_more_headroom_than_a_productive_oneheadroom_is_bounded_by_the_absolute_ceiling
Sabotage-proved: restoring the old arithmetic (headroom = floor, step
ignored) fails exactly the second and third and leaves the first passing.
Why this is filed as an issue and not a PR
Correcting the arithmetic changes behaviour, and the behaviour it produces is
not currently acceptable. Honouring the backoff is precisely what makes the
heap grow: measured on the rig over 3 interleaved rounds, the corrected version
buys −10.8 % turn CPU at 3300 characters and costs settled footprint
754 → 920 MB and peak RSS 968 → 1223 MB. A CPU win with a footprint regression
is rejected under the campaign's directive, so the fix must land together with
whatever makes backing off safe — not on its own.
The wider investigation into that arm is written up in
secret-tests/cc-perf-campaign/HANDOFF_arenabytes_solution_space.md: three
variants tried, all refuted, with the reasons.
A latent defect documented with a test that fails under sabotage is worth more
than a behaviour change nobody can accept.
Activity
fix(gc): price the tiny-parse pressure guard by the productivity backoff
fix(gc): re-baseline the whole-arena trigger after a malloc-pressure …
fix(train): carry #9838's PASS1_MARKED re-audit for the #9831 policy.…
proggeramlug commentedon Sep 6, 2026
Fixed by #9838, landed on
mainvia merge train #9875. The train merged as its own branch, so the close-keyword never fired — closing manually, verified on main.fix(gc): price the tiny-parse pressure guard by the productivity backoff
fix(gc): re-arm the idle reclaimer on elapsed idle, not only on colle…
perf(gc): let a tiny-parse boundary collect when the young generation…