Skip to content

GC: arena-bytes trigger re-arm depends on which collector ran (tsc peak RSS ±28 MB knife-edge) #11736

Description

@proggeramlug

Summary

The arena-bytes GC trigger re-arms at a point that depends on which collector discharged the pressure, not on the heap. The same heap re-arms about 50 MB apart depending on whether a budgeted non-moving minor or a precise-safepoint copying minor ran. On TypeScript (tscwork, N3) that turns a one-block difference in object placement into a +28 MB (+7%) peak RSS. The heap's live data is unchanged.

Evidence

Quiet dedicated host (EPYC 9275F, performance governor, boost off), release builds of main 9e29f59d43 and of #11713 (21ccb89264). #11713 changes no line in gc/policy.rs or arena/ (git diff 9e29f59d43 21ccb89264 -- crates/perry-runtime/src/gc/policy.rs crates/perry-runtime/src/arena is empty).

  • Peak RSS: tsc median 394.6 MB on main vs 422.4 MB on Remove inherited-read side table with shape-guarded read and accessor sites #11713 (10 paired reps).
  • Live data is the same on both. At cycle 94: 28,784,416 B / 242,927 objects vs 28,784,688 B / 242,929. It matches at every full collection.
  • The memory is committed, not merely unpurged. MIMALLOC_SHOW_STATS=1 reports a committed peak of 317 vs 350 MiB, and MIMALLOC_PURGE_DELAY=0 still gives 384 vs 411 MB.
  • Main takes the high mode too when perturbed: one main run with GC tracing on peaked at 417 MB.
  • It disappears when both take the same path: with PERRY_GC_INCREMENTAL=0, peaks are 353–360 MB on both.

The two modes split at the first trigger after full collection 93 (cycle 94, about 5.8 s), and the split is consistent across 18 PERRY_GC_DIAG traces:

arena_total − next_base collector empty nursery blocks arena trigger re-armed at
low mode (main, 9/9) 0 KiB budgeted non-moving minor (site=budgeted_start kind=due) 63 released ([gc-general-reclaim] released=63) 145–147 MB
high mode (#11713, 9/9) +1024 KiB copying minor at safepoint (site=safepoint kind=ArenaBytes) 62 stay reserved ~196 MB

In the high mode, the second allocation burst runs 3 consecutive arena minors with reservation going 159 → 183 → 191 → 208 → 222 MB before the old-gen full fires. That produces a peak of about 0.2 s.

Cause

gc_rebaseline_arena_trigger_after_collection (crates/perry-runtime/src/gc/policy.rs, around line 2785 at 9e29) re-arms with next_trigger = max(min(new_total + step, ceiling), new_total + floor), where new_total = arena_total_bytes().

  • What new_total counts: the empty nursery from-space blocks the copying minor keeps. It does not count the blocks the budgeted minor's general reclaim releases (arena/reset.rs).
  • Which collector catches the trigger:
    • The budgeted due-check is total >= next_arena_trigger_base() (around line 3752).
    • The safepoint arena path arms only when a block acquisition crosses the base.
    • So total == base is caught by the budgeted host poll, and one more block is caught by the copying minor.

Proposed fix

Make the re-arm independent of the collector. Derive it from committed bytes excluding empty, reusable nursery from-space, plus the nursery band and the step. Alternatively, have the copying minor drain empty from-space blocks above the nursery band, as the budgeted general reclaim already does.

Acceptance: the four-arm GC workload matrix (39 cells) plus tsc and Zod RSS, CPU and full counts. After the fix, main and #11713 should match in tsc RSS and fulls.

Activity

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions