1use std::collections::HashMap;
2use std::sync::{Arc, Mutex};
3use std::time::Instant;
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 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) }
81}
82
83impl MemoryTracker {
84 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 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 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 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 let age_seconds = info.created_at.elapsed().as_secs();
127
128 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 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 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
203pub 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 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 let leaks = tracker.detect_leaks();
260 assert_eq!(leaks.len(), 1);
261 assert_eq!(leaks[0].resource_type, "mmap");
262
263 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 let tracker = MemoryTracker::new(0); 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 let (count, _, _) = tracker.get_memory_stats();
301 assert_eq!(count, 0);
302 }
303}