Skip to content

Allocator pressure relief is macOS-only; long-lived Linux daemon accumulates 15GB of swap #201

Description

@iceteaSA

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.

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