Skip to main content

tenflowers_core/memory/
diagnostics.rs

1//! GPU Memory Diagnostics and Allocation Tracing
2//!
3//! This module provides comprehensive memory allocation tracking, diagnostics,
4//! and reporting for GPU memory management.
5
6use std::collections::{HashMap, VecDeque};
7use std::sync::{Arc, Mutex};
8use std::time::{Duration, Instant};
9
10use crate::Result;
11
12/// Detailed allocation event for tracing
13#[derive(Debug, Clone)]
14pub struct AllocationEvent {
15    /// Unique allocation ID
16    pub id: u64,
17    /// Timestamp when allocated
18    pub timestamp: Instant,
19    /// Requested size in bytes
20    pub requested_size: usize,
21    /// Actual allocated size in bytes (may be larger due to alignment)
22    pub allocated_size: usize,
23    /// Allocation alignment requirement
24    pub alignment: usize,
25    /// Stack trace or allocation source (for debugging)
26    pub source: Option<String>,
27    /// Device ID where allocated
28    pub device_id: usize,
29    /// Pool ID within device
30    pub pool_id: Option<usize>,
31    /// Whether this is still active
32    pub is_active: bool,
33    /// When deallocated (if applicable)
34    pub deallocated_at: Option<Instant>,
35    /// Lifetime in microseconds (if deallocated)
36    pub lifetime_us: Option<u64>,
37}
38
39impl AllocationEvent {
40    /// Create a new allocation event
41    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    /// Mark allocation as deallocated
65    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    /// Get lifetime duration
73    pub fn lifetime(&self) -> Option<Duration> {
74        self.deallocated_at
75            .map(|dealloc| dealloc.duration_since(self.timestamp))
76    }
77}
78
79/// Memory leak detection report
80#[derive(Debug, Clone)]
81pub struct LeakReport {
82    /// Number of leaked allocations
83    pub leak_count: usize,
84    /// Total leaked memory in bytes
85    pub leaked_bytes: usize,
86    /// Individual leak details
87    pub leaks: Vec<AllocationEvent>,
88    /// Oldest leak age
89    pub oldest_leak_age: Option<Duration>,
90    /// Average leak size
91    pub average_leak_size: f64,
92}
93
94/// Allocation statistics summary
95#[derive(Debug, Clone)]
96pub struct AllocationStats {
97    /// Total number of allocations
98    pub total_allocations: u64,
99    /// Total number of deallocations
100    pub total_deallocations: u64,
101    /// Currently active allocations
102    pub active_allocations: usize,
103    /// Current memory usage in bytes
104    pub current_usage: usize,
105    /// Peak memory usage in bytes
106    pub peak_usage: usize,
107    /// Total bytes ever allocated
108    pub total_allocated: usize,
109    /// Total bytes deallocated
110    pub total_deallocated: usize,
111    /// Average allocation size
112    pub average_allocation_size: f64,
113    /// Average lifetime in microseconds
114    pub average_lifetime_us: f64,
115    /// Allocation rate (allocs/second)
116    pub allocation_rate: f64,
117    /// Deallocation rate (deallocs/second)
118    pub deallocation_rate: f64,
119    /// Fragmentation score (0.0 = no fragmentation, 1.0 = severe)
120    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/// Allocation size distribution bucket
143#[derive(Debug, Clone)]
144pub struct SizeBucket {
145    /// Minimum size for this bucket (inclusive)
146    pub min_size: usize,
147    /// Maximum size for this bucket (exclusive)
148    pub max_size: usize,
149    /// Number of allocations in this bucket
150    pub count: usize,
151    /// Total bytes in this bucket
152    pub total_bytes: usize,
153}
154
155/// GPU Memory Diagnostics Tracker
156pub struct MemoryDiagnostics {
157    /// All allocation events (both active and historical)
158    events: Arc<Mutex<Vec<AllocationEvent>>>,
159    /// Active allocations indexed by ID
160    active: Arc<Mutex<HashMap<u64, usize>>>,
161    /// Next allocation ID
162    next_id: Arc<Mutex<u64>>,
163    /// Start time for rate calculations
164    start_time: Instant,
165    /// Diagnostic session metadata
166    session_metadata: Arc<Mutex<HashMap<String, String>>>,
167    /// Maximum events to keep in history
168    max_history_size: usize,
169    /// Enable detailed tracing
170    detailed_tracing: bool,
171}
172
173impl MemoryDiagnostics {
174    /// Create a new diagnostics tracker
175    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    /// Create diagnostics with custom configuration
188    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    /// Record a new allocation
201    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    /// Record a deallocation
244    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    /// Get current allocation statistics
263    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        // Calculate rates
311        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        // Estimate fragmentation (simplified metric)
318        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    /// Detect memory leaks (allocations alive longer than threshold)
327    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    /// Get allocation size distribution
369    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        // Define size buckets (in bytes)
376        let bucket_ranges = [
377            (0, 1024),                       // < 1KB
378            (1024, 16 * 1024),               // 1KB - 16KB
379            (16 * 1024, 256 * 1024),         // 16KB - 256KB
380            (256 * 1024, 1024 * 1024),       // 256KB - 1MB
381            (1024 * 1024, 16 * 1024 * 1024), // 1MB - 16MB
382            (16 * 1024 * 1024, usize::MAX),  // > 16MB
383        ];
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    /// Get allocation timeline (for visualization)
410    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)); // (timestamp, allocations, deallocations)
430        }
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    /// Clear old events to manage memory
454    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        // Enforce max history size
470        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    /// Set session metadata
477    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    /// Get session metadata
486    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    /// Reset all diagnostics
495    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    /// Generate a diagnostic report
518    pub fn generate_report(&self) -> String {
519        let stats = self.get_statistics();
520        let leaks = self.detect_leaks(Duration::from_secs(300)); // 5 minute threshold
521        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        // Statistics
527        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        // Leak detection
562        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        // Size distribution
582        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
611/// Global diagnostics instance for easy access
612static GLOBAL_DIAGNOSTICS: once_cell::sync::Lazy<MemoryDiagnostics> =
613    once_cell::sync::Lazy::new(|| MemoryDiagnostics::new());
614
615/// Get the global diagnostics instance
616pub 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        // Immediate check should show no leaks
657        let leaks = diag.detect_leaks(Duration::from_secs(1));
658        assert_eq!(leaks.leak_count, 0);
659
660        // After waiting, should detect leak
661        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); // < 1KB
672        diag.record_allocation(2048, 2048, 16, 0, None); // 1-16KB
673        diag.record_allocation(100000, 100000, 16, 0, None); // 16-256KB
674
675        let distribution = diag.get_size_distribution();
676
677        assert_eq!(distribution[0].count, 1); // < 1KB
678        assert_eq!(distribution[1].count, 1); // 1-16KB
679        assert_eq!(distribution[2].count, 1); // 16-256KB
680    }
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}