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
[perf] Minor GC does O(old-gen) work: full-region sweep walk, whole-heap RS rebuild, per-cycle page-meta iterations #6181
Minor GC cost includes several O(old-gen) components even when nothing old is touched:
Sweep visits/rewrites every old-gen header on every minor (gc/oldgen.rs:1181-1225 cursor over all regions; flags/accounting rewrites gc/oldgen.rs:1291-1311,1338,1357; minors set reclaim_dead_old_blocks=false but still walk, gc/cycle.rs:1390-1416).
The remembered-set finalize rebuild walks the ENTIRE heap (all arena objects + full malloc registry) every cycle (gc/cycle.rs:1244-1264, gc/verify.rs:279-349), and with require_marked=false for minors it re-dirties pages from unreachable-but-unswept old parents.
Old-page bookkeeping iterates every old 4 KB page meta up to 3× per minor (arena/page_meta.rs:555-571, gc/oldgen.rs:181-184,1199), plus the promotion-handoff pre-walk over all arena objects once survivor in-use ≥ 24 MB (gc/policy.rs:864-902).
Minor pause grows monotonically with process lifetime on servers.
Proposed fix
Restrict the minor sweep cursor to nursery+survivor regions; maintain old-gen accounting incrementally from promotion/reclaim events; per-block live counts so fully-dead block reset needs no per-object walk.
Status: the whole-heap old→young RS rebuild for minors was eliminated in #6213 (minors jump AtomicFinalize→Sweep; barriers + evacuation-sticky + #5029 snapshot repair carry coverage). Remaining from this issue: the minor sweep's full-region walk and per-cycle census cost — the census half just landed for budgeted cycles in #6239 (no exact set built at all); the sweep-walk restriction to young regions remains open and is queued in the #6180 flip campaign.
Per-sub-cost status on current main (post the July 9-10 GC campaign):
(b) whole-heap remembered-set rebuild on every minor: FIXED by perf(gc): minor collections don't walk/rebuild over old-gen (#6181) #6213 (gc/cycle.rs:1405-1429, // Fix 2 (#6181)) — minors early-exit past RememberedSetRebuild; regression tests test_minor_skips_whole_heap_old_to_young_rebuild + test_minor_preserves_old_to_young_edge_across_minors cover it. Don't re-touch.
(a) full old-gen sweep walk on every minor: still present, deliberately deferred.ArenaSweepObjectsState::new always builds a BlockIndex cursor over all 5 regions incl. Old (gc/cycle.rs:1337, arena/walk.rs). PR perf(gc): minor collections don't walk/rebuild over old-gen (#6181) #6213 scoped this out with a real correctness blocker: a minor's mark phase marks root/young-reachable old objects, and the minor sweep's full old-gen walk is the only place those MARKED bits get cleared before the next full GC (which assumes clean marks at entry). Skipping it naively → stale marks → next full GC skips tracing → frees live old children → UAF. Needs a design choice (minor-scoped mark suppression vs. a full-GC mark-clear pass + delta accounting) — not a quick patch.
(c) per-cycle page-meta iteration: partly present.old_pages_begin_gc_cycle/old_pages_reset_sweep_accounting/old_page_summary walk all page metas every cycle (the reset-accounting one is coupled to (a)). One clean, decoupled win exists:copied_minor_promotable_active_survivor_bytes (gc/policy.rs:765-786) calls the unfilteredarena_walk_objects_with_block_index just to filter by active block range — a filtered helper arena_walk_objects_filtered(block_filter, cb) (arena/walk.rs:686-775) already exists and skips out-of-range blocks in O(n_blocks). It's a pure measurement pass, no mark-bit coupling → safe S swap. (This was not addressed by perf(gc): census-free budgeted cycles + RSS floor (#6179, #6180 Stage 2) #6239, whose "census" is the mark-phase ValidPointerSet, a different structure — verified by diff.)
Recommendation: land the policy.rs:765 filtered-walk swap as an isolated S/low-risk PR (essentially conflict-free with the #6180/#6243 churn); keep (a) + the coupled reset-accounting as a separate design-gated follow-up once the mark-clear approach is chosen. The fix/6083-nursery-scoped-minor branch (#6129) is closed/superseded by #6239 and not reusable here.
Landed:#6277 — the promotable-survivor census (gc/policy.rs) now uses the block-filtered arena walk instead of the unfiltered one, so sub-cost (c)'s one cleanly-decoupled piece is done: the pure measurement pass drops from O(all arena objects) to O(active-survivor objects) + O(blocks). Regression test asserts the filtered census equals the old unfiltered-walk value byte-for-byte (with a guard that the equality isn't vacuously 0 == 0).
(a) full old-gen sweep walk on every minor — still open, still design-gated. A minor's mark phase marks root/young-reachable old objects, and the minor sweep's full old-gen walk is the only place those MARKED bits get cleared before the next full GC (which assumes clean marks at entry). Skipping it naively → stale marks → next full GC skips tracing → frees live old children (UAF). Needs the design choice (minor-scoped mark suppression vs. a full-GC mark-clear pass + delta accounting) before any patch.
Leaving open for (a) + the coupled reset-accounting.
Closing — two of the three sub-costs are fixed, and the third is refiled so it is visible as its own work.
Fixed: (b) whole-heap remembered-set rebuild and (c) per-cycle page-meta iterations, via #6213 / #6277 / #6568.
Refiled:#7716 — sub-cost (a), the minor's full-region sweep walk over the old generation (gc/oldgen.rs::sweep, still on the minor path at HEAD), which makes minor cost scale with old-gen size rather than nursery survivors.
That issue carries the two traps that make it less mechanical than it looks: the malloc sweep is a separate, trigger-gated path that must not be conflated with it, and #7682/#7690's precedent that removing work from the minor path can livelock on its own, because the nursery cap was keyed on bytes the changed collector no longer frees.
Activity
perf(gc): minor collections skip the whole-heap old→young RS rebuild (#…
proggeramlug commentedon Jul 10, 2026
Status: the whole-heap old→young RS rebuild for minors was eliminated in #6213 (minors jump AtomicFinalize→Sweep; barriers + evacuation-sticky + #5029 snapshot repair carry coverage). Remaining from this issue: the minor sweep's full-region walk and per-cycle census cost — the census half just landed for budgeted cycles in #6239 (no exact set built at all); the sweep-walk restriction to young regions remains open and is queued in the #6180 flip campaign.
proggeramlug commentedon Jul 10, 2026
Per-sub-cost status on current main (post the July 9-10 GC campaign):
gc/cycle.rs:1405-1429,// Fix 2 (#6181)) — minors early-exit pastRememberedSetRebuild; regression teststest_minor_skips_whole_heap_old_to_young_rebuild+test_minor_preserves_old_to_young_edge_across_minorscover it. Don't re-touch.ArenaSweepObjectsState::newalways builds aBlockIndexcursor over all 5 regions incl. Old (gc/cycle.rs:1337,arena/walk.rs). PR perf(gc): minor collections don't walk/rebuild over old-gen (#6181) #6213 scoped this out with a real correctness blocker: a minor's mark phase marks root/young-reachable old objects, and the minor sweep's full old-gen walk is the only place those MARKED bits get cleared before the next full GC (which assumes clean marks at entry). Skipping it naively → stale marks → next full GC skips tracing → frees live old children → UAF. Needs a design choice (minor-scoped mark suppression vs. a full-GC mark-clear pass + delta accounting) — not a quick patch.old_pages_begin_gc_cycle/old_pages_reset_sweep_accounting/old_page_summarywalk all page metas every cycle (the reset-accounting one is coupled to (a)). One clean, decoupled win exists:copied_minor_promotable_active_survivor_bytes(gc/policy.rs:765-786) calls the unfilteredarena_walk_objects_with_block_indexjust to filter by active block range — a filtered helperarena_walk_objects_filtered(block_filter, cb)(arena/walk.rs:686-775) already exists and skips out-of-range blocks in O(n_blocks). It's a pure measurement pass, no mark-bit coupling → safe S swap. (This was not addressed by perf(gc): census-free budgeted cycles + RSS floor (#6179, #6180 Stage 2) #6239, whose "census" is the mark-phaseValidPointerSet, a different structure — verified by diff.)Recommendation: land the
policy.rs:765filtered-walk swap as an isolated S/low-risk PR (essentially conflict-free with the #6180/#6243 churn); keep (a) + the coupled reset-accounting as a separate design-gated follow-up once the mark-clear approach is chosen. Thefix/6083-nursery-scoped-minorbranch (#6129) is closed/superseded by #6239 and not reusable here.perf(gc): use the filtered arena walk for the minor promotable-surviv…
proggeramlug commentedon Jul 11, 2026
Landed: #6277 — the promotable-survivor census (
gc/policy.rs) now uses the block-filtered arena walk instead of the unfiltered one, so sub-cost (c)'s one cleanly-decoupled piece is done: the pure measurement pass drops from O(all arena objects) to O(active-survivor objects) + O(blocks). Regression test asserts the filtered census equals the old unfiltered-walk value byte-for-byte (with a guard that the equality isn't vacuously0 == 0).Status of this issue's three sub-costs:
policy.rs:765unfiltered-walk half is now fixed (perf(gc): use the filtered arena walk for the minor promotable-survivor census (#6181) #6277). Theold_pages_*reset-accounting walks remain, coupled to (a).Leaving open for (a) + the coupled reset-accounting.
perf(gc): lazy epoch reset for old-page dirty-slot accounting (#6181) (…
proggeramlug commentedon Aug 9, 2026
Closing — two of the three sub-costs are fixed, and the third is refiled so it is visible as its own work.
Fixed: (b) whole-heap remembered-set rebuild and (c) per-cycle page-meta iterations, via #6213 / #6277 / #6568.
Refiled: #7716 — sub-cost (a), the minor's full-region sweep walk over the old generation (
gc/oldgen.rs::sweep, still on the minor path at HEAD), which makes minor cost scale with old-gen size rather than nursery survivors.That issue carries the two traps that make it less mechanical than it looks: the malloc sweep is a separate, trigger-gated path that must not be conflated with it, and #7682/#7690's precedent that removing work from the minor path can livelock on its own, because the nursery cap was keyed on bytes the changed collector no longer frees.