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#[cfg(debug_assertions)]
10fn capture_stack_trace() -> String {
11 let backtrace = Backtrace::force_capture();
12
13 let trace_str = backtrace.to_string();
15 let lines: Vec<&str> = trace_str.lines().collect();
16
17 let relevant_frames: Vec<&str> = lines
19 .into_iter()
20 .filter(|line| {
21 !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#[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#[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) }
77}
78
79impl MemoryTracker {
80 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 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 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 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 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 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 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
208pub 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 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 let leaks = tracker.detect_leaks();
265 assert_eq!(leaks.len(), 1);
266 assert_eq!(leaks[0].resource_type, "mmap");
267
268 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 let tracker = MemoryTracker::new(0); 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 let (count, _, _) = tracker.get_memory_stats();
306 assert_eq!(count, 0);
307 }
308}