framealloc/core/
budget.rs

1//! Memory budget management with per-tag tracking.
2
3use std::collections::HashMap;
4use std::sync::atomic::{AtomicUsize, Ordering};
5
6use crate::api::tag::AllocationTag;
7use crate::sync::mutex::Mutex;
8
9/// Manages memory budgets and limits with per-tag tracking.
10pub struct BudgetManager {
11    /// Global memory limit (0 = unlimited)
12    global_limit: usize,
13
14    /// Current total usage (atomic for fast reads)
15    current_usage: AtomicUsize,
16
17    /// Per-tag budgets and usage
18    tag_data: Mutex<HashMap<&'static str, TagBudget>>,
19
20    /// Callback for budget events
21    event_callback: Mutex<Option<Box<dyn Fn(BudgetEvent) + Send + Sync>>>,
22}
23
24/// Budget configuration and current usage for a specific tag.
25#[derive(Debug, Clone)]
26pub struct TagBudget {
27    /// Tag name
28    pub name: &'static str,
29
30    /// Soft limit (warning threshold)
31    pub soft_limit: usize,
32
33    /// Hard limit (allocation may fail)
34    pub hard_limit: usize,
35
36    /// Current usage in bytes
37    pub current_usage: usize,
38
39    /// Peak usage (high water mark)
40    pub peak_usage: usize,
41
42    /// Number of allocations
43    pub allocation_count: u64,
44
45    /// Number of deallocations
46    pub deallocation_count: u64,
47}
48
49impl TagBudget {
50    /// Create a new tag budget.
51    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    /// Check the budget status for a potential allocation.
64    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    /// Calculate usage as a percentage of hard limit.
77    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/// Result of a budget check.
87#[derive(Debug, Clone, Copy, PartialEq, Eq)]
88pub enum BudgetStatus {
89    /// Under budget, allocation allowed
90    Ok,
91
92    /// Over soft limit, warning issued
93    Warning,
94
95    /// Over hard limit, allocation denied
96    Exceeded,
97}
98
99/// Events emitted by the budget manager.
100#[derive(Debug, Clone)]
101pub enum BudgetEvent {
102    /// Soft limit exceeded
103    SoftLimitExceeded {
104        tag: &'static str,
105        current: usize,
106        limit: usize,
107    },
108    /// Hard limit exceeded
109    HardLimitExceeded {
110        tag: &'static str,
111        current: usize,
112        limit: usize,
113    },
114    /// Global limit exceeded
115    GlobalLimitExceeded {
116        current: usize,
117        limit: usize,
118    },
119    /// New peak usage recorded
120    NewPeak {
121        tag: &'static str,
122        peak: usize,
123    },
124}
125
126impl BudgetManager {
127    /// Create a new budget manager.
128    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    /// Set a callback for budget events.
138    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    /// Register a budget for a specific tag.
147    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    /// Register a budget by tag name.
153    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    /// Check if an allocation is within budget (global check).
159    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        // Check soft limit (90% of hard limit)
171        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; // Used in tagged allocations
179        BudgetStatus::Ok
180    }
181
182    /// Check and record a tagged allocation.
183    pub fn check_tagged_allocation(&self, tag: &AllocationTag, size: usize) -> BudgetStatus {
184        let mut data = self.tag_data.lock();
185        
186        // Get or create tag budget
187        let budget = data.entry(tag.name()).or_insert_with(|| {
188            TagBudget::new(tag.name(), 0, 0) // No limits by default
189        });
190
191        let status = budget.check_status(size);
192
193        // Record the allocation
194        budget.current_usage += size;
195        budget.allocation_count += 1;
196
197        // Update peak
198        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        // Emit events based on status
207        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    /// Record a tagged deallocation.
229    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    /// Get current global usage.
239    pub fn current_usage(&self) -> usize {
240        self.current_usage.load(Ordering::Relaxed)
241    }
242
243    /// Get global limit.
244    pub fn global_limit(&self) -> usize {
245        self.global_limit
246    }
247
248    /// Get all tag budgets for reporting.
249    pub fn get_all_tag_budgets(&self) -> Vec<TagBudget> {
250        let data = self.tag_data.lock();
251        data.values().cloned().collect()
252    }
253
254    /// Get a specific tag's budget.
255    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    /// Reset all tag statistics (but keep limits).
261    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    /// Emit a budget event to the callback.
273    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        // Allocate under soft limit
292        let status = manager.check_tagged_allocation(&tag, 500);
293        assert_eq!(status, BudgetStatus::Ok);
294        
295        // Allocate over soft limit
296        let status = manager.check_tagged_allocation(&tag, 600);
297        assert_eq!(status, BudgetStatus::Warning);
298        
299        // Check usage
300        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}