use std::sync::Mutex;
use std::time::{Duration, Instant};
pub const DEFAULT_ARENA_MAX: i32 = 4;
pub const DEFAULT_MMAP_THRESHOLD: i32 = 256 * 1024;
static LAST_TRIM: Mutex<Option<Instant>> = Mutex::new(None);
pub fn configure_system_allocator() {
let arena_max = env_int("WHYCODES_GLIBC_ARENA_MAX", DEFAULT_ARENA_MAX);
let mmap_threshold = env_int("WHYCODES_GLIBC_MMAP_THRESHOLD", DEFAULT_MMAP_THRESHOLD);
glibc::set_limits(arena_max, mmap_threshold);
}
fn env_int(key: &str, default: i32) -> i32 {
parse_int(std::env::var(key).as_deref().unwrap_or(""), default)
}
fn parse_int(raw: &str, default: i32) -> i32 {
raw.trim().parse().unwrap_or(default)
}
pub fn release_retained_heap(reason: &'static str) {
let trimmed = glibc::trim();
tracing::debug!(reason, trimmed, "released retained heap");
*crate::file_claims::recover_lock(LAST_TRIM.lock()) = Some(Instant::now());
}
pub fn release_retained_heap_debounced(reason: &'static str, min_interval: Duration) -> bool {
let last = *crate::file_claims::recover_lock(LAST_TRIM.lock());
let recent = last.is_some_and(|t| t.elapsed() < min_interval);
if recent {
return false;
}
release_retained_heap(reason);
true
}
pub fn retained_bytes() -> Option<u64> {
let rss_anon = rss_anon_bytes()?;
let live = glibc::live_bytes()?;
Some(rss_anon.saturating_sub(live))
}
#[cfg(target_os = "linux")]
pub fn rss_anon_bytes() -> Option<u64> {
let status = std::fs::read_to_string("/proc/self/status");
status.ok().and_then(|s| parse_rss_anon(&s))
}
#[cfg(not(target_os = "linux"))]
pub fn rss_anon_bytes() -> Option<u64> {
None
}
pub fn parse_rss_anon(status: &str) -> Option<u64> {
let line = status.lines().find(|l| l.starts_with("RssAnon:"))?;
let kb: u64 = line.split_whitespace().nth(1)?.parse().ok()?;
Some(kb * 1024)
}
#[derive(Debug)]
pub struct RetentionWatch {
threshold: u64,
baseline: Option<u64>,
}
impl RetentionWatch {
pub fn new(threshold_bytes: u64) -> Self {
Self {
threshold: threshold_bytes,
baseline: None,
}
}
pub fn check(&mut self, reason: &'static str) -> bool {
retained_bytes().is_some_and(|r| self.observe(r, reason, retained_bytes))
}
pub fn observe(
&mut self,
retained: u64,
reason: &'static str,
after_trim: fn() -> Option<u64>,
) -> bool {
let baseline = *self.baseline.get_or_insert(retained);
if retained < baseline {
self.baseline = Some(retained);
}
if retained.saturating_sub(baseline) < self.threshold {
return false;
}
release_retained_heap(reason);
self.baseline = Some(after_trim().unwrap_or(retained));
true
}
pub fn baseline(&self) -> Option<u64> {
self.baseline
}
}
#[cfg(all(target_os = "linux", target_env = "gnu"))]
mod glibc {
use std::ffi::{c_char, c_int, c_void};
const M_MMAP_THRESHOLD: c_int = -3;
const M_ARENA_MAX: c_int = -8;
#[repr(C)]
#[allow(dead_code)]
struct Mallinfo2 {
arena: usize,
ordblks: usize,
smblks: usize,
hblks: usize,
hblkhd: usize,
usmblks: usize,
fsmblks: usize,
uordblks: usize,
fordblks: usize,
keepcost: usize,
}
unsafe extern "C" {
fn mallopt(param: c_int, value: c_int) -> c_int;
fn malloc_trim(pad: usize) -> c_int;
fn dlsym(handle: *mut c_void, symbol: *const c_char) -> *mut c_void;
}
pub(super) fn set_limits(arena_max: i32, mmap_threshold: i32) {
let limits = [(M_ARENA_MAX, arena_max), (M_MMAP_THRESHOLD, mmap_threshold)];
for (param, value) in limits.into_iter().filter(|&(_, v)| v > 0) {
unsafe { mallopt(param, value) };
}
}
pub(super) fn trim() -> bool {
unsafe { malloc_trim(0) == 1 }
}
pub(super) fn live_bytes() -> Option<u64> {
let sym = unsafe { dlsym(std::ptr::null_mut(), c"mallinfo2".as_ptr()) };
(!sym.is_null()).then(|| {
let mallinfo2: extern "C" fn() -> Mallinfo2 = unsafe { std::mem::transmute(sym) };
let info = mallinfo2();
(info.uordblks + info.hblkhd) as u64
})
}
}
#[cfg(not(all(target_os = "linux", target_env = "gnu")))]
mod glibc {
pub(super) fn set_limits(_arena_max: i32, _mmap_threshold: i32) {}
pub(super) fn trim() -> bool {
false
}
pub(super) fn live_bytes() -> Option<u64> {
None
}
}
#[cfg(test)]
#[path = "process_memory_tests.rs"]
mod tests;