Skip to content

macOS tools attribute Perry's heap to IOAccelerator (GPU) — mimalloc VM tag 100 collision #6882

Description

@proggeramlug

Summary

On macOS, every memory-profiling tool (vmmap, footprint, Instruments' VM Tracker) attributes the bulk of a compiled Perry binary's memory to IOAccelerator — i.e. GPU driver memory. On an allocation-heavy benchmark this looks like a ~640 MB GPU leak in a headless console program:

REGION TYPE                        SIZE  RESIDENT   DIRTY
IOAccelerator                    644.4M    422.1M  422.1M        8
IOAccelerator (reserved)         384.0M        0K      0K        1

There is no GPU memory involved. This is the Perry JS heap: perry-runtime routes all allocation through mimalloc (#[global_allocator], issue #62, crates/perry-runtime/src/lib.rs), and mimalloc tags its mmap regions with VM tag 100 (mi_option_os_tag default). macOS tooling decodes tag 100 as IOAccelerator.

Proof

mi_option_os_tag is env-tunable on any existing binary. Re-running the same binary with the tag moved:

$ MIMALLOC_OS_TAG=240 ./bench & vmmap -summary $!
Memory Tag 240                   644.4M    422.1M  422.1M        8
Memory Tag 240 (reserved)        384.0M        0K      0K        1

Identical regions, identical sizes — only the label changes. IOAccelerator disappears entirely.

Repro

// churn.ts — allocation-churn benchmark
class Node2 { v: number; next: Node2 | null;
  constructor(v: number, next: Node2 | null) { this.v = v; this.next = next; } }
let sum = 0;
for (let round = 0; round < 200; round++) {
  let head: Node2 | null = null;
  for (let i = 0; i < 20000; i++) head = new Node2(i, head);
  let p = head; while (p !== null) { sum += p.v; p = p.next; }
}
console.log(sum);
$ perry compile churn.ts -o churn && ./churn & vmmap -summary $!

Environment: macOS 26.5 (25F71), Apple Silicon, Perry v0.5.1264.

Why it matters

  • Anyone profiling Perry memory on macOS starts by chasing a GPU ghost (this cost a full investigation detour before MIMALLOC_OS_TAG proved the collision).
  • It also hides the JS heap from attribution: the arena, GC metadata, and Rust-side structures all pool into one mislabeled category, so you can't tell heap from allocator overhead in Instruments.

Suggested fixes

  1. Set mi_option_os_tag to a distinctive unused tag at runtime init (e.g. 240) so tooling shows a neutral Memory Tag 240 instead of IOAccelerator. One line via mi_option_set at startup (or ship MIMALLOC_OS_TAG in docs as a stopgap).
  2. Document the collision in the profiling/memory docs (docs/memory.md / CONTRIBUTING): "Perry heap shows as IOAccelerator in vmmap/Instruments; it is mimalloc VM tag 100."
  3. Longer term: allocate arena blocks via direct mmap with Perry's own VM_MAKE_TAG, separating "JS heap" from "Rust allocator" in every tool. (This is also the fix direction for a related macOS RSS-retention issue — mimalloc purges via MADV_FREE, which stays in RSS/phys_footprint, so PERRY_GC_HEAP_LIMIT cannot bound what the allocator layer retains; details in a follow-up issue with measurements.)

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