Repository navigation
gc: adaptive tenuring picks survival age 1 and pays 2-5 unproductive full collections per claude-code turn for it #9851
Description
The adaptive tenuring loop selects survival age S = 1 for a large minority of
minors on claude-code, and each turn pays 2-5 full collections that reclaim
0.2-0.3 % for it. Pinning the age removes them, monotonically. This is on
main; it was surfaced by the regex-header-nursery work (#9845) but is not
caused by it — the unmodified allocator arm shows it identically.
Measurement
One 400-character claude-code reply, offline mock-API rig, sandbox rx.
PERRY_GC_DIAG=1 + PERRY_GC_TRACE=1. A = unmodified allocator,
R = #9845's nursery header. =N pins PERRY_GC_TENURING_SURVIVALS.
| arm | tenuring ages chosen | fulls | first-full reclaim | old-gen live at last full |
|---|---|---|---|---|
| A adaptive | {1:16, 2:6, 3:5, 4:15} |
7 | 0.2 % | 49.9 MB |
| A adaptive | {1:12, 2:6, 3:5, 4:15} |
5 | 0.3 % | 48.3 MB |
A pinned =2 |
{2:38} |
3 | 0.3 % | 45.3 MB |
| R adaptive | {1:13, 2:5, 3:5, 4:11} |
5 | 0.2 % | 48.7 MB |
| R adaptive | {1:15, 2:5, 3:4, 4:12} |
7 | 0.2 % | 46.6 MB |
R pinned =2 |
{2:35} |
3 | 0.3 % | 45.5 MB |
R pinned =4 |
{4:36} |
2 | 0.2 % | 34.7 MB |
Two readings:
- The effect is on
main, not on perf(regex): allocate the RegExp header in the nursery, not the malloc arm #9845. A and R adaptive overlap completely
({7,5} against {5,7}) and their age distributions are near-identical, so the
allocator change does not alter tenuring behaviour. A pre-registered
prediction that R would show more fulls was falsified by this matrix. - Fewer fulls, monotone in the pinned age, in both arms: 5-7 adaptive -> 3
at=2-> 2 at=4, with old-gen live at the last full falling 48-50 MB ->
45 MB -> 35 MB.
Why this is a defect and not a tuning preference
The fulls being removed reclaim 0.2-0.3 %. By #9589's rule — price a
collection by what it freed, never by occupancy — they are unproductive, and the
policy is scheduling 2-5 of them per turn because it tenured objects that were
about to die. Promoting on the first survival converts a short-lived object into
old-gen occupancy, and occupancy is what schedules the full.
This is the cost half of the argument; the arena lane owns the producer and the
structural fix (the occupancy rule's S = 1 + desired/influx reaching exactly 1
with no rung at 2 or 3, and the invariant that S = 1 destroys the measurement
that could refute it, since prev_copied = 0 holds the guard false forever).
Its rule — occupancy may lower only to 2, and reaching 1 belongs to the
mortality-measuring exits — is consistent with this matrix.
Three constraints on how these numbers may be used
- Count claim only. This box ran at load 100-300 throughout; its CPU
figures are unusable and none are quoted here. Every number above is a count
or a byte total from the trace. - Not merged with the other host. These fulls reclaim 0.2-0.3 %; the same
arms on a quiet host reclaim 14-25 %. The two hosts sit in different regions
of the same policy and their numbers are not combined into one table. PERRY_GC_TENURING_SURVIVALS=2is the diagnostic, not the fix. It is
documented diagnostic-only, and moving a threshold is explicitly not the
remedy being proposed.
Reproduce
stream_scale.py LABEL HOME PORT 100 400 150 \
--env PERRY_GC_DIAG=1 --env PERRY_GC_TRACE=1 --stderr OUT.txt -- <binary>
stream_scale.py LABEL HOME PORT 100 400 150 \
--env PERRY_GC_DIAG=1 --env PERRY_GC_TRACE=1 \
--env PERRY_GC_TENURING_SURVIVALS=2 --stderr OUT_t2.txt -- <binary>
Count collection_kind == "full" records, and read tenuring_survivals= off
the [gc-copy-minor] ran lines.
Activity
fix(gc): the survival-rate lock rates one fresh cohort, not the whole…
fix(gc): the tenuring occupancy rule may not claim promote-on-first-c…
fix(gc): the survival-rate lock rates one fresh cohort, not the whole…
fix(gc): the occupancy rule may not claim the CEILING before any roun…
fix(gc): the tenuring occupancy rule may not claim promote-on-first-c…
fix(gc): the survival-rate lock rates one fresh cohort, not the whole…
fix(gc): the occupancy rule may not claim the CEILING before any roun…
fix(gc): the tenuring occupancy rule may not claim promote-on-first-c…
fix(gc): the survival-rate lock rates one fresh cohort, not the whole…
fix(gc): the occupancy rule may not claim the CEILING before any roun…
proggeramlug commentedon Sep 7, 2026
Fixed by #9861, landed on
mainvia merge train #9978. The train merged as its own branch, so the close-keyword never fired — closing manually, verified on main.