Skip to content

gc: the ArenaBytes productivity backoff is computed and then discarded above the trigger ceiling #9831

Description

@proggeramlug

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions