Follow-ups from the audit of #7733 (merged as v0.5.1427). None blocked the merge; all four are cheap and concrete.
1. OBJECT_PROTOTYPES_NONEMPTY is a one-way latch — the #7510 shape, with a third victim
object_static_prototype_owner_moved's new fast path is gated on OBJECT_PROTOTYPES_NONEMPTY.load(Acquire) (object/prototype_chain.rs:~340). Nothing ever resets that latch to false — including prune_dead_object_prototype_owners when its retain() empties the map back out (prototype_chain.rs:328-335).
So a single Object.setPrototypeOf against a non-meta-capable owner anywhere in a process's lifetime — even one that later dies and gets pruned — permanently disables the fast path for the rest of that process. A long-running server that does one incidental re-prototyping early (of a RegExp, say) silently forfeits the 2.5 M-lock / 2.5 M-hash win, with no signal.
This is pre-existing and shared by all three readers (object_static_prototype:255, prune_dead_object_prototype_owners:329, and now the move hook), not introduced by #7733. But it is exactly #7510's finding — "one immortal side-table entry nullified every is_empty() fast path" — recurring. Fix: reset the latch when prune leaves the map empty, or replace it with a real emptiness check.
2. No positive-direction test for the recording
declining_to_escalate_records_no_pre_full_reading proves only the negative: a declined escalation leaves pre_in_use == 0. Nothing asserts that a true verdict from the real arena_growth_full_escalation_due() — not the test_note_full_cycle_reclaimed bypass — leaves a non-zero reading behind.
That positive direction is precisely the shape of the first-cut bug #7733's own changelog describes: recording wired at the wrong call sites, update_major_pacing_backoff silently no-op'ing, every test green. The gap exists because forcing due=true deterministically needs control over arena_in_use_bytes(), which isn't mockable today — real, but addressable.
3. The grow-then-churn transition is named but unmeasured
Array-growth forwarding stubs are reclaimable only by a full mark-sweep (arena_growth_full_escalation_due's own doc), and old_reclaim_pressure_due tracks old-gen occupancy, not stub bytes — so it does not substitute for that path.
The scenario: a warm/cache-build phase with a growing all-live structure drives the shift to cap, then the workload transitions to steady array-churn that needs a full to reclaim stubs but never regrows arena in-use past the old 2× threshold. That reclaim is delayed until 8×, i.e. up to 4× more transient RSS for that shape, self-healing after one full once it fires.
Bounded, not unbounded — but neither retain.ts (pure growth) nor tree.ts (always high-yield) exercises it. Both are extremes; the transition between them is the untested middle. A gc_ratchet probe that grows without garbage long enough to hit the cap, then switches to churn, asserting a peak-RSS bound, would close it.
4. gc-ratchet and gc-stress are not required contexts
Confirmed against branch protection — required contexts are lint, cargo-test, parity, compile-smoke, api-docs-drift, security-audit, conformance-smoke-complete. Both gc-ratchet and gc-stress run and can report red without blocking a merge.
That matters for #7733 specifically: 12_large_live_set's heap_used_bytes/heap_total_bytes cells (direction: "increase") are the gate that would catch a footprint regression from the backoff, and #7733's central quantitative claim rests on them. Advisory-only coverage of the exact risk the change carries.
This is CLAUDE.md's own "four ways a gate can be unable to fail", #2 — and its gc-stress example is the precedent. Promotion is a maintainer action; per the same doc a new gate should be observed green on main once first, which recent runs now satisfy.
Follow-ups from the audit of #7733 (merged as v0.5.1427). None blocked the merge; all four are cheap and concrete.
1.
OBJECT_PROTOTYPES_NONEMPTYis a one-way latch — the #7510 shape, with a third victimobject_static_prototype_owner_moved's new fast path is gated onOBJECT_PROTOTYPES_NONEMPTY.load(Acquire)(object/prototype_chain.rs:~340). Nothing ever resets that latch tofalse— includingprune_dead_object_prototype_ownerswhen itsretain()empties the map back out (prototype_chain.rs:328-335).So a single
Object.setPrototypeOfagainst a non-meta-capable owner anywhere in a process's lifetime — even one that later dies and gets pruned — permanently disables the fast path for the rest of that process. A long-running server that does one incidental re-prototyping early (of aRegExp, say) silently forfeits the 2.5 M-lock / 2.5 M-hash win, with no signal.This is pre-existing and shared by all three readers (
object_static_prototype:255,prune_dead_object_prototype_owners:329, and now the move hook), not introduced by #7733. But it is exactly #7510's finding — "one immortal side-table entry nullified everyis_empty()fast path" — recurring. Fix: reset the latch when prune leaves the map empty, or replace it with a real emptiness check.2. No positive-direction test for the recording
declining_to_escalate_records_no_pre_full_readingproves only the negative: a declined escalation leavespre_in_use == 0. Nothing asserts that atrueverdict from the realarena_growth_full_escalation_due()— not thetest_note_full_cycle_reclaimedbypass — leaves a non-zero reading behind.That positive direction is precisely the shape of the first-cut bug #7733's own changelog describes: recording wired at the wrong call sites,
update_major_pacing_backoffsilently no-op'ing, every test green. The gap exists because forcingdue=truedeterministically needs control overarena_in_use_bytes(), which isn't mockable today — real, but addressable.3. The grow-then-churn transition is named but unmeasured
Array-growth forwarding stubs are reclaimable only by a full mark-sweep (
arena_growth_full_escalation_due's own doc), andold_reclaim_pressure_duetracks old-gen occupancy, not stub bytes — so it does not substitute for that path.The scenario: a warm/cache-build phase with a growing all-live structure drives the shift to cap, then the workload transitions to steady array-churn that needs a full to reclaim stubs but never regrows arena in-use past the old 2× threshold. That reclaim is delayed until 8×, i.e. up to 4× more transient RSS for that shape, self-healing after one full once it fires.
Bounded, not unbounded — but neither
retain.ts(pure growth) nortree.ts(always high-yield) exercises it. Both are extremes; the transition between them is the untested middle. Agc_ratchetprobe that grows without garbage long enough to hit the cap, then switches to churn, asserting a peak-RSS bound, would close it.4.
gc-ratchetandgc-stressare not required contextsConfirmed against branch protection — required contexts are
lint, cargo-test, parity, compile-smoke, api-docs-drift, security-audit, conformance-smoke-complete. Bothgc-ratchetandgc-stressrun and can report red without blocking a merge.That matters for #7733 specifically:
12_large_live_set'sheap_used_bytes/heap_total_bytescells (direction: "increase") are the gate that would catch a footprint regression from the backoff, and #7733's central quantitative claim rests on them. Advisory-only coverage of the exact risk the change carries.This is CLAUDE.md's own "four ways a gate can be unable to fail", #2 — and its
gc-stressexample is the precedent. Promotion is a maintainer action; per the same doc a new gate should be observed green onmainonce first, which recent runs now satisfy.