macOS: freed 64 MB C++ buffers stay resident; a std::vector mmap allocator fixes it
In a Node native addon on macOS, repeatedly decoding a 64 MB chunk with std::vector scratch buffers (resize, use, free) left ~280 MB resident after the reads, even with malloc_zone_pressure_relief(nullptr, 0) called after the frees. The same code on glibc grew only ~8 MB, because glibc serves big blocks with mmap and munmaps them on free. Fix that works on both: give large buffers an allocator that calls mmap(MAP_PRIVATE|MAP_ANON) for allocations over ~1 MB and munmap in deallocate (vector passes the same n to both), with operator new/delete below that. Add a no-arg construct() that default-initializes, so resize() doesn't zero-fill pages you are about to overwrite. Measured: RSS growth over 6 large reads went from +282 MB to +11 MB. To reach vectors inside a third-party struct, add a small hook like #ifndef X_ALLOC #define X_ALLOC std::allocator and declare the members as std::vector<T, X_ALLOC<T>>, then template functions that take a scratch std::vector<uint8_t>& on the buffer type. Test the effect in a fresh child process (process.memoryUsage.rss before and after N reads), not in the test runner itself.
Follow-up: a ~1 MB threshold isn't enough when the retained memory is many sub-MB blocks (a decoded-block LRU). After freeing ~270 MB of cached blocks of a few hundred KB each, macOS phys_footprint (footprint <pid>) stayed ~210 MB: 167 MB dirty MALLOC_SMALL, even after malloc_zone_pressure_relief. Lowering the mmap threshold to 64 KiB on macOS only brought it to ~110 MB with the same CPU; 16 KiB slowed the work (more mmap/munmap). Keep ~1 MiB on glibc, where malloc_trim(0) already returns the pages. Also: on macOS, clean pages of a read-only file mmap don't count in phys_footprint, so madvise(MADV_DONTNEED) on the mapping doesn't lower it there (it does lower Linux PSS). Measure footprint, not ps RSS: RSS dropped to ~55 MB while footprint stayed high, because compressed dirty pages leave RSS but stay in footprint.