Skip to main content

vexy_vsvg/optimizer/
memory.rs

1// this_file: crates/vexy-vsvg/src/optimizer/memory.rs
2
3//! Memory budgeting system for optimizer runtime
4//!
5//! Provides approximate memory tracking during optimization to prevent
6//! excessive memory usage on large or malicious SVG files.
7
8use std::sync::atomic::{AtomicUsize, Ordering};
9use std::sync::Arc;
10
11#[derive(Debug, Clone)]
12pub struct MemoryBudget {
13    limit: usize,
14    used: Arc<AtomicUsize>,
15}
16
17impl PartialEq for MemoryBudget {
18    fn eq(&self, other: &Self) -> bool {
19        self.limit == other.limit && self.used() == other.used()
20    }
21}
22
23impl MemoryBudget {
24    /// Create a new memory budget with the specified limit
25    ///
26    /// # Arguments
27    ///
28    /// * `limit` - Maximum memory usage in bytes
29    ///
30    /// # Examples
31    ///
32    /// ```rust
33    /// use vexy_vsvg::optimizer::memory::MemoryBudget;
34    ///
35    /// // 100MB limit
36    /// let budget = MemoryBudget::new(100 * 1024 * 1024);
37    /// ```
38    pub fn new(limit: usize) -> Self {
39        Self {
40            limit,
41            used: Arc::new(AtomicUsize::new(0)),
42        }
43    }
44
45    /// Attempt to allocate memory from the budget
46    ///
47    /// # Arguments
48    ///
49    /// * `size` - Number of bytes to allocate
50    ///
51    /// # Returns
52    ///
53    /// `Ok(())` if allocation succeeds, `Err(MemoryError)` if budget exceeded
54    ///
55    /// # Examples
56    ///
57    /// ```rust
58    /// use vexy_vsvg::optimizer::memory::MemoryBudget;
59    ///
60    /// let budget = MemoryBudget::new(1024);
61    /// assert!(budget.allocate(512).is_ok());
62    /// assert!(budget.allocate(512).is_ok());
63    /// assert!(budget.allocate(512).is_err()); // Exceeds budget
64    /// ```
65    pub fn allocate(&self, size: usize) -> Result<(), MemoryError> {
66        let current = self.used.fetch_add(size, Ordering::Relaxed);
67        let new_total = current + size;
68
69        if new_total > self.limit {
70            // Rollback the allocation
71            self.used.fetch_sub(size, Ordering::Relaxed);
72            Err(MemoryError::BudgetExceeded {
73                requested: size,
74                used: current,
75                limit: self.limit,
76            })
77        } else {
78            Ok(())
79        }
80    }
81
82    /// Deallocate memory from the budget
83    ///
84    /// # Arguments
85    ///
86    /// * `size` - Number of bytes to deallocate
87    ///
88    /// # Examples
89    ///
90    /// ```rust
91    /// use vexy_vsvg::optimizer::memory::MemoryBudget;
92    ///
93    /// let budget = MemoryBudget::new(1024);
94    /// budget.allocate(512).unwrap();
95    /// budget.deallocate(512);
96    /// assert_eq!(budget.used(), 0);
97    /// ```
98    pub fn deallocate(&self, size: usize) {
99        self.used.fetch_sub(size, Ordering::Relaxed);
100    }
101
102    /// Get current memory usage
103    ///
104    /// # Returns
105    ///
106    /// Current approximate memory usage in bytes
107    pub fn used(&self) -> usize {
108        self.used.load(Ordering::Relaxed)
109    }
110
111    /// Get memory limit
112    ///
113    /// # Returns
114    ///
115    /// Maximum memory limit in bytes
116    pub fn limit(&self) -> usize {
117        self.limit
118    }
119
120    /// Get remaining memory budget
121    ///
122    /// # Returns
123    ///
124    /// Remaining memory available in bytes
125    pub fn remaining(&self) -> usize {
126        let current = self.used();
127        self.limit.saturating_sub(current)
128    }
129
130    /// Check if budget is exceeded
131    ///
132    /// # Returns
133    ///
134    /// `true` if current usage exceeds limit
135    pub fn is_exceeded(&self) -> bool {
136        self.used() > self.limit
137    }
138
139    /// Reset the budget to zero usage
140    pub fn reset(&self) {
141        self.used.store(0, Ordering::Relaxed);
142    }
143}
144
145/// Memory budget error types
146#[derive(Debug, Clone, thiserror::Error)]
147pub enum MemoryError {
148    /// Memory budget exceeded
149    #[error("Memory budget exceeded: requested {requested} bytes, used {used}/{limit} bytes")]
150    BudgetExceeded {
151        /// Bytes requested
152        requested: usize,
153        /// Bytes already used
154        used: usize,
155        /// Budget limit
156        limit: usize,
157    },
158}
159
160/// Size estimation heuristics for SVG nodes
161pub mod estimator {
162    use crate::ast::{Element, Node};
163
164    /// Estimate size of an element (base overhead + attributes + children)
165    ///
166    /// Heuristic: 200 bytes base + 50 bytes per attribute + key/value lengths
167    pub fn estimate_element(elem: &Element<'_>) -> usize {
168        let mut size = 200; // Base element overhead
169
170        // Attributes: 50 bytes overhead + key + value
171        for (key, value) in &elem.attributes {
172            size += 50 + key.len() + value.len();
173        }
174
175        // Namespaces
176        for (key, value) in &elem.namespaces {
177            size += 50 + key.len() + value.len();
178        }
179
180        // Tag name
181        size += elem.name.len();
182
183        size
184    }
185
186    /// Estimate size of a node
187    pub fn estimate_node(node: &Node<'_>) -> usize {
188        match node {
189            Node::Element(elem) => estimate_element(elem),
190            Node::Text(text) => 40 + text.len(),
191            Node::Comment(comment) => 40 + comment.len(),
192            Node::ProcessingInstruction { target, data } => 40 + target.len() + data.len(),
193            Node::CData(data) => 40 + data.len(),
194            Node::DocType(doctype) => 40 + doctype.len(),
195        }
196    }
197
198    /// Estimate size for adding a child to an element
199    pub fn estimate_child_addition(child: &Node<'_>) -> usize {
200        // Child node + Vec growth overhead
201        estimate_node(child) + 32
202    }
203
204    /// Estimate size for adding an attribute
205    pub fn estimate_attribute_addition(key: &str, value: &str) -> usize {
206        50 + key.len() + value.len()
207    }
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213
214    #[test]
215    fn test_budget_creation() {
216        let budget = MemoryBudget::new(1024);
217        assert_eq!(budget.limit(), 1024);
218        assert_eq!(budget.used(), 0);
219        assert_eq!(budget.remaining(), 1024);
220        assert!(!budget.is_exceeded());
221    }
222
223    #[test]
224    fn test_budget_allocation() {
225        let budget = MemoryBudget::new(1000);
226
227        assert!(budget.allocate(400).is_ok());
228        assert_eq!(budget.used(), 400);
229        assert_eq!(budget.remaining(), 600);
230
231        assert!(budget.allocate(400).is_ok());
232        assert_eq!(budget.used(), 800);
233        assert_eq!(budget.remaining(), 200);
234
235        // This should fail - exceeds budget
236        assert!(budget.allocate(400).is_err());
237        // Usage should not change after failed allocation
238        assert_eq!(budget.used(), 800);
239    }
240
241    #[test]
242    fn test_budget_deallocation() {
243        let budget = MemoryBudget::new(1000);
244
245        budget.allocate(500).unwrap();
246        assert_eq!(budget.used(), 500);
247
248        budget.deallocate(200);
249        assert_eq!(budget.used(), 300);
250        assert_eq!(budget.remaining(), 700);
251    }
252
253    #[test]
254    fn test_budget_exceeded() {
255        let budget = MemoryBudget::new(100);
256
257        budget.allocate(50).unwrap();
258        assert!(!budget.is_exceeded());
259
260        budget.allocate(50).unwrap();
261        assert!(!budget.is_exceeded());
262
263        // Try to exceed
264        let result = budget.allocate(50);
265        assert!(result.is_err());
266
267        if let Err(MemoryError::BudgetExceeded {
268            requested,
269            used,
270            limit,
271        }) = result
272        {
273            assert_eq!(requested, 50);
274            assert_eq!(used, 100);
275            assert_eq!(limit, 100);
276        }
277    }
278
279    #[test]
280    fn test_budget_reset() {
281        let budget = MemoryBudget::new(1000);
282
283        budget.allocate(500).unwrap();
284        assert_eq!(budget.used(), 500);
285
286        budget.reset();
287        assert_eq!(budget.used(), 0);
288        assert_eq!(budget.remaining(), 1000);
289    }
290
291    #[test]
292    fn test_estimator_node() {
293        use crate::ast::Node;
294
295        let text = Node::Text("Hello, World!".to_string().into_boxed_str());
296        let size = estimator::estimate_node(&text);
297        assert_eq!(size, 40 + 13); // 40 base + text length
298
299        let comment = Node::Comment("Test comment".to_string().into_boxed_str());
300        let size = estimator::estimate_node(&comment);
301        assert_eq!(size, 40 + 12);
302    }
303
304    #[test]
305    fn test_estimator_attribute() {
306        let size = estimator::estimate_attribute_addition("width", "100");
307        assert_eq!(size, 50 + 5 + 3); // 50 base + key len + value len
308    }
309}