You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
GC: copying minor reports not_attempted on every cycle in the memory-stability gates; old_page_forced_defrag moved > selected #9659
compile-smoke's step 14 (./scripts/run_memory_stability_tests.sh) fails two GC gates. Both are architectural assertions that the copying minor actually did work — the #7025 class of "a gate must assert its subject was live".
Run: 33743461798 (job compile-smoke), evidence artifact gc-evidence-Linux (id 9914097984).
1. The copying minor is never attempted
Five workloads report every cycle as not_attempted, with zero bytes moved:
workload
cycles
not_attempted
copied
promoted
moved
default_copying
8
8
0
0
0
string_heavy
4
4
0
0
0
closure_heavy
5
5
0
0
0
large_object_barriers
4
4
0
0
0
raw_numeric_layouts
3
3
0
0
0
Critically, this is not a declined copy. Every other fallback reason is zero:
conservative_stack.scan_bytes = 0, truncated_cycles = 0, unbounded_cycles = 0 — so the usual "a conservative scan made the copying minor ineligible" explanation does not apply here. The minor path appears not to be entered at all.
That workload does run (copy_only_roots=1, moved_bytes=80), so this is an accounting inconsistency rather than an inactive path.
Why this surfaced now
This gate runs in exactly one place — test.yml:2837, inside compile-smoke, which only runs in the full tier. The last two release cycles (runs 33626738093, 33709074616) both failed at step 8 (Compile all test files) and never reached step 14. With step 8 fixed, step 14 executed for the first time in months.
So this is almost certainly pre-existing and was dark, not a regression introduced by the release candidate. The scripts were last touched by 41e8479a56 ("clear r21 full-suite blockers", 2026-08-27), which suggests they last passed around then.
What still passes
Everything else in the step is green, including the workloads that exercise real behaviour: all canaries, raw numeric object field semantics, and the [gc-trace] workloads json_roundtrip, string_churn, object_property_churn, mixed_request_shaping, map_set_churn, promise_churn. gc-stress (all legs) and gc-ratchet pass in the same tier.
So the failing assertions are about whether the copying minor was exercised, not about observed incorrect behaviour.
Ask
Is not_attempted on every cycle expected for these workloads in CI (e.g. they never reach the nursery pressure that triggers a moving minor), or has the moving-minor path stopped being entered? The former means the gate needs a liveness precondition; the latter is a real architectural regression.
The whole heap is 6.4 KB, so nothing ever reaches nursery pressure — every collection comes from an explicit gc(). Per CLAUDE.md, a manual gc() runs a full mark-sweep and only runs an evacuating minor first when PERRY_GC_FORCE_EVACUATE=1 is set (#6946). These five set neither the knob nor enough allocation to trigger a natural minor, so the copying minor is correctly not_attempted — and the gate then fails them for it.
So the assertion is unconditional while the configuration that satisfies it is per-workload. That is a category error in the gate, not a defect in the collector.
Note on why it surfaced now
This runs only at test.yml:2837 inside compile-smoke, full tier only. Runs 33626738093 and 33709074616 both died at step 8 and never reached it, so the gate has been dark for months.
Options
Scope the assertion to workloads that force evacuation — the copying-minor assertion stays fully in force wherever a copying minor is actually exercised, and stops being applied where one cannot occur. Minimal and correct.
Add PERRY_GC_FORCE_EVACUATE=1 to the other five — broadens real copying coverage, and is what the gate seems to have intended. Higher value, but it makes five workloads newly exercise evacuation, which may surface genuine findings; that deserves its own change rather than riding a release.
old_page_forced_defrag's old_page_moved_bytes(80) > old_page_selected_live_bytes(32) is unrelated to the above and still stands as a real accounting inconsistency.
Correction: the inversion is real, but inverting it is NOT the fix
I tried the obvious repair — flip target_gates_require_copied_minor so the requirement lands on the workload that forces evacuation — and ran it against this run's real report. It only moves the failure:
async_promise_closures: fallback reasons other than none: {'not_attempted': 5}
async_promise_closures: copied-minor ineligible cycles=5
async_promise_closures: malloc_registry_rebuilds=1, want 0
Reverted; nothing proposed from it.
The actual contract problem
The strict block asserts three separate things:
assertion
demands
fallback reasons other than none
every cycle attempted a copying minor
ineligible_cycles == 0
every cycle was eligible
copied_objects + promoted_objects > 0
at least one cycle moved something
The third is the meaningful "the subject was live" check. The first two demand that 100% of collections in the workload be copying minors — which no workload here satisfies:
async_promise_closures attempts copying on 5 of 10 cycles (and copies 486,088 B, promotes 158,648 B) → fails the first two.
The other five attempt it on 0 of N → fail all three.
So no workload in the suite can pass the strict block as written, whichever way the predicate points. That is the real defect, and it is larger than a predicate flip.
What is established
The collector is healthy.async_promise_closures copies 486 KB and promotes 159 KB; pointer_free_numeric copies 528 B with not_attempted=0.
The gate has been dark for months (full tier only, and the last two cycles died before reaching it), so this contract has not been exercised in a long time.
What the fix needs to decide (maintainer call)
Should a workload be allowed to mix full sweeps with copying minors — i.e. should not_attempted / ineligible_cycles be tolerated while still requiring productive > 0 and still failing on real fallback reasons (conservative_stack, pinned_young_*, barriers_inactive)? That looks right to me, but it changes what the gate promises.
Should the five non-forcing workloads get PERRY_GC_FORCE_EVACUATE=1 so they genuinely exercise copying, rather than being exempted?
async_promise_closures: malloc_registry_rebuilds=1, want 0 is a separate assertion that also fails and needs its own answer.
I am deliberately not guessing at these on a release pin.
Activity
proggeramlug commentedon Sep 3, 2026
Resolved: this is a mis-specified gate, not a collector regression
Pulled the per-workload numbers out of the evidence artifact. The correlation is exact:
PERRY_GC_FORCE_EVACUATE=1async_promise_closuresdefault_copyingstring_heavyclosure_heavylarge_object_barriersraw_numeric_layoutsThe five failing workloads are exactly the five that do not set the knob, in
workload_specsinrun_target_collector_architecture_gates.The copying minor is healthy.
async_promise_closurescopies 486 KB and promotes 159 KB, andpointer_free_numericcopies 528 B withnot_attempted=0.Why the other five report
not_attemptedon every cycleTheir raw traces show every collection is an explicit one:
The whole heap is 6.4 KB, so nothing ever reaches nursery pressure — every collection comes from an explicit
gc(). Per CLAUDE.md, a manualgc()runs a full mark-sweep and only runs an evacuating minor first whenPERRY_GC_FORCE_EVACUATE=1is set (#6946). These five set neither the knob nor enough allocation to trigger a natural minor, so the copying minor is correctlynot_attempted— and the gate then fails them for it.So the assertion is unconditional while the configuration that satisfies it is per-workload. That is a category error in the gate, not a defect in the collector.
Note on why it surfaced now
This runs only at
test.yml:2837insidecompile-smoke, full tier only. Runs33626738093and33709074616both died at step 8 and never reached it, so the gate has been dark for months.Options
PERRY_GC_FORCE_EVACUATE=1to the other five — broadens real copying coverage, and is what the gate seems to have intended. Higher value, but it makes five workloads newly exercise evacuation, which may surface genuine findings; that deserves its own change rather than riding a release.old_page_forced_defrag'sold_page_moved_bytes(80) > old_page_selected_live_bytes(32)is unrelated to the above and still stands as a real accounting inconsistency.proggeramlug commentedon Sep 3, 2026
Correction: the inversion is real, but inverting it is NOT the fix
I tried the obvious repair — flip
target_gates_require_copied_minorso the requirement lands on the workload that forces evacuation — and ran it against this run's real report. It only moves the failure:Reverted; nothing proposed from it.
The actual contract problem
The strict block asserts three separate things:
fallback reasons other than noneineligible_cycles == 0copied_objects + promoted_objects > 0The third is the meaningful "the subject was live" check. The first two demand that 100% of collections in the workload be copying minors — which no workload here satisfies:
async_promise_closuresattempts copying on 5 of 10 cycles (and copies 486,088 B, promotes 158,648 B) → fails the first two.So no workload in the suite can pass the strict block as written, whichever way the predicate points. That is the real defect, and it is larger than a predicate flip.
What is established
async_promise_closurescopies 486 KB and promotes 159 KB;pointer_free_numericcopies 528 B withnot_attempted=0.not_attemptedon the non-forcing workloads is correct behaviour, not a regression: they drive collections purely through explicitgc()on a ~6.4 KB heap (collection_kind=full,progress_kind=explicit_fullon every cycle), andgc()only evacuates first when the knob is set (GC testing: PERRY_GC_FORCE_EVACUATE is inert for gc()-driven tests (full mark-sweep + forced conservative scan) — stress claims may be unsupported #6946).What the fix needs to decide (maintainer call)
not_attempted/ineligible_cyclesbe tolerated while still requiringproductive > 0and still failing on real fallback reasons (conservative_stack,pinned_young_*,barriers_inactive)? That looks right to me, but it changes what the gate promises.PERRY_GC_FORCE_EVACUATE=1so they genuinely exercise copying, rather than being exempted?async_promise_closures: malloc_registry_rebuilds=1, want 0is a separate assertion that also fails and needs its own answer.I am deliberately not guessing at these on a release pin.