Skip to main content

dataprof_core/
memory_tracker.rs

1use std::collections::HashMap;
2use std::sync::{Arc, Mutex};
3use std::time::{SystemTime, UNIX_EPOCH};
4
5#[cfg(debug_assertions)]
6use std::backtrace::Backtrace;
7
8/// Capture stack trace for debugging memory leaks (debug builds only)
9#[cfg(debug_assertions)]
10fn capture_stack_trace() -> String {
11    let backtrace = Backtrace::force_capture();
12
13    // Filter and format the backtrace to show only relevant frames
14    let trace_str = backtrace.to_string();
15    let lines: Vec<&str> = trace_str.lines().collect();
16
17    // Take up to 10 most relevant frames, skip internal Rust/std frames
18    let relevant_frames: Vec<&str> = lines
19        .into_iter()
20        .filter(|line| {
21            // Skip internal Rust frames and our own memory tracker frames
22            !line.contains("rust_begin_unwind")
23                && !line.contains("rust_panic")
24                && !line.contains("core::panic")
25                && !line.contains("std::panic")
26                && !line.contains("backtrace::capture")
27                && !line.contains("memory_tracker::capture_stack_trace")
28                && !line.contains("memory_tracker::track_allocation")
29        })
30        .take(10)
31        .collect();
32
33    if relevant_frames.is_empty() {
34        "Stack trace not available".to_string()
35    } else {
36        relevant_frames.join("\n")
37    }
38}
39
40/// Placeholder for release builds
41#[cfg(not(debug_assertions))]
42#[allow(dead_code)]
43fn capture_stack_trace() -> String {
44    "Stack trace only available in debug builds".to_string()
45}
46
47/// Memory resource tracker to detect potential leaks
48#[derive(Debug, Clone)]
49pub struct MemoryTracker {
50    allocations: Arc<Mutex<HashMap<String, AllocationInfo>>>,
51    leak_threshold_mb: usize,
52}
53
54#[derive(Debug, Clone)]
55struct AllocationInfo {
56    size_bytes: usize,
57    timestamp: u64,
58    resource_type: String,
59    #[cfg(debug_assertions)]
60    stack_trace: String,
61}
62
63#[derive(Debug)]
64pub struct MemoryLeak {
65    pub resource_id: String,
66    pub size_bytes: usize,
67    pub age_seconds: u64,
68    pub resource_type: String,
69    #[cfg(debug_assertions)]
70    pub stack_trace: String,
71}
72
73impl Default for MemoryTracker {
74    fn default() -> Self {
75        Self::new(100) // Default 100MB threshold
76    }
77}
78
79impl MemoryTracker {
80    /// Create a new memory tracker with leak detection threshold in MB
81    pub fn new(leak_threshold_mb: usize) -> Self {
82        Self {
83            allocations: Arc::new(Mutex::new(HashMap::new())),
84            leak_threshold_mb,
85        }
86    }
87
88    /// Track a memory allocation
89    pub fn track_allocation(&self, resource_id: String, size_bytes: usize, resource_type: &str) {
90        let timestamp = SystemTime::now()
91            .duration_since(UNIX_EPOCH)
92            .unwrap_or_else(|_| std::time::Duration::from_secs(0))
93            .as_secs();
94
95        #[cfg(debug_assertions)]
96        let stack_trace = capture_stack_trace();
97
98        let info = AllocationInfo {
99            size_bytes,
100            timestamp,
101            resource_type: resource_type.to_string(),
102            #[cfg(debug_assertions)]
103            stack_trace,
104        };
105
106        if let Ok(mut allocations) = self.allocations.lock() {
107            allocations.insert(resource_id, info);
108        }
109    }
110
111    /// Mark a resource as deallocated
112    pub fn track_deallocation(&self, resource_id: &str) {
113        if let Ok(mut allocations) = self.allocations.lock() {
114            allocations.remove(resource_id);
115        }
116    }
117
118    /// Check for potential memory leaks
119    pub fn detect_leaks(&self) -> Vec<MemoryLeak> {
120        let current_time = SystemTime::now()
121            .duration_since(UNIX_EPOCH)
122            .unwrap_or_else(|_| std::time::Duration::from_secs(0))
123            .as_secs();
124
125        let threshold_bytes = self.leak_threshold_mb * 1024 * 1024;
126
127        if let Ok(allocations) = self.allocations.lock() {
128            allocations
129                .iter()
130                .filter_map(|(id, info)| {
131                    let age_seconds = current_time - info.timestamp;
132
133                    // Consider it a leak if:
134                    // - Size is above threshold OR
135                    // - Age is more than 60 seconds
136                    if info.size_bytes > threshold_bytes || age_seconds > 60 {
137                        Some(MemoryLeak {
138                            resource_id: id.clone(),
139                            size_bytes: info.size_bytes,
140                            age_seconds,
141                            resource_type: info.resource_type.clone(),
142                            #[cfg(debug_assertions)]
143                            stack_trace: info.stack_trace.clone(),
144                        })
145                    } else {
146                        None
147                    }
148                })
149                .collect()
150        } else {
151            Vec::new()
152        }
153    }
154
155    /// Get memory usage summary
156    pub fn get_memory_stats(&self) -> (usize, usize, usize) {
157        if let Ok(allocations) = self.allocations.lock() {
158            let total_allocations = allocations.len();
159            let total_bytes: usize = allocations.values().map(|info| info.size_bytes).sum();
160            let total_mb = total_bytes / (1024 * 1024);
161
162            (total_allocations, total_bytes, total_mb)
163        } else {
164            (0, 0, 0)
165        }
166    }
167
168    /// Report detected leaks
169    pub fn report_leaks(&self) -> String {
170        let leaks = self.detect_leaks();
171
172        if leaks.is_empty() {
173            "No memory leaks detected.".to_string()
174        } else {
175            let mut report = format!("{} potential memory leak(s) detected:\n\n", leaks.len());
176
177            for leak in leaks {
178                #[cfg(debug_assertions)]
179                {
180                    report.push_str(&format!(
181                        "• Resource: {} ({})\n  Size: {} bytes ({:.2} MB)\n  Age: {}s\n  Stack trace:\n{}\n\n",
182                        leak.resource_id,
183                        leak.resource_type,
184                        leak.size_bytes,
185                        leak.size_bytes as f64 / (1024.0 * 1024.0),
186                        leak.age_seconds,
187                        leak.stack_trace
188                    ));
189                }
190                #[cfg(not(debug_assertions))]
191                {
192                    report.push_str(&format!(
193                        "• Resource: {} ({})\n  Size: {} bytes ({:.2} MB)\n  Age: {}s\n\n",
194                        leak.resource_id,
195                        leak.resource_type,
196                        leak.size_bytes,
197                        leak.size_bytes as f64 / (1024.0 * 1024.0),
198                        leak.age_seconds
199                    ));
200                }
201            }
202
203            report
204        }
205    }
206}
207
208/// RAII wrapper for tracked resources
209pub struct TrackedResource<T> {
210    resource: T,
211    resource_id: String,
212    tracker: MemoryTracker,
213}
214
215impl<T> TrackedResource<T> {
216    pub fn new(
217        resource: T,
218        resource_id: String,
219        size_bytes: usize,
220        resource_type: &str,
221        tracker: MemoryTracker,
222    ) -> Self {
223        tracker.track_allocation(resource_id.clone(), size_bytes, resource_type);
224
225        Self {
226            resource,
227            resource_id,
228            tracker,
229        }
230    }
231
232    pub fn get(&self) -> &T {
233        &self.resource
234    }
235
236    pub fn get_mut(&mut self) -> &mut T {
237        &mut self.resource
238    }
239}
240
241impl<T> Drop for TrackedResource<T> {
242    fn drop(&mut self) {
243        self.tracker.track_deallocation(&self.resource_id);
244    }
245}
246
247#[cfg(test)]
248mod tests {
249    use super::*;
250
251    #[test]
252    fn test_memory_tracking() {
253        let tracker = MemoryTracker::new(50);
254
255        // Track a large allocation
256        tracker.track_allocation("test_mmap_1".to_string(), 100 * 1024 * 1024, "mmap");
257
258        let (count, bytes, mb) = tracker.get_memory_stats();
259        assert_eq!(count, 1);
260        assert_eq!(bytes, 100 * 1024 * 1024);
261        assert_eq!(mb, 100);
262
263        // Should detect leak due to size
264        let leaks = tracker.detect_leaks();
265        assert_eq!(leaks.len(), 1);
266        assert_eq!(leaks[0].resource_type, "mmap");
267
268        // Clean up
269        tracker.track_deallocation("test_mmap_1");
270        let leaks = tracker.detect_leaks();
271        assert_eq!(leaks.len(), 0);
272    }
273
274    #[test]
275    fn test_age_based_leak_detection() {
276        // Use very low threshold so our allocation triggers
277        let tracker = MemoryTracker::new(0); // 0MB threshold
278
279        // Track a small allocation that will be detected as leak
280        tracker.track_allocation("test_small".to_string(), 1024, "buffer");
281
282        let leaks = tracker.detect_leaks();
283        assert_eq!(leaks.len(), 1);
284        assert_eq!(leaks[0].resource_type, "buffer");
285    }
286
287    #[test]
288    fn test_tracked_resource_raii() {
289        let tracker = MemoryTracker::new(50);
290
291        {
292            let _resource = TrackedResource::new(
293                vec![0u8; 1024],
294                "test_vec".to_string(),
295                1024,
296                "vector",
297                tracker.clone(),
298            );
299
300            let (count, _, _) = tracker.get_memory_stats();
301            assert_eq!(count, 1);
302        }
303
304        // Should be automatically deallocated
305        let (count, _, _) = tracker.get_memory_stats();
306        assert_eq!(count, 0);
307    }
308}