krishiv_common/
memory_budget.rs1#![forbid(unsafe_code)]
2
3use std::sync::Arc;
6use std::sync::atomic::{AtomicU64, Ordering};
7
8#[derive(Debug)]
17pub struct MemoryBudget {
18 used_bytes: AtomicU64,
19 peak_bytes: AtomicU64,
21 limit_bytes: Option<u64>,
22}
23
24impl MemoryBudget {
25 pub fn unlimited() -> Arc<Self> {
28 Arc::new(Self {
29 used_bytes: AtomicU64::new(0),
30 peak_bytes: AtomicU64::new(0),
31 limit_bytes: None,
32 })
33 }
34
35 pub fn limited(limit_bytes: u64) -> Arc<Self> {
37 Arc::new(Self {
38 used_bytes: AtomicU64::new(0),
39 peak_bytes: AtomicU64::new(0),
40 limit_bytes: Some(limit_bytes),
41 })
42 }
43
44 pub fn from_limit(limit_bytes: Option<u64>) -> Arc<Self> {
46 match limit_bytes {
47 Some(b) if b > 0 => Self::limited(b),
48 _ => Self::unlimited(),
49 }
50 }
51
52 pub fn limit(&self) -> Option<u64> {
54 self.limit_bytes
55 }
56
57 pub fn used_bytes(&self) -> u64 {
59 self.used_bytes.load(Ordering::Relaxed)
60 }
61
62 pub fn peak_bytes(&self) -> u64 {
64 self.peak_bytes.load(Ordering::Relaxed)
65 }
66
67 pub fn try_reserve(&self, bytes: u64) -> bool {
73 let Some(limit) = self.limit_bytes else {
74 let new = self
75 .used_bytes
76 .fetch_add(bytes, Ordering::Relaxed)
77 .saturating_add(bytes);
78 self.peak_bytes.fetch_max(new, Ordering::Relaxed);
79 return true;
80 };
81 let prev = self.used_bytes.fetch_add(bytes, Ordering::Relaxed);
82 if prev + bytes > limit {
83 self.used_bytes.fetch_sub(bytes, Ordering::Relaxed);
85 false
86 } else {
87 self.peak_bytes
88 .fetch_max(prev.saturating_add(bytes), Ordering::Relaxed);
89 true
90 }
91 }
92
93 pub fn release(&self, bytes: u64) {
95 let _ = self
96 .used_bytes
97 .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |cur| {
98 Some(cur.saturating_sub(bytes))
99 });
100 }
101
102 pub fn remaining(&self) -> Option<u64> {
104 self.limit_bytes
105 .map(|l| l.saturating_sub(self.used_bytes()))
106 }
107}
108
109pub fn cgroup_memory_limit_bytes() -> Option<u64> {
114 const V1_UNLIMITED_FLOOR: u64 = 1 << 60;
115 for path in [
116 "/sys/fs/cgroup/memory.max",
117 "/sys/fs/cgroup/memory/memory.limit_in_bytes",
118 ] {
119 if let Ok(raw) = std::fs::read_to_string(path) {
120 let trimmed = raw.trim();
121 if trimmed == "max" {
122 return None;
123 }
124 return trimmed
125 .parse::<u64>()
126 .ok()
127 .filter(|&n| n > 0 && n < V1_UNLIMITED_FLOOR);
128 }
129 }
130 None
131}
132
133#[derive(Debug, Clone, Copy, PartialEq, Eq)]
142pub struct CgroupMemoryUsage {
143 pub current_bytes: u64,
145 pub anon_bytes: u64,
147 pub file_bytes: u64,
152}
153
154impl CgroupMemoryUsage {
155 pub fn unaccounted_bytes(&self) -> u64 {
157 self.current_bytes.saturating_sub(self.anon_bytes)
158 }
159}
160
161pub fn cgroup_memory_usage() -> Option<CgroupMemoryUsage> {
167 let current = std::fs::read_to_string("/sys/fs/cgroup/memory.current")
168 .ok()?
169 .trim()
170 .parse::<u64>()
171 .ok()?;
172 let stat = std::fs::read_to_string("/sys/fs/cgroup/memory.stat").ok()?;
173 let mut anon = 0u64;
174 let mut file = 0u64;
175 for line in stat.lines() {
176 let mut parts = line.split_ascii_whitespace();
177 match (parts.next(), parts.next()) {
178 (Some("anon"), Some(v)) => anon = v.parse().unwrap_or(0),
179 (Some("file"), Some(v)) => file = v.parse().unwrap_or(0),
180 _ => {}
181 }
182 }
183 Some(CgroupMemoryUsage {
184 current_bytes: current,
185 anon_bytes: anon,
186 file_bytes: file,
187 })
188}
189
190#[cfg(test)]
191mod tests {
192 use super::*;
193
194 #[test]
198 fn cgroup_usage_is_absent_or_internally_consistent() {
199 if let Some(usage) = cgroup_memory_usage() {
200 assert!(
201 usage.current_bytes >= usage.anon_bytes,
202 "anon ({}) cannot exceed total charge ({})",
203 usage.anon_bytes,
204 usage.current_bytes
205 );
206 assert_eq!(
207 usage.unaccounted_bytes(),
208 usage.current_bytes - usage.anon_bytes
209 );
210 }
211 }
212
213 #[test]
214 fn unlimited_always_accepts() {
215 let b = MemoryBudget::unlimited();
216 assert!(b.try_reserve(u64::MAX / 2));
217 assert!(b.try_reserve(u64::MAX / 2));
218 assert!(b.limit().is_none());
219 assert!(b.remaining().is_none());
220 }
221
222 #[test]
223 fn limited_rejects_over_limit() {
224 let b = MemoryBudget::limited(100);
225 assert!(b.try_reserve(60));
226 assert!(!b.try_reserve(60)); assert_eq!(b.used_bytes(), 60); }
229
230 #[test]
231 fn release_reduces_counter() {
232 let b = MemoryBudget::limited(100);
233 assert!(b.try_reserve(80));
234 b.release(40);
235 assert_eq!(b.used_bytes(), 40);
236 assert!(b.try_reserve(60)); }
238
239 #[test]
240 fn release_saturates_at_zero() {
241 let b = MemoryBudget::unlimited();
242 b.release(999); assert_eq!(b.used_bytes(), 0);
244 }
245
246 #[test]
247 fn remaining_tracks_available() {
248 let b = MemoryBudget::limited(200);
249 assert_eq!(b.remaining(), Some(200));
250 b.try_reserve(50);
251 assert_eq!(b.remaining(), Some(150));
252 }
253
254 #[test]
255 fn from_limit_none_is_unlimited() {
256 let b = MemoryBudget::from_limit(None);
257 assert!(b.limit().is_none());
258 }
259
260 #[test]
261 fn from_limit_zero_is_unlimited() {
262 let b = MemoryBudget::from_limit(Some(0));
263 assert!(b.limit().is_none());
264 }
265
266 #[test]
267 fn peak_tracks_high_water_mark() {
268 let b = MemoryBudget::limited(100);
269 assert!(b.try_reserve(80));
270 b.release(80);
271 assert!(b.try_reserve(30));
272 assert_eq!(b.used_bytes(), 30);
273 assert_eq!(b.peak_bytes(), 80, "peak must survive releases");
274 }
275
276 #[test]
277 fn peak_ignores_rejected_reservations() {
278 let b = MemoryBudget::limited(100);
279 assert!(b.try_reserve(50));
280 assert!(!b.try_reserve(60)); assert_eq!(b.peak_bytes(), 50);
282 }
283
284 #[test]
285 fn peak_tracked_for_unlimited_budget() {
286 let b = MemoryBudget::unlimited();
287 b.try_reserve(1000);
288 b.release(500);
289 assert_eq!(b.peak_bytes(), 1000);
290 }
291}