1use std::collections::{HashMap, VecDeque};
7use std::sync::{Arc, Mutex};
8use std::time::{Duration, Instant};
9
10use crate::Result;
11
12#[derive(Debug, Clone)]
14pub struct AllocationEvent {
15 pub id: u64,
17 pub timestamp: Instant,
19 pub requested_size: usize,
21 pub allocated_size: usize,
23 pub alignment: usize,
25 pub source: Option<String>,
27 pub device_id: usize,
29 pub pool_id: Option<usize>,
31 pub is_active: bool,
33 pub deallocated_at: Option<Instant>,
35 pub lifetime_us: Option<u64>,
37}
38
39impl AllocationEvent {
40 pub fn new(
42 id: u64,
43 requested_size: usize,
44 allocated_size: usize,
45 alignment: usize,
46 device_id: usize,
47 source: Option<String>,
48 ) -> Self {
49 Self {
50 id,
51 timestamp: Instant::now(),
52 requested_size,
53 allocated_size,
54 alignment,
55 source,
56 device_id,
57 pool_id: None,
58 is_active: true,
59 deallocated_at: None,
60 lifetime_us: None,
61 }
62 }
63
64 pub fn mark_deallocated(&mut self) {
66 let now = Instant::now();
67 self.deallocated_at = Some(now);
68 self.lifetime_us = Some(now.duration_since(self.timestamp).as_micros() as u64);
69 self.is_active = false;
70 }
71
72 pub fn lifetime(&self) -> Option<Duration> {
74 self.deallocated_at
75 .map(|dealloc| dealloc.duration_since(self.timestamp))
76 }
77}
78
79#[derive(Debug, Clone)]
81pub struct LeakReport {
82 pub leak_count: usize,
84 pub leaked_bytes: usize,
86 pub leaks: Vec<AllocationEvent>,
88 pub oldest_leak_age: Option<Duration>,
90 pub average_leak_size: f64,
92}
93
94#[derive(Debug, Clone)]
96pub struct AllocationStats {
97 pub total_allocations: u64,
99 pub total_deallocations: u64,
101 pub active_allocations: usize,
103 pub current_usage: usize,
105 pub peak_usage: usize,
107 pub total_allocated: usize,
109 pub total_deallocated: usize,
111 pub average_allocation_size: f64,
113 pub average_lifetime_us: f64,
115 pub allocation_rate: f64,
117 pub deallocation_rate: f64,
119 pub fragmentation_score: f64,
121}
122
123impl Default for AllocationStats {
124 fn default() -> Self {
125 Self {
126 total_allocations: 0,
127 total_deallocations: 0,
128 active_allocations: 0,
129 current_usage: 0,
130 peak_usage: 0,
131 total_allocated: 0,
132 total_deallocated: 0,
133 average_allocation_size: 0.0,
134 average_lifetime_us: 0.0,
135 allocation_rate: 0.0,
136 deallocation_rate: 0.0,
137 fragmentation_score: 0.0,
138 }
139 }
140}
141
142#[derive(Debug, Clone)]
144pub struct SizeBucket {
145 pub min_size: usize,
147 pub max_size: usize,
149 pub count: usize,
151 pub total_bytes: usize,
153}
154
155pub struct MemoryDiagnostics {
157 events: Arc<Mutex<Vec<AllocationEvent>>>,
159 active: Arc<Mutex<HashMap<u64, usize>>>,
161 next_id: Arc<Mutex<u64>>,
163 start_time: Instant,
165 session_metadata: Arc<Mutex<HashMap<String, String>>>,
167 max_history_size: usize,
169 detailed_tracing: bool,
171}
172
173impl MemoryDiagnostics {
174 pub fn new() -> Self {
176 Self {
177 events: Arc::new(Mutex::new(Vec::new())),
178 active: Arc::new(Mutex::new(HashMap::new())),
179 next_id: Arc::new(Mutex::new(0)),
180 start_time: Instant::now(),
181 session_metadata: Arc::new(Mutex::new(HashMap::new())),
182 max_history_size: 100_000,
183 detailed_tracing: false,
184 }
185 }
186
187 pub fn with_config(max_history_size: usize, detailed_tracing: bool) -> Self {
189 Self {
190 events: Arc::new(Mutex::new(Vec::new())),
191 active: Arc::new(Mutex::new(HashMap::new())),
192 next_id: Arc::new(Mutex::new(0)),
193 start_time: Instant::now(),
194 session_metadata: Arc::new(Mutex::new(HashMap::new())),
195 max_history_size,
196 detailed_tracing,
197 }
198 }
199
200 pub fn record_allocation(
202 &self,
203 requested_size: usize,
204 allocated_size: usize,
205 alignment: usize,
206 device_id: usize,
207 source: Option<String>,
208 ) -> u64 {
209 let mut next_id = self
210 .next_id
211 .lock()
212 .unwrap_or_else(|poisoned| poisoned.into_inner());
213 let id = *next_id;
214 *next_id += 1;
215 drop(next_id);
216
217 let event = AllocationEvent::new(
218 id,
219 requested_size,
220 allocated_size,
221 alignment,
222 device_id,
223 source,
224 );
225
226 let mut events = self
227 .events
228 .lock()
229 .unwrap_or_else(|poisoned| poisoned.into_inner());
230 let event_idx = events.len();
231 events.push(event);
232 drop(events);
233
234 let mut active = self
235 .active
236 .lock()
237 .unwrap_or_else(|poisoned| poisoned.into_inner());
238 active.insert(id, event_idx);
239
240 id
241 }
242
243 pub fn record_deallocation(&self, id: u64) {
245 let mut active = self
246 .active
247 .lock()
248 .unwrap_or_else(|poisoned| poisoned.into_inner());
249 if let Some(event_idx) = active.remove(&id) {
250 drop(active);
251
252 let mut events = self
253 .events
254 .lock()
255 .unwrap_or_else(|poisoned| poisoned.into_inner());
256 if let Some(event) = events.get_mut(event_idx) {
257 event.mark_deallocated();
258 }
259 }
260 }
261
262 pub fn get_statistics(&self) -> AllocationStats {
264 let events = self
265 .events
266 .lock()
267 .unwrap_or_else(|poisoned| poisoned.into_inner());
268 let active = self
269 .active
270 .lock()
271 .unwrap_or_else(|poisoned| poisoned.into_inner());
272
273 let mut stats = AllocationStats {
274 active_allocations: active.len(),
275 ..Default::default()
276 };
277
278 let mut current_usage = 0usize;
279 let mut total_lifetime_us = 0u64;
280 let mut lifetime_count = 0usize;
281
282 for event in events.iter() {
283 stats.total_allocations += 1;
284 stats.total_allocated += event.allocated_size;
285
286 if event.is_active {
287 current_usage += event.allocated_size;
288 } else {
289 stats.total_deallocations += 1;
290 stats.total_deallocated += event.allocated_size;
291 if let Some(lifetime) = event.lifetime_us {
292 total_lifetime_us += lifetime;
293 lifetime_count += 1;
294 }
295 }
296 }
297
298 stats.current_usage = current_usage;
299 stats.peak_usage = stats.peak_usage.max(current_usage);
300
301 if stats.total_allocations > 0 {
302 stats.average_allocation_size =
303 stats.total_allocated as f64 / stats.total_allocations as f64;
304 }
305
306 if lifetime_count > 0 {
307 stats.average_lifetime_us = total_lifetime_us as f64 / lifetime_count as f64;
308 }
309
310 let elapsed = self.start_time.elapsed().as_secs_f64();
312 if elapsed > 0.0 {
313 stats.allocation_rate = stats.total_allocations as f64 / elapsed;
314 stats.deallocation_rate = stats.total_deallocations as f64 / elapsed;
315 }
316
317 if stats.active_allocations > 0 {
319 stats.fragmentation_score =
320 1.0 - (stats.current_usage as f64 / stats.peak_usage as f64).min(1.0);
321 }
322
323 stats
324 }
325
326 pub fn detect_leaks(&self, age_threshold: Duration) -> LeakReport {
328 let events = self
329 .events
330 .lock()
331 .unwrap_or_else(|poisoned| poisoned.into_inner());
332 let now = Instant::now();
333
334 let mut leaks = Vec::new();
335 let mut leaked_bytes = 0usize;
336 let mut oldest_leak_age = None;
337
338 for event in events.iter() {
339 if event.is_active {
340 let age = now.duration_since(event.timestamp);
341 if age > age_threshold {
342 leaked_bytes += event.allocated_size;
343 oldest_leak_age = Some(
344 oldest_leak_age
345 .map(|current: Duration| current.max(age))
346 .unwrap_or(age),
347 );
348 leaks.push(event.clone());
349 }
350 }
351 }
352
353 let average_leak_size = if !leaks.is_empty() {
354 leaked_bytes as f64 / leaks.len() as f64
355 } else {
356 0.0
357 };
358
359 LeakReport {
360 leak_count: leaks.len(),
361 leaked_bytes,
362 leaks,
363 oldest_leak_age,
364 average_leak_size,
365 }
366 }
367
368 pub fn get_size_distribution(&self) -> Vec<SizeBucket> {
370 let events = self
371 .events
372 .lock()
373 .unwrap_or_else(|poisoned| poisoned.into_inner());
374
375 let bucket_ranges = [
377 (0, 1024), (1024, 16 * 1024), (16 * 1024, 256 * 1024), (256 * 1024, 1024 * 1024), (1024 * 1024, 16 * 1024 * 1024), (16 * 1024 * 1024, usize::MAX), ];
384
385 let mut buckets: Vec<SizeBucket> = bucket_ranges
386 .iter()
387 .map(|&(min, max)| SizeBucket {
388 min_size: min,
389 max_size: max,
390 count: 0,
391 total_bytes: 0,
392 })
393 .collect();
394
395 for event in events.iter().filter(|e| e.is_active) {
396 for bucket in buckets.iter_mut() {
397 if event.allocated_size >= bucket.min_size && event.allocated_size < bucket.max_size
398 {
399 bucket.count += 1;
400 bucket.total_bytes += event.allocated_size;
401 break;
402 }
403 }
404 }
405
406 buckets
407 }
408
409 pub fn get_timeline(&self, bucket_duration: Duration) -> Vec<(Instant, usize, usize)> {
411 let events = self
412 .events
413 .lock()
414 .unwrap_or_else(|poisoned| poisoned.into_inner());
415
416 if events.is_empty() {
417 return Vec::new();
418 }
419
420 let start_time = events[0].timestamp;
421 let end_time = Instant::now();
422 let total_duration = end_time.duration_since(start_time);
423 let num_buckets =
424 (total_duration.as_secs_f64() / bucket_duration.as_secs_f64()).ceil() as usize;
425
426 let mut timeline = Vec::with_capacity(num_buckets);
427 for i in 0..num_buckets {
428 let bucket_start = start_time + bucket_duration * i as u32;
429 timeline.push((bucket_start, 0, 0)); }
431
432 for event in events.iter() {
433 let alloc_offset = event.timestamp.duration_since(start_time);
434 let alloc_bucket =
435 (alloc_offset.as_secs_f64() / bucket_duration.as_secs_f64()) as usize;
436 if alloc_bucket < timeline.len() {
437 timeline[alloc_bucket].1 += 1;
438 }
439
440 if let Some(dealloc_time) = event.deallocated_at {
441 let dealloc_offset = dealloc_time.duration_since(start_time);
442 let dealloc_bucket =
443 (dealloc_offset.as_secs_f64() / bucket_duration.as_secs_f64()) as usize;
444 if dealloc_bucket < timeline.len() {
445 timeline[dealloc_bucket].2 += 1;
446 }
447 }
448 }
449
450 timeline
451 }
452
453 pub fn cleanup_old_events(&self, retention_duration: Duration) {
455 let mut events = self
456 .events
457 .lock()
458 .unwrap_or_else(|poisoned| poisoned.into_inner());
459 let now = Instant::now();
460
461 events.retain(|event| {
462 event.is_active
463 || event
464 .deallocated_at
465 .map(|dealloc| now.duration_since(dealloc) < retention_duration)
466 .unwrap_or(false)
467 });
468
469 if events.len() > self.max_history_size {
471 let excess = events.len() - self.max_history_size;
472 events.drain(0..excess);
473 }
474 }
475
476 pub fn set_metadata(&self, key: String, value: String) {
478 let mut metadata = self
479 .session_metadata
480 .lock()
481 .unwrap_or_else(|poisoned| poisoned.into_inner());
482 metadata.insert(key, value);
483 }
484
485 pub fn get_metadata(&self, key: &str) -> Option<String> {
487 let metadata = self
488 .session_metadata
489 .lock()
490 .unwrap_or_else(|poisoned| poisoned.into_inner());
491 metadata.get(key).cloned()
492 }
493
494 pub fn reset(&self) {
496 let mut events = self
497 .events
498 .lock()
499 .unwrap_or_else(|poisoned| poisoned.into_inner());
500 events.clear();
501 drop(events);
502
503 let mut active = self
504 .active
505 .lock()
506 .unwrap_or_else(|poisoned| poisoned.into_inner());
507 active.clear();
508 drop(active);
509
510 let mut next_id = self
511 .next_id
512 .lock()
513 .unwrap_or_else(|poisoned| poisoned.into_inner());
514 *next_id = 0;
515 }
516
517 pub fn generate_report(&self) -> String {
519 let stats = self.get_statistics();
520 let leaks = self.detect_leaks(Duration::from_secs(300)); let distribution = self.get_size_distribution();
522
523 let mut report = String::new();
524 report.push_str("=== GPU Memory Diagnostics Report ===\n\n");
525
526 report.push_str(&format!("Total Allocations: {}\n", stats.total_allocations));
528 report.push_str(&format!(
529 "Total Deallocations: {}\n",
530 stats.total_deallocations
531 ));
532 report.push_str(&format!(
533 "Active Allocations: {}\n",
534 stats.active_allocations
535 ));
536 report.push_str(&format!(
537 "Current Usage: {:.2} MB\n",
538 stats.current_usage as f64 / 1_048_576.0
539 ));
540 report.push_str(&format!(
541 "Peak Usage: {:.2} MB\n",
542 stats.peak_usage as f64 / 1_048_576.0
543 ));
544 report.push_str(&format!(
545 "Average Allocation Size: {:.2} KB\n",
546 stats.average_allocation_size / 1024.0
547 ));
548 report.push_str(&format!(
549 "Average Lifetime: {:.2} ms\n",
550 stats.average_lifetime_us / 1000.0
551 ));
552 report.push_str(&format!(
553 "Allocation Rate: {:.2} allocs/sec\n",
554 stats.allocation_rate
555 ));
556 report.push_str(&format!(
557 "Fragmentation Score: {:.2}\n\n",
558 stats.fragmentation_score
559 ));
560
561 report.push_str(&format!("Detected Leaks: {}\n", leaks.leak_count));
563 if leaks.leak_count > 0 {
564 report.push_str(&format!(
565 "Leaked Memory: {:.2} MB\n",
566 leaks.leaked_bytes as f64 / 1_048_576.0
567 ));
568 report.push_str(&format!(
569 "Average Leak Size: {:.2} KB\n",
570 leaks.average_leak_size / 1024.0
571 ));
572 if let Some(age) = leaks.oldest_leak_age {
573 report.push_str(&format!(
574 "Oldest Leak Age: {:.2} seconds\n",
575 age.as_secs_f64()
576 ));
577 }
578 }
579 report.push('\n');
580
581 report.push_str("Size Distribution:\n");
583 for bucket in distribution.iter() {
584 if bucket.count > 0 {
585 let min_kb = bucket.min_size / 1024;
586 let max_kb = if bucket.max_size == usize::MAX {
587 String::from("∞")
588 } else {
589 format!("{}", bucket.max_size / 1024)
590 };
591 report.push_str(&format!(
592 " {} KB - {} KB: {} allocations ({:.2} MB)\n",
593 min_kb,
594 max_kb,
595 bucket.count,
596 bucket.total_bytes as f64 / 1_048_576.0
597 ));
598 }
599 }
600
601 report
602 }
603}
604
605impl Default for MemoryDiagnostics {
606 fn default() -> Self {
607 Self::new()
608 }
609}
610
611static GLOBAL_DIAGNOSTICS: once_cell::sync::Lazy<MemoryDiagnostics> =
613 once_cell::sync::Lazy::new(|| MemoryDiagnostics::new());
614
615pub fn global_diagnostics() -> &'static MemoryDiagnostics {
617 &GLOBAL_DIAGNOSTICS
618}
619
620#[cfg(test)]
621mod tests {
622 use super::*;
623
624 #[test]
625 fn test_allocation_tracking() {
626 let diag = MemoryDiagnostics::new();
627
628 let id1 = diag.record_allocation(1024, 1024, 16, 0, Some("test1".to_string()));
629 let id2 = diag.record_allocation(2048, 2048, 16, 0, Some("test2".to_string()));
630
631 let stats = diag.get_statistics();
632 assert_eq!(stats.total_allocations, 2);
633 assert_eq!(stats.active_allocations, 2);
634 assert_eq!(stats.current_usage, 3072);
635
636 diag.record_deallocation(id1);
637
638 let stats = diag.get_statistics();
639 assert_eq!(stats.total_deallocations, 1);
640 assert_eq!(stats.active_allocations, 1);
641 assert_eq!(stats.current_usage, 2048);
642
643 diag.record_deallocation(id2);
644
645 let stats = diag.get_statistics();
646 assert_eq!(stats.active_allocations, 0);
647 assert_eq!(stats.current_usage, 0);
648 }
649
650 #[test]
651 fn test_leak_detection() {
652 let diag = MemoryDiagnostics::new();
653
654 let _id1 = diag.record_allocation(1024, 1024, 16, 0, Some("leak_test".to_string()));
655
656 let leaks = diag.detect_leaks(Duration::from_secs(1));
658 assert_eq!(leaks.leak_count, 0);
659
660 std::thread::sleep(Duration::from_millis(100));
662 let leaks = diag.detect_leaks(Duration::from_millis(50));
663 assert_eq!(leaks.leak_count, 1);
664 assert_eq!(leaks.leaked_bytes, 1024);
665 }
666
667 #[test]
668 fn test_size_distribution() {
669 let diag = MemoryDiagnostics::new();
670
671 diag.record_allocation(512, 512, 16, 0, None); diag.record_allocation(2048, 2048, 16, 0, None); diag.record_allocation(100000, 100000, 16, 0, None); let distribution = diag.get_size_distribution();
676
677 assert_eq!(distribution[0].count, 1); assert_eq!(distribution[1].count, 1); assert_eq!(distribution[2].count, 1); }
681
682 #[test]
683 fn test_statistics_calculation() {
684 let diag = MemoryDiagnostics::new();
685
686 let id1 = diag.record_allocation(1000, 1024, 16, 0, None);
687 let id2 = diag.record_allocation(2000, 2048, 16, 0, None);
688
689 std::thread::sleep(Duration::from_millis(10));
690 diag.record_deallocation(id1);
691
692 let stats = diag.get_statistics();
693 assert_eq!(stats.total_allocations, 2);
694 assert_eq!(stats.total_deallocations, 1);
695 assert_eq!(stats.active_allocations, 1);
696 assert!(stats.average_allocation_size > 0.0);
697 assert!(stats.average_lifetime_us > 0.0);
698
699 diag.record_deallocation(id2);
700 }
701
702 #[test]
703 fn test_reset() {
704 let diag = MemoryDiagnostics::new();
705
706 diag.record_allocation(1024, 1024, 16, 0, None);
707 diag.record_allocation(2048, 2048, 16, 0, None);
708
709 let stats = diag.get_statistics();
710 assert_eq!(stats.total_allocations, 2);
711
712 diag.reset();
713
714 let stats = diag.get_statistics();
715 assert_eq!(stats.total_allocations, 0);
716 assert_eq!(stats.active_allocations, 0);
717 }
718
719 #[test]
720 fn test_report_generation() {
721 let diag = MemoryDiagnostics::new();
722
723 diag.record_allocation(1024, 1024, 16, 0, Some("test".to_string()));
724 diag.record_allocation(2048, 2048, 16, 0, None);
725
726 let report = diag.generate_report();
727 assert!(report.contains("GPU Memory Diagnostics Report"));
728 assert!(report.contains("Total Allocations: 2"));
729 assert!(report.contains("Active Allocations: 2"));
730 }
731}