Skip to main content

dataprof_core/
memory_tracker.rs

1use std::collections::HashMap;
2use std::sync::{Arc, Mutex};
3use std::time::Instant;
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    /// Monotonic timestamp captured at allocation. Using `Instant` rather than
58    /// `SystemTime` avoids u64 underflow panics if the wall clock jumps
59    /// backward (NTP correction, manual clock changes, suspend/resume) between
60    /// the allocation and the leak-detection scan.
61    created_at: Instant,
62    resource_type: String,
63    #[cfg(debug_assertions)]
64    stack_trace: String,
65}
66
67#[derive(Debug)]
68pub struct MemoryLeak {
69    pub resource_id: String,
70    pub size_bytes: usize,
71    pub age_seconds: u64,
72    pub resource_type: String,
73    #[cfg(debug_assertions)]
74    pub stack_trace: String,
75}
76
77impl Default for MemoryTracker {
78    fn default() -> Self {
79        Self::new(100) // Default 100MB threshold
80    }
81}
82
83impl MemoryTracker {
84    /// Create a new memory tracker with leak detection threshold in MB
85    pub fn new(leak_threshold_mb: usize) -> Self {
86        Self {
87            allocations: Arc::new(Mutex::new(HashMap::new())),
88            leak_threshold_mb,
89        }
90    }
91
92    /// Track a memory allocation
93    pub fn track_allocation(&self, resource_id: String, size_bytes: usize, resource_type: &str) {
94        #[cfg(debug_assertions)]
95        let stack_trace = capture_stack_trace();
96
97        let info = AllocationInfo {
98            size_bytes,
99            created_at: Instant::now(),
100            resource_type: resource_type.to_string(),
101            #[cfg(debug_assertions)]
102            stack_trace,
103        };
104
105        if let Ok(mut allocations) = self.allocations.lock() {
106            allocations.insert(resource_id, info);
107        }
108    }
109
110    /// Mark a resource as deallocated
111    pub fn track_deallocation(&self, resource_id: &str) {
112        if let Ok(mut allocations) = self.allocations.lock() {
113            allocations.remove(resource_id);
114        }
115    }
116
117    /// Check for potential memory leaks
118    pub fn detect_leaks(&self) -> Vec<MemoryLeak> {
119        let threshold_bytes = self.leak_threshold_mb * 1024 * 1024;
120
121        if let Ok(allocations) = self.allocations.lock() {
122            allocations
123                .iter()
124                .filter_map(|(id, info)| {
125                    // Monotonic — cannot underflow even under clock skew.
126                    let age_seconds = info.created_at.elapsed().as_secs();
127
128                    // Consider it a leak if:
129                    // - Size is above threshold OR
130                    // - Age is more than 60 seconds
131                    if info.size_bytes > threshold_bytes || age_seconds > 60 {
132                        Some(MemoryLeak {
133                            resource_id: id.clone(),
134                            size_bytes: info.size_bytes,
135                            age_seconds,
136                            resource_type: info.resource_type.clone(),
137                            #[cfg(debug_assertions)]
138                            stack_trace: info.stack_trace.clone(),
139                        })
140                    } else {
141                        None
142                    }
143                })
144                .collect()
145        } else {
146            Vec::new()
147        }
148    }
149
150    /// Get memory usage summary
151    pub fn get_memory_stats(&self) -> (usize, usize, usize) {
152        if let Ok(allocations) = self.allocations.lock() {
153            let total_allocations = allocations.len();
154            let total_bytes: usize = allocations.values().map(|info| info.size_bytes).sum();
155            let total_mb = total_bytes / (1024 * 1024);
156
157            (total_allocations, total_bytes, total_mb)
158        } else {
159            (0, 0, 0)
160        }
161    }
162
163    /// Report detected leaks
164    pub fn report_leaks(&self) -> String {
165        let leaks = self.detect_leaks();
166
167        if leaks.is_empty() {
168            "No memory leaks detected.".to_string()
169        } else {
170            let mut report = format!("{} potential memory leak(s) detected:\n\n", leaks.len());
171
172            for leak in leaks {
173                #[cfg(debug_assertions)]
174                {
175                    report.push_str(&format!(
176                        "• Resource: {} ({})\n  Size: {} bytes ({:.2} MB)\n  Age: {}s\n  Stack trace:\n{}\n\n",
177                        leak.resource_id,
178                        leak.resource_type,
179                        leak.size_bytes,
180                        leak.size_bytes as f64 / (1024.0 * 1024.0),
181                        leak.age_seconds,
182                        leak.stack_trace
183                    ));
184                }
185                #[cfg(not(debug_assertions))]
186                {
187                    report.push_str(&format!(
188                        "• Resource: {} ({})\n  Size: {} bytes ({:.2} MB)\n  Age: {}s\n\n",
189                        leak.resource_id,
190                        leak.resource_type,
191                        leak.size_bytes,
192                        leak.size_bytes as f64 / (1024.0 * 1024.0),
193                        leak.age_seconds
194                    ));
195                }
196            }
197
198            report
199        }
200    }
201}
202
203/// RAII wrapper for tracked resources
204pub struct TrackedResource<T> {
205    resource: T,
206    resource_id: String,
207    tracker: MemoryTracker,
208}
209
210impl<T> TrackedResource<T> {
211    pub fn new(
212        resource: T,
213        resource_id: String,
214        size_bytes: usize,
215        resource_type: &str,
216        tracker: MemoryTracker,
217    ) -> Self {
218        tracker.track_allocation(resource_id.clone(), size_bytes, resource_type);
219
220        Self {
221            resource,
222            resource_id,
223            tracker,
224        }
225    }
226
227    pub fn get(&self) -> &T {
228        &self.resource
229    }
230
231    pub fn get_mut(&mut self) -> &mut T {
232        &mut self.resource
233    }
234}
235
236impl<T> Drop for TrackedResource<T> {
237    fn drop(&mut self) {
238        self.tracker.track_deallocation(&self.resource_id);
239    }
240}
241
242#[cfg(test)]
243mod tests {
244    use super::*;
245
246    #[test]
247    fn test_memory_tracking() {
248        let tracker = MemoryTracker::new(50);
249
250        // Track a large allocation
251        tracker.track_allocation("test_mmap_1".to_string(), 100 * 1024 * 1024, "mmap");
252
253        let (count, bytes, mb) = tracker.get_memory_stats();
254        assert_eq!(count, 1);
255        assert_eq!(bytes, 100 * 1024 * 1024);
256        assert_eq!(mb, 100);
257
258        // Should detect leak due to size
259        let leaks = tracker.detect_leaks();
260        assert_eq!(leaks.len(), 1);
261        assert_eq!(leaks[0].resource_type, "mmap");
262
263        // Clean up
264        tracker.track_deallocation("test_mmap_1");
265        let leaks = tracker.detect_leaks();
266        assert_eq!(leaks.len(), 0);
267    }
268
269    #[test]
270    fn test_age_based_leak_detection() {
271        // Use very low threshold so our allocation triggers
272        let tracker = MemoryTracker::new(0); // 0MB threshold
273
274        // Track a small allocation that will be detected as leak
275        tracker.track_allocation("test_small".to_string(), 1024, "buffer");
276
277        let leaks = tracker.detect_leaks();
278        assert_eq!(leaks.len(), 1);
279        assert_eq!(leaks[0].resource_type, "buffer");
280    }
281
282    #[test]
283    fn test_tracked_resource_raii() {
284        let tracker = MemoryTracker::new(50);
285
286        {
287            let _resource = TrackedResource::new(
288                vec![0u8; 1024],
289                "test_vec".to_string(),
290                1024,
291                "vector",
292                tracker.clone(),
293            );
294
295            let (count, _, _) = tracker.get_memory_stats();
296            assert_eq!(count, 1);
297        }
298
299        // Should be automatically deallocated
300        let (count, _, _) = tracker.get_memory_stats();
301        assert_eq!(count, 0);
302    }
303}