//! Give memory the system allocator keeps after frees back to the operating system. //! //! macOS's allocator keeps freed large blocks mapped as "reusable": they no longer count in the //! process's memory footprint but stay in its resident size until the system runs short. After a //! raw decode (hundreds of MB of temporaries) that made the resident size climb towards the sum of //! everything ever decoded. `malloc_zone_pressure_relief` (libSystem) returns those pages now. //! Elsewhere (glibc, Windows) blocks this large are unmapped when freed: nothing to do. //! //! This is LightCraft's only crate allowed `unsafe` (one FFI call, in `macos`). Its API is safe, //! and on failure or on other platforms it simply releases nothing. #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] /// Return free allocator pages to the system. Returns the number of bytes released (always 0 on /// platforms where freed large blocks are already unmapped). Safe to call at any time, from any /// thread. pub fn release_free_memory() -> usize { #[cfg(target_os = "macos")] { macos::release() } #[cfg(not(target_os = "macos"))] { 0 } } #[cfg(target_os = "macos")] #[allow(unsafe_code)] mod macos { unsafe extern "C" { /// libSystem malloc: release free memory of `zone` (all zones when null) back to the /// system, up to `goal` bytes (0 = as much as possible). Returns the bytes released. fn malloc_zone_pressure_relief(zone: *mut std::ffi::c_void, goal: usize) -> usize; } pub fn release() -> usize { // SAFETY: `malloc_zone_pressure_relief` is a documented, thread-safe libSystem function // (malloc/malloc.h). A null zone means "every zone" and a goal of 0 means "as much as // possible". It only returns already-free pages to the system: it reads and writes no // memory we own, takes no pointers we must keep alive, and has no failure mode beyond // releasing 0 bytes. unsafe { malloc_zone_pressure_relief(std::ptr::null_mut(), 0) } } } #[cfg(test)] mod tests { use super::release_free_memory; #[test] fn releasing_with_nothing_freed_is_harmless() { // Twice in a row: the second call usually has nothing left to release. let _ = release_free_memory(); let _ = release_free_memory(); } #[test] fn freed_large_blocks_can_be_released_and_memory_stays_usable() { // Allocate and free well over the allocator's large-block threshold, then release. let blocks: Vec> = (0..8).map(|i| vec![i as u8; 16 << 20]).collect(); let sum: u64 = blocks.iter().map(|b| u64::from(b[b.len() - 1])).sum(); assert_eq!(sum, (0..8u64).sum()); drop(blocks); let released = release_free_memory(); if cfg!(not(target_os = "macos")) { assert_eq!(released, 0); } // The allocator must still work normally afterwards. let again = vec![7u8; 16 << 20]; assert_eq!(again[again.len() - 1], 7); } #[test] fn concurrent_calls_are_safe() { let handles: Vec<_> = (0..4).map(|_| std::thread::spawn(release_free_memory)).collect(); for h in handles { assert!(h.join().is_ok()); } } }