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.
step starts at 128 MB (GC_STEP_INITIAL) and doubles toward
GC_THRESHOLD_MAX_BYTES (1 GB) whenever pct_freed falls 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_floor
an_unproductive_collection_earns_more_headroom_than_a_productive_one
headroom_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.
Summary
gc_finish_arena_trigger_collectionmaintains an adaptivestepthat is meantto back the
ArenaBytesthreshold 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 MBno matter how little the lastcollection freed.
This is the shape of #9589, where the idle reducer scored every full
unproductive because
old_gen_in_use_bytesis a sum of bump offsets a sweepcannot lower. There the fix was to price the cycle by its own freed bytes. Here
the price is already computed and thrown away.
Mechanism
gc_trigger_absolute_ceiling_bytes()is a quarter of the device budget cappedat 128 MB.
stepstarts at 128 MB (GC_STEP_INITIAL) and doubles towardGC_THRESHOLD_MAX_BYTES(1 GB) wheneverpct_freedfalls outside 10–84 %.So
new_total + stepclears the ceiling on the first collection andcappedis pinned at the ceiling for the rest of the process. Once
new_totalpassesceiling − headroom_floor,max(capped, floor)selectsfloorevery time, andstepstops affecting the result at all.Measured
Compiled claude-code TUI, 3300-character streamed reply,
PERRY_GC_DIAG=1,66
[gc-step]lines:pct_freedsweep_freedblock_reclaimsteppre_in_userangeThe 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-pricingonproggeramlug/perryextracts thecomputation as a pure function — the bug is invisible in the fused expression —
and adds
gc::tests::arena_trigger_pricing:a_productive_collection_keeps_the_headroom_flooran_unproductive_collection_earns_more_headroom_than_a_productive_oneheadroom_is_bounded_by_the_absolute_ceilingSabotage-proved: restoring the old arithmetic (headroom = floor,
stepignored) 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: threevariants 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.