Summary
#6182 made copied-minor weak processing proportional to registered weak holders, but the full/fallback path was deliberately left unchanged. As a result, one live WeakRef/WeakMap/WeakSet/FinalizationRegistry holder makes every full/fallback weak-processing phase walk every live arena object.
The code says this directly:
This is also performed as one unsliced atomic-finalize step:
Impact
- Full GC latency scales with unrelated heap size when a program uses even one weak holder.
js_gc_step_us / mutator-assist budgeting cannot bound the pause: a step with one unit can run the full walk.
- The recent full-mark-sweep pacing work reduces how often this happens, but does not bound the individual pause when it does happen.
Proposed direction
Use WEAK_HOLDERS for full/fallback processing as well. The full cycle already has ValidPointerSet / FullCycleLiveness, and full/fallback pruning already removes dead holders via prune_dead_weak_holders. Make iteration state part of AtomicFinalizeState so the holder registry is processed in budgeted slices.
Acceptance criteria
- Full/fallback weak processing cost is O(registered holders), not O(all live arena objects).
- Dead/stale registry entries are safely classified and pruned before dereference; full-cycle evacuation continues to rekey addresses.
WeakProcessing maintains a cursor/snapshot and honors the supplied work budget across calls.
- Add a scaling regression: a fixed one-holder workload with 10x unrelated live objects must not produce ~10x weak-processing work/pause.
- Add correctness coverage for WeakRef, FinalizationRegistry, WeakMap, and WeakSet on full and fallback cycles, including dead holders and moved holders.
Successor to the copied-minor-only optimization in #6182.
Summary
#6182 made copied-minor weak processing proportional to registered weak holders, but the full/fallback path was deliberately left unchanged. As a result, one live WeakRef/WeakMap/WeakSet/FinalizationRegistry holder makes every full/fallback weak-processing phase walk every live arena object.
The code says this directly:
process_weak_targets_after_markwalks EVERY live objectThis is also performed as one unsliced atomic-finalize step:
WeakProcessingcalls the entire pass after checking onlybudget != 0Impact
js_gc_step_us/ mutator-assist budgeting cannot bound the pause: a step with one unit can run the full walk.Proposed direction
Use
WEAK_HOLDERSfor full/fallback processing as well. The full cycle already hasValidPointerSet/FullCycleLiveness, and full/fallback pruning already removes dead holders viaprune_dead_weak_holders. Make iteration state part ofAtomicFinalizeStateso the holder registry is processed in budgeted slices.Acceptance criteria
WeakProcessingmaintains a cursor/snapshot and honors the supplied work budget across calls.Successor to the copied-minor-only optimization in #6182.