Skip to main content

noxu_dbi/
memory_budget.rs

1use std::sync::atomic::{AtomicI64, Ordering};
2
3/// MemoryBudget calculates available memory and tracks usage.
4///
5/// Centralizes all memory calculations. Objects that need memory
6/// should get settings from this class.
7///
8///
9pub struct MemoryBudget {
10    /// Maximum cache size in bytes.
11    max_memory: i64,
12    /// Current cache usage: tree (IN) memory.
13    tree_memory_usage: AtomicI64,
14    /// Current cache usage: lock memory.
15    lock_memory_usage: AtomicI64,
16    /// Current cache usage: admin/misc memory.
17    admin_memory_usage: AtomicI64,
18    /// Current cache usage: log buffer memory.
19    log_buffer_budget: i64,
20}
21
22/// Estimated object overheads in bytes (Rust equivalents of constants).
23/// These are approximate sizes used for memory accounting.
24pub struct MemoryOverhead;
25
26impl MemoryOverhead {
27    /// Overhead for a LockImpl.
28    pub const LOCKIMPL_OVERHEAD: i64 = 96;
29    /// Overhead for a ThinLockImpl.
30    pub const THINLOCKIMPL_OVERHEAD: i64 = 32;
31    /// Overhead for a LockInfo.
32    pub const LOCKINFO_OVERHEAD: i64 = 32;
33    /// Overhead for a HashMap entry.
34    pub const HASHMAP_ENTRY_OVERHEAD: i64 = 48;
35    /// Overhead for a boxed i64.
36    pub const LONG_OVERHEAD: i64 = 16;
37    /// Overhead for an IN node (approximate).
38    pub const IN_OVERHEAD: i64 = 400;
39    /// Overhead for a BIN node.
40    pub const BIN_OVERHEAD: i64 = 500;
41    /// Overhead for an LN.
42    pub const LN_OVERHEAD: i64 = 48;
43    /// Overhead for a Txn.
44    pub const TXN_OVERHEAD: i64 = 200;
45    /// Overhead for a BasicLocker.
46    pub const BASICLOCKER_OVERHEAD: i64 = 80;
47}
48
49impl MemoryBudget {
50    /// Creates a new MemoryBudget with the given max cache size.
51    pub fn new(max_memory: i64) -> Self {
52        // Reserve 7% for log buffers (matching default)
53        let log_buffer_budget = max_memory * 7 / 100;
54
55        MemoryBudget {
56            max_memory,
57            tree_memory_usage: AtomicI64::new(0),
58            lock_memory_usage: AtomicI64::new(0),
59            admin_memory_usage: AtomicI64::new(0),
60            log_buffer_budget,
61        }
62    }
63
64    /// Returns the maximum cache size.
65    pub fn max_memory(&self) -> i64 {
66        self.max_memory
67    }
68
69    /// Returns the log buffer budget.
70    pub fn log_buffer_budget(&self) -> i64 {
71        self.log_buffer_budget
72    }
73
74    /// Returns total current memory usage.
75    pub fn total_usage(&self) -> i64 {
76        self.tree_memory_usage.load(Ordering::Relaxed)
77            + self.lock_memory_usage.load(Ordering::Relaxed)
78            + self.admin_memory_usage.load(Ordering::Relaxed)
79    }
80
81    /// Returns the available (free) cache memory.
82    pub fn available_memory(&self) -> i64 {
83        self.max_memory - self.total_usage()
84    }
85
86    /// Returns true if the cache is over budget.
87    pub fn is_over_budget(&self) -> bool {
88        self.total_usage() > self.max_memory
89    }
90
91    // Tree memory
92    pub fn get_tree_memory_usage(&self) -> i64 {
93        self.tree_memory_usage.load(Ordering::Relaxed)
94    }
95
96    pub fn update_tree_memory_usage(&self, delta: i64) {
97        self.tree_memory_usage.fetch_add(delta, Ordering::Relaxed);
98    }
99
100    // Lock memory
101    pub fn get_lock_memory_usage(&self) -> i64 {
102        self.lock_memory_usage.load(Ordering::Relaxed)
103    }
104
105    pub fn update_lock_memory_usage(&self, delta: i64) {
106        self.lock_memory_usage.fetch_add(delta, Ordering::Relaxed);
107    }
108
109    // Admin memory
110    pub fn get_admin_memory_usage(&self) -> i64 {
111        self.admin_memory_usage.load(Ordering::Relaxed)
112    }
113
114    pub fn update_admin_memory_usage(&self, delta: i64) {
115        self.admin_memory_usage.fetch_add(delta, Ordering::Relaxed);
116    }
117
118    /// Resets all usage counters to zero.
119    pub fn reset(&self) {
120        self.tree_memory_usage.store(0, Ordering::Relaxed);
121        self.lock_memory_usage.store(0, Ordering::Relaxed);
122        self.admin_memory_usage.store(0, Ordering::Relaxed);
123    }
124
125    /// Returns a summary of memory usage.
126    pub fn get_stats(&self) -> MemoryBudgetStats {
127        MemoryBudgetStats {
128            max_memory: self.max_memory,
129            total_usage: self.total_usage(),
130            tree_memory: self.get_tree_memory_usage(),
131            lock_memory: self.get_lock_memory_usage(),
132            admin_memory: self.get_admin_memory_usage(),
133            log_buffer_budget: self.log_buffer_budget,
134        }
135    }
136}
137
138/// Snapshot of memory budget statistics.
139#[derive(Debug, Clone)]
140pub struct MemoryBudgetStats {
141    pub max_memory: i64,
142    pub total_usage: i64,
143    pub tree_memory: i64,
144    pub lock_memory: i64,
145    pub admin_memory: i64,
146    pub log_buffer_budget: i64,
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152
153    #[test]
154    fn test_total_usage_is_sum() {
155        let budget = MemoryBudget::new(1000);
156
157        budget.update_tree_memory_usage(100);
158        budget.update_lock_memory_usage(50);
159        budget.update_admin_memory_usage(25);
160
161        assert_eq!(budget.total_usage(), 175);
162    }
163
164    #[test]
165    fn test_available_memory_calculation() {
166        let budget = MemoryBudget::new(1000);
167
168        assert_eq!(budget.available_memory(), 1000);
169
170        budget.update_tree_memory_usage(300);
171        assert_eq!(budget.available_memory(), 700);
172
173        budget.update_lock_memory_usage(200);
174        assert_eq!(budget.available_memory(), 500);
175    }
176
177    #[test]
178    fn test_is_over_budget() {
179        let budget = MemoryBudget::new(1000);
180
181        assert!(!budget.is_over_budget());
182
183        budget.update_tree_memory_usage(500);
184        assert!(!budget.is_over_budget());
185
186        budget.update_lock_memory_usage(500);
187        assert!(!budget.is_over_budget());
188
189        budget.update_admin_memory_usage(1);
190        assert!(budget.is_over_budget());
191    }
192
193    #[test]
194    fn test_update_with_positive_and_negative_deltas() {
195        let budget = MemoryBudget::new(1000);
196
197        budget.update_tree_memory_usage(500);
198        assert_eq!(budget.get_tree_memory_usage(), 500);
199
200        budget.update_tree_memory_usage(-200);
201        assert_eq!(budget.get_tree_memory_usage(), 300);
202
203        budget.update_tree_memory_usage(-300);
204        assert_eq!(budget.get_tree_memory_usage(), 0);
205    }
206
207    #[test]
208    fn test_reset_clears_all() {
209        let budget = MemoryBudget::new(1000);
210
211        budget.update_tree_memory_usage(100);
212        budget.update_lock_memory_usage(200);
213        budget.update_admin_memory_usage(300);
214
215        budget.reset();
216
217        assert_eq!(budget.get_tree_memory_usage(), 0);
218        assert_eq!(budget.get_lock_memory_usage(), 0);
219        assert_eq!(budget.get_admin_memory_usage(), 0);
220        assert_eq!(budget.total_usage(), 0);
221    }
222
223    #[test]
224    fn test_log_buffer_budget_is_7_percent() {
225        let budget = MemoryBudget::new(10000);
226
227        // 7% of 10000 = 700
228        assert_eq!(budget.log_buffer_budget(), 700);
229    }
230
231    #[test]
232    fn test_memory_overhead_constants_exist() {
233        // Just verify the constants are accessible
234        assert_eq!(MemoryOverhead::LOCKIMPL_OVERHEAD, 96);
235        assert_eq!(MemoryOverhead::THINLOCKIMPL_OVERHEAD, 32);
236        assert_eq!(MemoryOverhead::LOCKINFO_OVERHEAD, 32);
237        assert_eq!(MemoryOverhead::HASHMAP_ENTRY_OVERHEAD, 48);
238        assert_eq!(MemoryOverhead::LONG_OVERHEAD, 16);
239        assert_eq!(MemoryOverhead::IN_OVERHEAD, 400);
240        assert_eq!(MemoryOverhead::BIN_OVERHEAD, 500);
241        assert_eq!(MemoryOverhead::LN_OVERHEAD, 48);
242        assert_eq!(MemoryOverhead::TXN_OVERHEAD, 200);
243        assert_eq!(MemoryOverhead::BASICLOCKER_OVERHEAD, 80);
244    }
245
246    #[test]
247    fn test_get_stats() {
248        let budget = MemoryBudget::new(5000);
249
250        budget.update_tree_memory_usage(1000);
251        budget.update_lock_memory_usage(500);
252        budget.update_admin_memory_usage(250);
253
254        let stats = budget.get_stats();
255
256        assert_eq!(stats.max_memory, 5000);
257        assert_eq!(stats.total_usage, 1750);
258        assert_eq!(stats.tree_memory, 1000);
259        assert_eq!(stats.lock_memory, 500);
260        assert_eq!(stats.admin_memory, 250);
261        assert_eq!(stats.log_buffer_budget, 350); // 7% of 5000
262    }
263}