1use std::sync::atomic::{AtomicUsize, Ordering};
31
32use crate::error::{QueryError, Result};
33
34#[derive(Debug)]
39pub struct MemoryTracker {
40 budget: usize,
42 allocated: AtomicUsize,
44}
45
46impl MemoryTracker {
47 #[must_use]
49 pub fn new(budget_bytes: usize) -> Self {
50 Self {
51 budget: budget_bytes,
52 allocated: AtomicUsize::new(0),
53 }
54 }
55
56 pub fn try_allocate(&self, bytes: usize) -> Result<()> {
66 loop {
68 let current = self.allocated.load(Ordering::Relaxed);
69 let new =
70 current
71 .checked_add(bytes)
72 .ok_or_else(|| QueryError::QueryMemoryExceeded {
73 requested: bytes,
74 allocated: current,
75 budget: self.budget,
76 })?;
77
78 if new > self.budget {
79 return Err(QueryError::QueryMemoryExceeded {
80 requested: bytes,
81 allocated: current,
82 budget: self.budget,
83 });
84 }
85
86 if self
87 .allocated
88 .compare_exchange_weak(current, new, Ordering::AcqRel, Ordering::Relaxed)
89 .is_ok()
90 {
91 return Ok(());
92 }
93 }
95 }
96
97 pub fn deallocate(&self, bytes: usize) {
103 let prev = self.allocated.fetch_sub(bytes, Ordering::AcqRel);
104 debug_assert!(
105 prev >= bytes,
106 "deallocate({bytes}) but only {prev} bytes were allocated"
107 );
108 }
109
110 #[must_use]
112 pub fn allocated(&self) -> usize {
113 self.allocated.load(Ordering::Relaxed)
114 }
115
116 #[must_use]
118 pub fn budget(&self) -> usize {
119 self.budget
120 }
121
122 #[must_use]
125 pub fn remaining(&self) -> usize {
126 self.budget
127 .saturating_sub(self.allocated.load(Ordering::Relaxed))
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134
135 #[test]
136 fn basic_allocate_deallocate() {
137 let t = MemoryTracker::new(1000);
138 assert_eq!(t.budget(), 1000);
139 assert_eq!(t.allocated(), 0);
140 assert_eq!(t.remaining(), 1000);
141
142 t.try_allocate(400).unwrap();
143 assert_eq!(t.allocated(), 400);
144 assert_eq!(t.remaining(), 600);
145
146 t.try_allocate(600).unwrap();
147 assert_eq!(t.allocated(), 1000);
148 assert_eq!(t.remaining(), 0);
149
150 t.deallocate(300);
151 assert_eq!(t.allocated(), 700);
152 assert_eq!(t.remaining(), 300);
153 }
154
155 #[test]
156 fn exceeds_budget() {
157 let t = MemoryTracker::new(100);
158 t.try_allocate(60).unwrap();
159
160 let err = t.try_allocate(50).unwrap_err();
161 match err {
162 QueryError::QueryMemoryExceeded {
163 requested,
164 allocated,
165 budget,
166 } => {
167 assert_eq!(requested, 50);
168 assert_eq!(allocated, 60);
169 assert_eq!(budget, 100);
170 }
171 other => panic!("unexpected error: {other}"),
172 }
173
174 assert_eq!(t.allocated(), 60);
176 }
177
178 #[test]
179 fn exact_budget() {
180 let t = MemoryTracker::new(100);
181 t.try_allocate(100).unwrap();
182 assert_eq!(t.allocated(), 100);
183 assert_eq!(t.remaining(), 0);
184
185 assert!(t.try_allocate(1).is_err());
187 }
188
189 #[test]
190 fn zero_budget() {
191 let t = MemoryTracker::new(0);
192 assert!(t.try_allocate(1).is_err());
193 t.try_allocate(0).unwrap();
195 }
196
197 #[test]
198 fn deallocate_full() {
199 let t = MemoryTracker::new(500);
200 t.try_allocate(500).unwrap();
201 t.deallocate(500);
202 assert_eq!(t.allocated(), 0);
203 assert_eq!(t.remaining(), 500);
204
205 t.try_allocate(500).unwrap();
207 }
208
209 #[test]
210 fn concurrent_allocations() {
211 use std::sync::Arc;
212 use std::thread;
213
214 let t = Arc::new(MemoryTracker::new(10_000));
215 let mut handles = vec![];
216
217 for _ in 0..10 {
218 let tracker = Arc::clone(&t);
219 handles.push(thread::spawn(move || {
220 for _ in 0..100 {
221 let _ = tracker.try_allocate(1);
223 }
224 }));
225 }
226
227 for h in handles {
228 h.join().unwrap();
229 }
230
231 assert!(t.allocated() <= 1000);
233 }
234
235 #[test]
236 fn error_display() {
237 let t = MemoryTracker::new(100);
238 t.try_allocate(80).unwrap();
239 let err = t.try_allocate(30).unwrap_err();
240 let msg = err.to_string();
241 assert!(msg.contains("30"), "should mention requested bytes");
242 assert!(msg.contains("80"), "should mention allocated bytes");
243 assert!(msg.contains("100"), "should mention budget");
244 }
245}