81 lines
3.3 KiB
Rust
81 lines
3.3 KiB
Rust
//! 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<Vec<u8>> = (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());
|
|
}
|
|
}
|
|
}
|