1use std::collections::HashMap;
4use std::sync::atomic::{AtomicUsize, Ordering};
5
6use crate::api::tag::AllocationTag;
7use crate::sync::mutex::Mutex;
8
9pub struct BudgetManager {
11 global_limit: usize,
13
14 current_usage: AtomicUsize,
16
17 tag_data: Mutex<HashMap<&'static str, TagBudget>>,
19
20 event_callback: Mutex<Option<Box<dyn Fn(BudgetEvent) + Send + Sync>>>,
22}
23
24#[derive(Debug, Clone)]
26pub struct TagBudget {
27 pub name: &'static str,
29
30 pub soft_limit: usize,
32
33 pub hard_limit: usize,
35
36 pub current_usage: usize,
38
39 pub peak_usage: usize,
41
42 pub allocation_count: u64,
44
45 pub deallocation_count: u64,
47}
48
49impl TagBudget {
50 pub fn new(name: &'static str, soft_limit: usize, hard_limit: usize) -> Self {
52 Self {
53 name,
54 soft_limit,
55 hard_limit,
56 current_usage: 0,
57 peak_usage: 0,
58 allocation_count: 0,
59 deallocation_count: 0,
60 }
61 }
62
63 pub fn check_status(&self, additional_size: usize) -> BudgetStatus {
65 let projected = self.current_usage + additional_size;
66
67 if self.hard_limit > 0 && projected > self.hard_limit {
68 BudgetStatus::Exceeded
69 } else if self.soft_limit > 0 && projected > self.soft_limit {
70 BudgetStatus::Warning
71 } else {
72 BudgetStatus::Ok
73 }
74 }
75
76 pub fn usage_percent(&self) -> f64 {
78 if self.hard_limit == 0 {
79 0.0
80 } else {
81 (self.current_usage as f64 / self.hard_limit as f64) * 100.0
82 }
83 }
84}
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
88pub enum BudgetStatus {
89 Ok,
91
92 Warning,
94
95 Exceeded,
97}
98
99#[derive(Debug, Clone)]
101pub enum BudgetEvent {
102 SoftLimitExceeded {
104 tag: &'static str,
105 current: usize,
106 limit: usize,
107 },
108 HardLimitExceeded {
110 tag: &'static str,
111 current: usize,
112 limit: usize,
113 },
114 GlobalLimitExceeded {
116 current: usize,
117 limit: usize,
118 },
119 NewPeak {
121 tag: &'static str,
122 peak: usize,
123 },
124}
125
126impl BudgetManager {
127 pub fn new(global_limit: usize) -> Self {
129 Self {
130 global_limit,
131 current_usage: AtomicUsize::new(0),
132 tag_data: Mutex::new(HashMap::new()),
133 event_callback: Mutex::new(None),
134 }
135 }
136
137 pub fn set_event_callback<F>(&self, callback: F)
139 where
140 F: Fn(BudgetEvent) + Send + Sync + 'static,
141 {
142 let mut cb = self.event_callback.lock();
143 *cb = Some(Box::new(callback));
144 }
145
146 pub fn register_tag(&self, tag: &AllocationTag, soft_limit: usize, hard_limit: usize) {
148 let mut data = self.tag_data.lock();
149 data.insert(tag.name(), TagBudget::new(tag.name(), soft_limit, hard_limit));
150 }
151
152 pub fn register_tag_budget(&self, name: &'static str, soft_limit: usize, hard_limit: usize) {
154 let mut data = self.tag_data.lock();
155 data.insert(name, TagBudget::new(name, soft_limit, hard_limit));
156 }
157
158 pub fn check_allocation(&self, size: usize, new_total: usize) -> BudgetStatus {
160 if self.global_limit > 0 && new_total > self.global_limit {
161 self.emit_event(BudgetEvent::GlobalLimitExceeded {
162 current: new_total,
163 limit: self.global_limit,
164 });
165 return BudgetStatus::Exceeded;
166 }
167
168 self.current_usage.store(new_total, Ordering::Relaxed);
169
170 if self.global_limit > 0 {
172 let soft_limit = self.global_limit * 9 / 10;
173 if new_total > soft_limit {
174 return BudgetStatus::Warning;
175 }
176 }
177
178 let _ = size; BudgetStatus::Ok
180 }
181
182 pub fn check_tagged_allocation(&self, tag: &AllocationTag, size: usize) -> BudgetStatus {
184 let mut data = self.tag_data.lock();
185
186 let budget = data.entry(tag.name()).or_insert_with(|| {
188 TagBudget::new(tag.name(), 0, 0) });
190
191 let status = budget.check_status(size);
192
193 budget.current_usage += size;
195 budget.allocation_count += 1;
196
197 if budget.current_usage > budget.peak_usage {
199 budget.peak_usage = budget.current_usage;
200 self.emit_event(BudgetEvent::NewPeak {
201 tag: tag.name(),
202 peak: budget.peak_usage,
203 });
204 }
205
206 match status {
208 BudgetStatus::Warning => {
209 self.emit_event(BudgetEvent::SoftLimitExceeded {
210 tag: tag.name(),
211 current: budget.current_usage,
212 limit: budget.soft_limit,
213 });
214 }
215 BudgetStatus::Exceeded => {
216 self.emit_event(BudgetEvent::HardLimitExceeded {
217 tag: tag.name(),
218 current: budget.current_usage,
219 limit: budget.hard_limit,
220 });
221 }
222 BudgetStatus::Ok => {}
223 }
224
225 status
226 }
227
228 pub fn record_tagged_deallocation(&self, tag: &AllocationTag, size: usize) {
230 let mut data = self.tag_data.lock();
231
232 if let Some(budget) = data.get_mut(tag.name()) {
233 budget.current_usage = budget.current_usage.saturating_sub(size);
234 budget.deallocation_count += 1;
235 }
236 }
237
238 pub fn current_usage(&self) -> usize {
240 self.current_usage.load(Ordering::Relaxed)
241 }
242
243 pub fn global_limit(&self) -> usize {
245 self.global_limit
246 }
247
248 pub fn get_all_tag_budgets(&self) -> Vec<TagBudget> {
250 let data = self.tag_data.lock();
251 data.values().cloned().collect()
252 }
253
254 pub fn get_tag_budget(&self, tag: &AllocationTag) -> Option<TagBudget> {
256 let data = self.tag_data.lock();
257 data.get(tag.name()).cloned()
258 }
259
260 pub fn reset_stats(&self) {
262 let mut data = self.tag_data.lock();
263 for budget in data.values_mut() {
264 budget.current_usage = 0;
265 budget.peak_usage = 0;
266 budget.allocation_count = 0;
267 budget.deallocation_count = 0;
268 }
269 self.current_usage.store(0, Ordering::Relaxed);
270 }
271
272 fn emit_event(&self, event: BudgetEvent) {
274 if let Some(ref callback) = *self.event_callback.lock() {
275 callback(event);
276 }
277 }
278}
279
280#[cfg(test)]
281mod tests {
282 use super::*;
283
284 #[test]
285 fn test_tag_budget_tracking() {
286 let manager = BudgetManager::new(0);
287 let tag = AllocationTag::new("test");
288
289 manager.register_tag(&tag, 1000, 2000);
290
291 let status = manager.check_tagged_allocation(&tag, 500);
293 assert_eq!(status, BudgetStatus::Ok);
294
295 let status = manager.check_tagged_allocation(&tag, 600);
297 assert_eq!(status, BudgetStatus::Warning);
298
299 let budget = manager.get_tag_budget(&tag).unwrap();
301 assert_eq!(budget.current_usage, 1100);
302 assert_eq!(budget.allocation_count, 2);
303 }
304
305 #[test]
306 fn test_hard_limit() {
307 let manager = BudgetManager::new(0);
308 let tag = AllocationTag::new("limited");
309
310 manager.register_tag(&tag, 500, 1000);
311
312 manager.check_tagged_allocation(&tag, 800);
313 let status = manager.check_tagged_allocation(&tag, 300);
314
315 assert_eq!(status, BudgetStatus::Exceeded);
316 }
317
318 #[test]
319 fn test_deallocation() {
320 let manager = BudgetManager::new(0);
321 let tag = AllocationTag::new("dealloc_test");
322
323 manager.check_tagged_allocation(&tag, 1000);
324 manager.record_tagged_deallocation(&tag, 400);
325
326 let budget = manager.get_tag_budget(&tag).unwrap();
327 assert_eq!(budget.current_usage, 600);
328 assert_eq!(budget.deallocation_count, 1);
329 }
330}