memory.rs has an allocator-reclamation path that Linux never gets:
#[cfg(target_os = "macos")]
pub fn relieve_allocator_pressure() -> AllocatorPressureRelief { … malloc_zone_pressure_relief … } // :616
#[cfg(target_os = "macos")]
pub fn spawn_allocator_slack_relief_if_due(…) -> bool { … } // :584
There is no Linux counterpart — malloc_trim, mallopt, and M_ARENA_MAX have zero occurrences in the crate.
The doc comment on spawn_allocator_slack_relief_if_due describes the exact failure this causes:
one long-lived chatty session used to block reclamation for the process lifetime (observed: 5.1 GB RSS over ~600 MB of live data)
That diagnosis was correct and the macOS fix works. Linux just never got the symmetric one, because until the subc daemon nothing ran long enough to need it — standalone spawned a worker per session and exited.
Observed
An 18-hour aft --subc instance on Linux (glibc, 16 cores, 7 project roots):
VmSwap 15.3 GB ← 2.5× peak RSS
VmHWM 6.0 GB
VmRSS 2.3 GB
VmSwap far exceeding VmHWM is the allocate-touch-once-never-again signature: pages get touched once, land in a per-thread arena that is never reused, and get swapped out permanently. Box swap reached 99.8%; killing the process returned ~18 GB.
Region-level detail on that instance: 55 rw-p anon regions of 32-132 MB, 2348 MB of them swapped (95% of process swap), all but one ~100% cold. Anonymous was 97.6% of RSS and the mapping count was low and static (38 map_files), which rules out the pread disk index and mmap growth — this is allocator retention, not a mapping leak.
Confirmation
Running the same binary with MALLOC_ARENA_MAX=2, at equal 9-minute uptime:
uncapped : ~50 cold regions, 2348 MB touched-then-swapped
capped : 27 regions, 1891 MB RSS, 0 MB swapped
of which 1698 MB is one fully-resident heap (the real working set)
Caveat stated honestly: the capped arm has more free RAM available than the baseline did, so the swap difference alone is confounded. The pressure-independent evidence is the touched footprint and the residency breakdown, which agree. A longer arm is still running.
Note when reading region counts: glibc reserves 64 MB of address space per arena eagerly and commits lazily, so 18 arena-sized regions can appear under a cap of 2 with 17 of them at ~0% resident. Counting map entries measures reservations; only RSS measures what can swap. (I misread this myself first time.)
Suggested fix
malloc_trim(0) on the same idle-sweep gate the macOS path already uses — spawn_allocator_slack_relief_if_due's trigger conditions look directly reusable. That keeps the "caller owns the gate because relief adds latency" contract intact.
MALLOC_ARENA_MAX as a spawn-env default is the cheaper mitigation, but it trades swap for malloc lock contention on a process that runs ~195 threads, and aft has paths asserting absolute wall-clock bounds (the storm rigs). I would not set it low without a latency guard.
Related but separate: the thread count itself is inflated by per-root inspect pools — filed separately, since the fixes are independent.
memory.rshas an allocator-reclamation path that Linux never gets:There is no Linux counterpart —
malloc_trim,mallopt, andM_ARENA_MAXhave zero occurrences in the crate.The doc comment on
spawn_allocator_slack_relief_if_duedescribes the exact failure this causes:That diagnosis was correct and the macOS fix works. Linux just never got the symmetric one, because until the subc daemon nothing ran long enough to need it — standalone spawned a worker per session and exited.
Observed
An 18-hour
aft --subcinstance on Linux (glibc, 16 cores, 7 project roots):VmSwap far exceeding VmHWM is the allocate-touch-once-never-again signature: pages get touched once, land in a per-thread arena that is never reused, and get swapped out permanently. Box swap reached 99.8%; killing the process returned ~18 GB.
Region-level detail on that instance: 55
rw-panon regions of 32-132 MB, 2348 MB of them swapped (95% of process swap), all but one ~100% cold. Anonymous was 97.6% of RSS and the mapping count was low and static (38map_files), which rules out the pread disk index and mmap growth — this is allocator retention, not a mapping leak.Confirmation
Running the same binary with
MALLOC_ARENA_MAX=2, at equal 9-minute uptime:Caveat stated honestly: the capped arm has more free RAM available than the baseline did, so the swap difference alone is confounded. The pressure-independent evidence is the touched footprint and the residency breakdown, which agree. A longer arm is still running.
Note when reading region counts: glibc reserves 64 MB of address space per arena eagerly and commits lazily, so 18 arena-sized regions can appear under a cap of 2 with 17 of them at ~0% resident. Counting map entries measures reservations; only RSS measures what can swap. (I misread this myself first time.)
Suggested fix
malloc_trim(0)on the same idle-sweep gate the macOS path already uses —spawn_allocator_slack_relief_if_due's trigger conditions look directly reusable. That keeps the "caller owns the gate because relief adds latency" contract intact.MALLOC_ARENA_MAXas a spawn-env default is the cheaper mitigation, but it trades swap for malloc lock contention on a process that runs ~195 threads, and aft has paths asserting absolute wall-clock bounds (the storm rigs). I would not set it low without a latency guard.Related but separate: the thread count itself is inflated by per-root inspect pools — filed separately, since the fixes are independent.