whycodes_core/
process_memory.rs1use std::sync::Mutex;
19use std::time::{Duration, Instant};
20
21pub const DEFAULT_ARENA_MAX: i32 = 4;
23pub const DEFAULT_MMAP_THRESHOLD: i32 = 256 * 1024;
25
26static LAST_TRIM: Mutex<Option<Instant>> = Mutex::new(None);
27
28pub fn configure_system_allocator() {
32 let arena_max = env_int("WHYCODES_GLIBC_ARENA_MAX", DEFAULT_ARENA_MAX);
33 let mmap_threshold = env_int("WHYCODES_GLIBC_MMAP_THRESHOLD", DEFAULT_MMAP_THRESHOLD);
34 glibc::set_limits(arena_max, mmap_threshold);
35}
36
37fn env_int(key: &str, default: i32) -> i32 {
38 parse_int(std::env::var(key).as_deref().unwrap_or(""), default)
39}
40
41fn parse_int(raw: &str, default: i32) -> i32 {
42 raw.trim().parse().unwrap_or(default)
43}
44
45pub fn release_retained_heap(reason: &'static str) {
47 let trimmed = glibc::trim();
48 tracing::debug!(reason, trimmed, "released retained heap");
49 *crate::file_claims::recover_lock(LAST_TRIM.lock()) = Some(Instant::now());
50}
51
52pub fn release_retained_heap_debounced(reason: &'static str, min_interval: Duration) -> bool {
55 let last = *crate::file_claims::recover_lock(LAST_TRIM.lock());
56 let recent = last.is_some_and(|t| t.elapsed() < min_interval);
57 if recent {
58 return false;
59 }
60 release_retained_heap(reason);
61 true
62}
63
64pub fn retained_bytes() -> Option<u64> {
67 let rss_anon = rss_anon_bytes()?;
68 let live = glibc::live_bytes()?;
69 Some(rss_anon.saturating_sub(live))
70}
71
72#[cfg(target_os = "linux")]
74pub fn rss_anon_bytes() -> Option<u64> {
75 let status = std::fs::read_to_string("/proc/self/status");
76 status.ok().and_then(|s| parse_rss_anon(&s))
77}
78
79#[cfg(not(target_os = "linux"))]
81pub fn rss_anon_bytes() -> Option<u64> {
82 None
83}
84
85pub fn parse_rss_anon(status: &str) -> Option<u64> {
87 let line = status.lines().find(|l| l.starts_with("RssAnon:"))?;
88 let kb: u64 = line.split_whitespace().nth(1)?.parse().ok()?;
89 Some(kb * 1024)
90}
91
92#[derive(Debug)]
97pub struct RetentionWatch {
98 threshold: u64,
99 baseline: Option<u64>,
100}
101
102impl RetentionWatch {
103 pub fn new(threshold_bytes: u64) -> Self {
104 Self {
105 threshold: threshold_bytes,
106 baseline: None,
107 }
108 }
109
110 pub fn check(&mut self, reason: &'static str) -> bool {
112 retained_bytes().is_some_and(|r| self.observe(r, reason, retained_bytes))
113 }
114
115 pub fn observe(
117 &mut self,
118 retained: u64,
119 reason: &'static str,
120 after_trim: fn() -> Option<u64>,
121 ) -> bool {
122 let baseline = *self.baseline.get_or_insert(retained);
123 if retained < baseline {
124 self.baseline = Some(retained);
125 }
126 if retained.saturating_sub(baseline) < self.threshold {
127 return false;
128 }
129 release_retained_heap(reason);
130 self.baseline = Some(after_trim().unwrap_or(retained));
131 true
132 }
133
134 pub fn baseline(&self) -> Option<u64> {
136 self.baseline
137 }
138}
139
140#[cfg(all(target_os = "linux", target_env = "gnu"))]
141mod glibc {
142 use std::ffi::{c_char, c_int, c_void};
143
144 const M_MMAP_THRESHOLD: c_int = -3;
146 const M_ARENA_MAX: c_int = -8;
147
148 #[repr(C)]
150 #[allow(dead_code)]
151 struct Mallinfo2 {
152 arena: usize,
153 ordblks: usize,
154 smblks: usize,
155 hblks: usize,
156 hblkhd: usize,
157 usmblks: usize,
158 fsmblks: usize,
159 uordblks: usize,
160 fordblks: usize,
161 keepcost: usize,
162 }
163
164 unsafe extern "C" {
165 fn mallopt(param: c_int, value: c_int) -> c_int;
166 fn malloc_trim(pad: usize) -> c_int;
167 fn dlsym(handle: *mut c_void, symbol: *const c_char) -> *mut c_void;
168 }
169
170 pub(super) fn set_limits(arena_max: i32, mmap_threshold: i32) {
171 let limits = [(M_ARENA_MAX, arena_max), (M_MMAP_THRESHOLD, mmap_threshold)];
172 for (param, value) in limits.into_iter().filter(|&(_, v)| v > 0) {
174 unsafe { mallopt(param, value) };
177 }
178 }
179
180 pub(super) fn trim() -> bool {
181 unsafe { malloc_trim(0) == 1 }
183 }
184
185 pub(super) fn live_bytes() -> Option<u64> {
188 let sym = unsafe { dlsym(std::ptr::null_mut(), c"mallinfo2".as_ptr()) };
191 (!sym.is_null()).then(|| {
192 let mallinfo2: extern "C" fn() -> Mallinfo2 = unsafe { std::mem::transmute(sym) };
193 let info = mallinfo2();
194 (info.uordblks + info.hblkhd) as u64
195 })
196 }
197}
198
199#[cfg(not(all(target_os = "linux", target_env = "gnu")))]
200mod glibc {
201 pub(super) fn set_limits(_arena_max: i32, _mmap_threshold: i32) {}
202
203 pub(super) fn trim() -> bool {
204 false
205 }
206
207 pub(super) fn live_bytes() -> Option<u64> {
208 None
209 }
210}
211
212#[cfg(test)]
213#[path = "process_memory_tests.rs"]
214mod tests;