Skip to content

GC: copying minor reports not_attempted on every cycle in the memory-stability gates; old_page_forced_defrag moved > selected #9659

Open

Description

@proggeramlug

Summary

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:

none=0  copy_only_roots=0  barriers_inactive=0  conservative_stack=0
malloc_registry_unavailable=0  pinned_young_root=0
pinned_young_dirty_slot=0  pinned_young_transitive=0
not_attempted=8

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.

large_object_barriers additionally reports missing large-object exclusion telemetry.

2. old_page_forced_defrag: moved more than was selected

old_page_forced_defrag:5: evacuation.old_page_moved_bytes(80)
                        > evacuation_policy.old_page_selected_live_bytes(32)

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.

Activity

  1. proggeramlug commented on Sep 3, 2026

    @proggeramlug
    ContributorAuthor

    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:

    workload sets PERRY_GC_FORCE_EVACUATE=1 cycles not_attempted copied promoted
    async_promise_closures yes 10 5 486,088 B 158,648 B
    default_copying no 8 8 0 0
    string_heavy no 4 4 0 0
    closure_heavy no 5 5 0 0
    large_object_barriers no 4 4 0 0
    raw_numeric_layouts no 3 3 0 0

    The five failing workloads are exactly the five that do not set the knob, in workload_specs in run_target_collector_architecture_gates.

    The copying minor is healthy. async_promise_closures copies 486 KB and promotes 159 KB, and pointer_free_numeric copies 528 B with not_attempted=0.

    Why the other five report not_attempted on every cycle

    Their raw traces show every collection is an explicit one:

    "collection_kind":"full"
    "progress_kind":"explicit_full"
    "reason":"explicit_or_emergency"
    "arena":{"in_use_bytes":6408}

    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

    1. 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.
    2. 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.

  2. proggeramlug commented on Sep 3, 2026

    @proggeramlug
    ContributorAuthor

    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

    What the fix needs to decide (maintainer call)

    1. 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.
    2. Should the five non-forcing workloads get PERRY_GC_FORCE_EVACUATE=1 so they genuinely exercise copying, rather than being exempted?
    3. 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.

  3. added
    performanceRuntime, compile-time, build-size, or memory performance
    on Oct 26, 2026
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

    performanceRuntime, compile-time, build-size, or memory performance

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions