Skip to content

gc(macos): back Apple GC blocks with tagged 1 MiB regions - #12391

Merged
proggeramlug merged 3 commits into
mainfrom
perf/macos-gc-regions
Oct 11, 2026
Merged

proggeramlug merged 3 commits into
mainfrom
perf/macos-gc-regions

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Summary

On Apple platforms, GC arena blocks now come from their own anonymous mappings instead of mimalloc. They are 1 MiB, aligned to 1 MiB, and tagged VM_MEMORY_APPLICATION_SPECIFIC_2 (241) so vmmap/footprint tools show them separately from mimalloc (240). Linux (R2 reservation) and the other targets are unchanged. The regions stay in the existing non-Linux region registry, so classification and pooling are unchanged.

  • Release and reactivation. Idle pool and Eden pages are released with MADV_FREE_REUSABLE, which drops them from the physical footprint immediately. They are reactivated with MADV_FREE_REUSE before reuse:
    • on every pool handoff;
    • in Arena::set_current for idle Eden blocks. Idle discard never touches the current block, and every block becomes current before its bytes are written, so the bump path carries no check.
  • Block size. Blocks stay 1 MiB. A 2 MiB geometry was measured on Apple: it added 4–6 MiB of arena capacity and raised peak footprint, with no huge-page benefit to pay for it.
  • mimalloc purge drain. Block allocation no longer passes through mimalloc, and mimalloc runs its deferred purges on its own allocation traffic. Freed collector and runtime memory then stayed dirty until the next full collection. An Effect workload peaked 5 MiB higher, with identical GC counts and arena sizes; MIMALLOC_PURGE_DELAY=0 removed the gap. The fix: when the heap maps a fresh region, run mi_collect(true) at most once per mimalloc's default purge delay (10 ms) per thread. PERRY_GC_MALLOC_PURGE=0 disables it together with the existing cycle purge, so this adds no new knob.
  • Test-only fix. The first commit restores the build of a macOS-only fixture in value/dynamic_object.rs. BufferHeader's fields became private, and the fixture no longer compiled, so perry-runtime's test crate did not build on macOS at all.

This follows #12247, the codegen floor fix for *-apple-macosx* triples. Without that fix, every inline guard missed for objects below 2 TiB, where these regions land.

Measurements

The machine is an M1 Max on macOS 26.5, which is shared, so primary metrics are retired instructions and lifetime peak physical footprint from proc_pid_rusage. Runs are n=5, interleaved, and each timed run is preceded by an untimed run of the same binary. All builds are default auto-optimized, and every run matched Node 26.5.1 output.

Rebased branch vs main at a760b5497a; main-r is the same binary repeated, as the noise floor:

Program Instructions Peak footprint main-r footprint
hello −0.18% +0.00 MiB +0.00
TypeScript 5.8 compile +0.06% +0.11 MiB (within span) +0.28
Zod ×5000 +0.01% −0.11 MiB −0.03
qs parse +0.01% −0.34 MiB +0.02
qs stringify −0.02% −0.11 MiB +0.02
commander −0.01% −0.19 MiB +0.00
Fastify inject −0.07% −6.56 MiB (48.7 → 42.1) +0.03
buffer_heavy +0.07% −0.44 MiB +0.00
worker_heavy +0.36% +1.9 MiB (noise; thread spans 12–18 MiB) +4.9
THP probe −0.34% −0.05 MiB +0.00

Bold marks a difference beyond the larger same-binary span.

Effect. The Effect workload does not link on current main in either arm: duplicate _perry_birth_class/_perry_birth_empty_array across two modules, unrelated to this change. It was measured on the previous base (7a5584c12d):

  • branch 106.17 MiB, spread 0.09;
  • main 107.77 MiB, bimodal with spread 2.4;
  • retired instructions −0.01%.

Before the purge drain, that comparison was +5.3 MiB.

RSS, a secondary metric, can be higher (buffer +3.7 MiB, Fastify +0.9 MiB) while footprint is lower. Reusable pages stay resident until the kernel needs them, but they are not charged to the process.

Tests

cargo test --release -p perry-runtime -- --test-threads=1 on macOS:

  • branch: 5,677 passed, 11 failed;
  • main with the fixture fix (needed for the test crate to build): 5,668 passed, 10 failed.

The ten failures are the same set on both. The eleventh on the branch is gc::tests::net_transport::n9_a_stale_resolve_leaves_the_new_plan_and_backlog_alone (WouldBlock on a test socket read). It is flaky on main too: it failed 2 of 3 isolated runs on main and 3 of 3 on the branch.

New tests:

  • arena::region::apple_tests:
    • 1 MiB geometry;
    • regions are aligned, tagged 241 and registered/deregistered;
    • the release/reuse round trip changes the kernel's reusable-page state;
    • invalid extents are refused.
  • arena::block::reuse_window_tests:
    • whole-region pool charge;
    • pool handoff reactivates pages;
    • an already-discarded block reactivates inside a new pool window;
    • an idle Eden block reactivates when it becomes current (bump path issues no syscall; forward scan and set_current both reactivate).
  • gc::cycle_malloc_trim::region_growth_purge_tests: the purge is gated to once per purge delay, and fresh region maps reach the gate.

The pool-pressure fixture now seeds the pool with region backing instead of std::alloc.

No version bump.

Ralph Küpper added 3 commits October 11, 2026 08:11
BufferHeader's fields became private to the buffer store, so the
macOS-only fixture in length_handle_band_tests no longer compiled and
the runtime test crate failed to build on macOS. Write the header's
length, capacity and link words directly.
On Apple platforms arena blocks now come from their own anonymous
mappings instead of mimalloc: aligned to the 1 MiB block size and tagged
VM_MEMORY_APPLICATION_SPECIFIC_2 (241) so they show up apart from
mimalloc (240) in vmmap and footprint tools. They stay in the existing
region registry, so classification and pooling are unchanged.

Idle pool and Eden pages are released with MADV_FREE_REUSABLE, which
drops them from the physical footprint at once, and are reactivated with
MADV_FREE_REUSE before reuse: on every pool handoff, and in
Arena::set_current for Eden blocks. Every block becomes current before
its bytes are written and idle discard never touches the current block,
so the bump path carries no check.

Blocks stay 1 MiB. 2 MiB blocks added 4 to 6 MiB of arena capacity on
Apple with no huge-page benefit to pay for it. Linux and other targets
are unchanged.
With Apple GC blocks in their own regions, block allocation no longer
passes through mimalloc, and mimalloc runs its deferred purges on its own
allocation traffic. Freed collector and runtime memory then stayed dirty
until the next full collection: an Effect workload peaked 5 MiB higher
than with mimalloc-backed blocks, which had been reusing that memory. GC
counts and arena sizes were identical; MIMALLOC_PURGE_DELAY=0 removed the
gap, and a forced mi_collect on a time gate removed it without the cost
of purging on every free.

When the GC heap maps a fresh region, run mi_collect(true) at most once
per mimalloc's default purge delay (10 ms) on that thread.
PERRY_GC_MALLOC_PURGE=0 disables it together with the existing cycle
purge.
@proggeramlug
proggeramlug merged commit 3c8a22a into main Oct 11, 2026
6 checks passed
proggeramlug pushed a commit that referenced this pull request Oct 11, 2026
@coderabbitai

coderabbitai Bot commented Oct 11, 2026

Copy link
Copy Markdown

Review in Change Stack →

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 3b132829-4ed8-4c64-87cd-667c0fb972fc

📥 Commits

Reviewing files that changed from the base of the PR and between 15b6149 and c8f6e2e.


📒 Files selected for processing (10)
  • crates/perry-runtime/src/arena/block.rs
  • crates/perry-runtime/src/arena/block/reuse_window_tests.rs
  • crates/perry-runtime/src/arena/mod.rs
  • crates/perry-runtime/src/arena/region.rs
  • crates/perry-runtime/src/arena/region/apple.rs
  • crates/perry-runtime/src/arena/region/apple_tests.rs
  • crates/perry-runtime/src/gc/cycle_malloc_trim.rs
  • crates/perry-runtime/src/gc/mod.rs
  • crates/perry-runtime/src/gc/tests/block_pool_pressure.rs
  • crates/perry-runtime/src/value/dynamic_object.rs

 _______________________________________
< For a good time, call 1-800-COD-RABT. >
 ---------------------------------------
  \
   \   \
        \ /\
        ( )
      .( o ).
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR

🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

proggeramlug pushed a commit that referenced this pull request Oct 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant