1use anyhow::Result;
31use serde::{Deserialize, Serialize};
32use std::collections::{HashMap, VecDeque};
33use std::sync::atomic::{AtomicU64, Ordering};
34use std::sync::{Arc, Mutex};
35use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
36use tokio::time::interval;
37use uuid::Uuid;
38
39const MAX_TIMELINE_SNAPSHOTS: usize = 10_000;
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct MemoryProfilingConfig {
46 pub enable_heap_tracking: bool,
48 pub enable_leak_detection: bool,
50 pub enable_pattern_analysis: bool,
52 pub enable_fragmentation_monitoring: bool,
54 pub enable_gc_pressure_analysis: bool,
56 pub sampling_interval_ms: u64,
58 pub max_allocation_records: usize,
60 pub large_allocation_threshold: usize,
62 pub pattern_analysis_window_secs: u64,
64 pub leak_detection_threshold_secs: u64,
66}
67
68impl Default for MemoryProfilingConfig {
69 fn default() -> Self {
70 Self {
71 enable_heap_tracking: true,
72 enable_leak_detection: true,
73 enable_pattern_analysis: true,
74 enable_fragmentation_monitoring: true,
75 enable_gc_pressure_analysis: true,
76 sampling_interval_ms: 100, max_allocation_records: 100000,
78 large_allocation_threshold: 1024 * 1024, pattern_analysis_window_secs: 60, leak_detection_threshold_secs: 300, }
82 }
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct AllocationRecord {
88 pub id: Uuid,
89 pub size: usize,
90 pub timestamp: SystemTime,
91 pub stack_trace: Vec<String>,
92 pub allocation_type: AllocationType,
93 pub freed: bool,
94 pub freed_at: Option<SystemTime>,
95 pub tags: Vec<String>, }
97
98#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
100pub enum AllocationType {
101 Tensor,
102 Buffer,
103 Weights,
104 Gradients,
105 Activations,
106 Cache,
107 Temporary,
108 Other(String),
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct MemorySnapshot {
114 pub timestamp: SystemTime,
115 pub total_heap_bytes: usize,
116 pub used_heap_bytes: usize,
117 pub free_heap_bytes: usize,
118 pub peak_heap_bytes: usize,
119 pub allocation_count: usize,
120 pub free_count: usize,
121 pub fragmentation_ratio: f64,
122 pub gc_pressure_score: f64,
123 pub allocations_by_type: HashMap<AllocationType, usize>,
124 pub allocations_by_size: HashMap<String, usize>, }
126
127#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct MemoryLeak {
130 pub allocation_id: Uuid,
131 pub size: usize,
132 pub age_seconds: f64,
133 pub timestamp: SystemTime,
139 pub allocation_type: AllocationType,
140 pub stack_trace: Vec<String>,
141 pub tags: Vec<String>,
142 pub severity: LeakSeverity,
143}
144
145#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
147pub enum LeakSeverity {
148 Low, Medium, High, Critical, }
153
154#[derive(Debug, Clone, Serialize, Deserialize)]
156pub struct AllocationPattern {
157 pub pattern_type: PatternType,
158 pub description: String,
159 pub confidence: f64, pub impact_score: f64, pub recommendations: Vec<String>,
162 pub examples: Vec<AllocationRecord>,
163}
164
165const SHORT_LIVED_THRESHOLD: Duration = Duration::from_secs(1);
168
169#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
171pub enum PatternType {
172 MemoryLeak, ChurningAllocations, FragmentationCausing, LargeAllocations, UnbalancedTypes, PeakUsageSpikes, }
179
180#[derive(Debug, Clone, Serialize, Deserialize)]
182pub struct FragmentationAnalysis {
183 pub fragmentation_ratio: f64,
184 pub largest_free_block: usize,
185 pub total_free_memory: usize,
186 pub free_block_count: usize,
187 pub average_free_block_size: f64,
188 pub fragmentation_severity: FragmentationSeverity,
189 pub recommendations: Vec<String>,
190}
191
192#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
194pub enum FragmentationSeverity {
195 Low, Medium, High, Severe, }
200
201#[derive(Debug, Clone, Serialize, Deserialize)]
203pub struct GCPressureAnalysis {
204 pub pressure_score: f64, pub allocation_rate: f64, pub deallocation_rate: f64, pub churn_rate: f64, pub pressure_level: GCPressureLevel,
209 pub contributing_factors: Vec<String>,
210 pub recommendations: Vec<String>,
211}
212
213#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
215pub enum GCPressureLevel {
216 Low,
217 Medium,
218 High,
219 Critical,
220}
221
222#[derive(Debug, Clone, Serialize, Deserialize)]
224pub struct MemoryProfilingReport {
225 pub session_id: Uuid,
226 pub start_time: SystemTime,
227 pub end_time: SystemTime,
228 pub duration_secs: f64,
229 pub config: MemoryProfilingConfig,
230
231 pub peak_memory_mb: f64,
233 pub average_memory_mb: f64,
234 pub total_allocations: usize,
235 pub total_deallocations: usize,
236 pub net_allocations: i64,
237
238 pub memory_timeline: Vec<MemorySnapshot>,
240
241 pub potential_leaks: Vec<MemoryLeak>,
243 pub leak_summary: HashMap<AllocationType, usize>,
244
245 pub detected_patterns: Vec<AllocationPattern>,
247
248 pub fragmentation_analysis: FragmentationAnalysis,
250
251 pub gc_pressure_analysis: GCPressureAnalysis,
253
254 pub allocations_by_type: HashMap<AllocationType, AllocationTypeStats>,
256 pub allocations_by_size_bucket: HashMap<String, usize>,
257
258 pub profiling_overhead_ms: f64,
260 pub sampling_accuracy: Option<f64>,
265}
266
267#[derive(Debug, Clone, Serialize, Deserialize)]
269pub struct AllocationTypeStats {
270 pub total_allocations: usize,
271 pub total_deallocations: usize,
272 pub current_count: usize,
273 pub total_bytes_allocated: usize,
274 pub total_bytes_deallocated: usize,
275 pub current_bytes: usize,
276 pub peak_count: usize,
277 pub peak_bytes: usize,
278 pub average_allocation_size: f64,
279 pub largest_allocation: usize,
280}
281
282#[derive(Debug)]
284pub struct MemoryProfiler {
285 config: MemoryProfilingConfig,
286 session_id: Uuid,
287 start_time: Option<Instant>,
288 allocations: Arc<Mutex<HashMap<Uuid, AllocationRecord>>>,
289 memory_timeline: Arc<Mutex<VecDeque<MemorySnapshot>>>,
290 type_stats: Arc<Mutex<HashMap<AllocationType, AllocationTypeStats>>>,
291 running: Arc<Mutex<bool>>,
292 profiling_start_time: Option<Instant>,
293 samples_taken: Arc<AtomicU64>,
297}
298
299impl MemoryProfiler {
300 pub fn new(config: MemoryProfilingConfig) -> Self {
302 Self {
303 config,
304 session_id: Uuid::new_v4(),
305 start_time: None,
306 allocations: Arc::new(Mutex::new(HashMap::new())),
307 memory_timeline: Arc::new(Mutex::new(VecDeque::new())),
308 type_stats: Arc::new(Mutex::new(HashMap::new())),
309 running: Arc::new(Mutex::new(false)),
310 profiling_start_time: None,
311 samples_taken: Arc::new(AtomicU64::new(0)),
312 }
313 }
314
315 pub async fn start(&mut self) -> Result<()> {
317 let mut running = self.running.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
318 if *running {
319 return Err(anyhow::anyhow!("Memory profiler is already running"));
320 }
321
322 *running = true;
323 self.start_time = Some(Instant::now());
324 self.profiling_start_time = Some(Instant::now());
325
326 if self.config.enable_heap_tracking {
328 self.start_sampling().await?;
329 }
330
331 tracing::info!("Memory profiler started for session {}", self.session_id);
332 Ok(())
333 }
334
335 pub async fn stop(&mut self) -> Result<MemoryProfilingReport> {
337 {
338 let mut running = self.running.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
339 if !*running {
340 return Err(anyhow::anyhow!("Memory profiler is not running"));
341 }
342 *running = false;
343 }
344 let end_time = SystemTime::now();
347 let start_time = self
348 .start_time
349 .ok_or_else(|| anyhow::anyhow!("start_time should be set when profiler is running"))?;
350 let duration =
351 end_time.duration_since(UNIX_EPOCH)?.as_secs_f64() - start_time.elapsed().as_secs_f64();
352
353 let profiling_overhead = if let Some(prof_start) = self.profiling_start_time {
355 prof_start.elapsed().as_millis() as f64 * 0.01 } else {
357 0.0
358 };
359
360 let report = self.generate_report(end_time, duration, profiling_overhead).await?;
361
362 tracing::info!("Memory profiler stopped for session {}", self.session_id);
363 Ok(report)
364 }
365
366 pub fn record_allocation(
368 &self,
369 size: usize,
370 allocation_type: AllocationType,
371 tags: Vec<String>,
372 ) -> Result<Uuid> {
373 let running = self.running.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
374 if !*running {
375 return Err(anyhow::anyhow!("Memory profiler is not running"));
376 }
377
378 let allocation_id = Uuid::new_v4();
379 let record = AllocationRecord {
380 id: allocation_id,
381 size,
382 timestamp: SystemTime::now(),
383 stack_trace: self.capture_stack_trace(),
384 allocation_type: allocation_type.clone(),
385 freed: false,
386 freed_at: None,
387 tags,
388 };
389
390 let mut allocations =
392 self.allocations.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
393 allocations.insert(allocation_id, record);
394
395 self.update_type_stats(&allocation_type, size, true);
397
398 Ok(allocation_id)
399 }
400
401 pub fn record_deallocation(&self, allocation_id: Uuid) -> Result<()> {
403 let running = self.running.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
404 if !*running {
405 return Ok(()); }
407
408 let mut allocations =
409 self.allocations.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
410 if let Some(record) = allocations.get_mut(&allocation_id) {
411 record.freed = true;
412 record.freed_at = Some(SystemTime::now());
413
414 self.update_type_stats(&record.allocation_type, record.size, false);
416 }
417
418 Ok(())
419 }
420
421 pub fn tag_allocation(&self, allocation_id: Uuid, tag: String) -> Result<()> {
423 let mut allocations =
424 self.allocations.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
425 if let Some(record) = allocations.get_mut(&allocation_id) {
426 record.tags.push(tag);
427 }
428 Ok(())
429 }
430
431 pub fn get_memory_snapshot(&self) -> Result<MemorySnapshot> {
433 let allocations = self.allocations.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
434 let timeline = self.memory_timeline.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
435
436 let (allocation_rate, deallocation_rate) = allocation_rates_from_timeline(&timeline);
437 Ok(snapshot_from_allocations(
438 &allocations,
439 allocation_rate,
440 deallocation_rate,
441 ))
442 }
443
444 pub fn detect_leaks(&self) -> Result<Vec<MemoryLeak>> {
446 let allocations = self.allocations.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
447 let now = SystemTime::now();
448 let threshold = Duration::from_secs(self.config.leak_detection_threshold_secs);
449 let mut leaks = Vec::new();
450
451 for record in allocations.values() {
452 if !record.freed {
453 let age = now.duration_since(record.timestamp)?;
454 if age > threshold {
455 let age_seconds = age.as_secs_f64();
456 let severity = self.classify_leak_severity(record.size, age_seconds);
457
458 leaks.push(MemoryLeak {
459 allocation_id: record.id,
460 size: record.size,
461 age_seconds,
462 timestamp: record.timestamp,
463 allocation_type: record.allocation_type.clone(),
464 stack_trace: record.stack_trace.clone(),
465 tags: record.tags.clone(),
466 severity,
467 });
468 }
469 }
470 }
471
472 leaks.sort_by(|a, b| b.severity.cmp(&a.severity).then(b.size.cmp(&a.size)));
474
475 Ok(leaks)
476 }
477
478 pub fn analyze_patterns(&self) -> Result<Vec<AllocationPattern>> {
480 let mut patterns = Vec::new();
481
482 if let Ok(leak_pattern) = self.detect_leak_pattern() {
484 patterns.push(leak_pattern);
485 }
486
487 if let Ok(churn_pattern) = self.detect_churn_pattern() {
489 patterns.push(churn_pattern);
490 }
491
492 if let Ok(large_alloc_pattern) = self.detect_large_allocation_pattern() {
494 patterns.push(large_alloc_pattern);
495 }
496
497 if let Ok(frag_pattern) = self.detect_fragmentation_pattern() {
499 patterns.push(frag_pattern);
500 }
501
502 Ok(patterns)
503 }
504
505 pub fn analyze_fragmentation(&self) -> Result<FragmentationAnalysis> {
507 let snapshot = self.get_memory_snapshot()?;
508
509 let fragmentation_ratio = snapshot.fragmentation_ratio;
510 let severity = match fragmentation_ratio {
511 r if r < 0.1 => FragmentationSeverity::Low,
512 r if r < 0.3 => FragmentationSeverity::Medium,
513 r if r < 0.6 => FragmentationSeverity::High,
514 _ => FragmentationSeverity::Severe,
515 };
516
517 let recommendations = match severity {
518 FragmentationSeverity::Low => {
519 vec!["Memory fragmentation is low. Continue current practices.".to_string()]
520 },
521 FragmentationSeverity::Medium => vec![
522 "Consider pooling allocations of similar sizes.".to_string(),
523 "Monitor for increasing fragmentation trends.".to_string(),
524 ],
525 FragmentationSeverity::High => vec![
526 "Implement memory pooling for frequent allocations.".to_string(),
527 "Consider compaction strategies for long-running processes.".to_string(),
528 "Review allocation patterns for optimization opportunities.".to_string(),
529 ],
530 FragmentationSeverity::Severe => vec![
531 "Critical fragmentation detected. Immediate action required.".to_string(),
532 "Implement custom allocators with compaction.".to_string(),
533 "Consider restarting the process to reset memory layout.".to_string(),
534 "Review and optimize allocation strategies.".to_string(),
535 ],
536 };
537
538 Ok(FragmentationAnalysis {
539 fragmentation_ratio,
540 largest_free_block: snapshot.free_heap_bytes, total_free_memory: snapshot.free_heap_bytes,
542 free_block_count: snapshot.free_count,
543 average_free_block_size: if snapshot.free_count > 0 {
544 snapshot.free_heap_bytes as f64 / snapshot.free_count as f64
545 } else {
546 0.0
547 },
548 fragmentation_severity: severity,
549 recommendations,
550 })
551 }
552
553 pub fn analyze_gc_pressure(&self) -> Result<GCPressureAnalysis> {
555 let fragmentation_ratio = self.get_memory_snapshot()?.fragmentation_ratio;
558
559 let timeline = self.memory_timeline.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
560
561 let (allocation_rate, deallocation_rate) = self.calculate_allocation_rates(&timeline);
562 let pressure_score = self.calculate_gc_pressure_score(
563 allocation_rate,
564 deallocation_rate,
565 fragmentation_ratio,
566 );
567 let churn_rate = allocation_rate.min(deallocation_rate);
568
569 let pressure_level = match pressure_score {
570 p if p < 0.25 => GCPressureLevel::Low,
571 p if p < 0.5 => GCPressureLevel::Medium,
572 p if p < 0.75 => GCPressureLevel::High,
573 _ => GCPressureLevel::Critical,
574 };
575
576 let mut contributing_factors = Vec::new();
577 let mut recommendations = Vec::new();
578
579 if allocation_rate > 1000.0 {
580 contributing_factors.push("High allocation rate".to_string());
581 recommendations.push("Consider object pooling or reuse strategies".to_string());
582 }
583
584 if churn_rate > 500.0 {
585 contributing_factors.push("High allocation churn".to_string());
586 recommendations.push("Reduce temporary object creation".to_string());
587 }
588
589 if pressure_level == GCPressureLevel::Critical {
590 recommendations
591 .push("Consider manual memory management for critical paths".to_string());
592 }
593
594 Ok(GCPressureAnalysis {
595 pressure_score,
596 allocation_rate,
597 deallocation_rate,
598 churn_rate,
599 pressure_level,
600 contributing_factors,
601 recommendations,
602 })
603 }
604
605 async fn start_sampling(&self) -> Result<()> {
608 let interval_duration = Duration::from_millis(self.config.sampling_interval_ms);
609 let mut interval = interval(interval_duration);
610 let timeline = Arc::clone(&self.memory_timeline);
611 let allocations = Arc::clone(&self.allocations);
612 let running = Arc::clone(&self.running);
613 let samples_taken = Arc::clone(&self.samples_taken);
614
615 tokio::spawn(async move {
616 loop {
617 interval.tick().await;
618
619 let is_running = {
620 let running_guard =
621 running.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
622 *running_guard
623 };
624
625 if !is_running {
626 break;
627 }
628
629 {
633 let allocations_guard =
634 allocations.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
635 let mut timeline_guard =
636 timeline.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
637 let (allocation_rate, deallocation_rate) =
638 allocation_rates_from_timeline(&timeline_guard);
639 let snapshot = snapshot_from_allocations(
640 &allocations_guard,
641 allocation_rate,
642 deallocation_rate,
643 );
644 timeline_guard.push_back(snapshot);
645 while timeline_guard.len() > MAX_TIMELINE_SNAPSHOTS {
646 timeline_guard.pop_front();
647 }
648 }
649 samples_taken.fetch_add(1, Ordering::Relaxed);
650 }
651 });
652
653 Ok(())
654 }
655
656 pub async fn generate_report(
657 &self,
658 end_time: SystemTime,
659 duration_secs: f64,
660 profiling_overhead_ms: f64,
661 ) -> Result<MemoryProfilingReport> {
662 let (
665 total_allocations,
666 total_deallocations,
667 net_allocations,
668 peak_memory_mb,
669 average_memory_mb,
670 allocations_by_size_bucket,
671 timeline_snapshot,
672 type_stats_snapshot,
673 ) = {
674 let allocations =
675 self.allocations.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
676 let timeline =
677 self.memory_timeline.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
678 let type_stats =
679 self.type_stats.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
680
681 let total_allocs = allocations.len();
682 let total_deallocs = allocations.values().filter(|r| r.freed).count();
683 let net_allocs = total_allocs as i64 - total_deallocs as i64;
684
685 let peak_mem = timeline
687 .iter()
688 .map(|s| s.peak_heap_bytes as f64 / 1024.0 / 1024.0)
689 .fold(0.0, f64::max);
690
691 let avg_mem = if !timeline.is_empty() {
692 timeline.iter().map(|s| s.used_heap_bytes as f64 / 1024.0 / 1024.0).sum::<f64>()
693 / timeline.len() as f64
694 } else {
695 0.0
696 };
697
698 let mut size_buckets = HashMap::new();
700 for record in allocations.values() {
701 let bucket = size_bucket(record.size);
702 *size_buckets.entry(bucket).or_insert(0) += 1;
703 }
704
705 let timeline_snap: Vec<_> = timeline.iter().cloned().collect();
706 let type_stats_snap = type_stats.clone();
707
708 (
709 total_allocs,
710 total_deallocs,
711 net_allocs,
712 peak_mem,
713 avg_mem,
714 size_buckets,
715 timeline_snap,
716 type_stats_snap,
717 )
718 };
719 let potential_leaks = self.detect_leaks()?;
722 let detected_patterns = self.analyze_patterns()?;
723 let fragmentation_analysis = self.analyze_fragmentation()?;
724 let gc_pressure_analysis = self.analyze_gc_pressure()?;
725
726 let mut leak_summary = HashMap::new();
727 for leak in &potential_leaks {
728 *leak_summary.entry(leak.allocation_type.clone()).or_insert(0) += 1;
729 }
730
731 Ok(MemoryProfilingReport {
732 session_id: self.session_id,
733 start_time: UNIX_EPOCH
734 + Duration::from_secs_f64(
735 SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs_f64() - duration_secs,
736 ),
737 end_time,
738 duration_secs,
739 config: self.config.clone(),
740 peak_memory_mb,
741 average_memory_mb,
742 total_allocations,
743 total_deallocations,
744 net_allocations,
745 memory_timeline: timeline_snapshot,
746 potential_leaks,
747 leak_summary,
748 detected_patterns,
749 fragmentation_analysis,
750 gc_pressure_analysis,
751 allocations_by_type: type_stats_snapshot,
752 allocations_by_size_bucket,
753 profiling_overhead_ms,
754 sampling_accuracy: self.compute_sampling_accuracy(duration_secs),
755 })
756 }
757
758 fn compute_sampling_accuracy(&self, duration_secs: f64) -> Option<f64> {
763 if !self.config.enable_heap_tracking {
764 return None;
765 }
766 let interval_secs = (self.config.sampling_interval_ms.max(1) as f64) / 1000.0;
767 let expected_samples = (duration_secs / interval_secs).max(1.0);
768 let actual_samples = self.samples_taken.load(Ordering::Relaxed) as f64;
769 Some((actual_samples / expected_samples).min(1.0))
770 }
771
772 fn capture_stack_trace(&self) -> Vec<String> {
781 let backtrace = std::backtrace::Backtrace::force_capture();
782 match backtrace.status() {
783 std::backtrace::BacktraceStatus::Captured => {
784 let frames = parse_backtrace_frames(&backtrace);
785 if frames.is_empty() {
786 vec!["<stack trace captured but no frames could be resolved>".to_string()]
787 } else {
788 frames
789 }
790 },
791 std::backtrace::BacktraceStatus::Unsupported => {
792 vec!["<stack trace unavailable: unsupported on this platform>".to_string()]
793 },
794 _ => vec!["<stack trace unavailable>".to_string()],
795 }
796 }
797
798 fn update_type_stats(
799 &self,
800 allocation_type: &AllocationType,
801 size: usize,
802 is_allocation: bool,
803 ) {
804 let mut type_stats =
805 self.type_stats.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
806 let stats = type_stats.entry(allocation_type.clone()).or_insert(AllocationTypeStats {
807 total_allocations: 0,
808 total_deallocations: 0,
809 current_count: 0,
810 total_bytes_allocated: 0,
811 total_bytes_deallocated: 0,
812 current_bytes: 0,
813 peak_count: 0,
814 peak_bytes: 0,
815 average_allocation_size: 0.0,
816 largest_allocation: 0,
817 });
818
819 if is_allocation {
820 stats.total_allocations += 1;
821 stats.current_count += 1;
822 stats.total_bytes_allocated += size;
823 stats.current_bytes += size;
824 stats.peak_count = stats.peak_count.max(stats.current_count);
825 stats.peak_bytes = stats.peak_bytes.max(stats.current_bytes);
826 stats.largest_allocation = stats.largest_allocation.max(size);
827 } else {
828 stats.total_deallocations += 1;
829 stats.current_count = stats.current_count.saturating_sub(1);
830 stats.total_bytes_deallocated += size;
831 stats.current_bytes = stats.current_bytes.saturating_sub(size);
832 }
833
834 stats.average_allocation_size = if stats.total_allocations > 0 {
835 stats.total_bytes_allocated as f64 / stats.total_allocations as f64
836 } else {
837 0.0
838 };
839 }
840
841 fn classify_leak_severity(&self, size: usize, age_seconds: f64) -> LeakSeverity {
842 let large_size = size > self.config.large_allocation_threshold;
843 let old_age = age_seconds > 1800.0; let very_old_age = age_seconds > 3600.0; match (large_size, old_age, very_old_age) {
847 (true, _, true) => LeakSeverity::Critical,
848 (true, true, _) => LeakSeverity::High,
849 (true, false, _) => LeakSeverity::Medium,
850 (false, true, _) => LeakSeverity::Medium,
851 _ => LeakSeverity::Low,
852 }
853 }
854
855 fn calculate_gc_pressure_score(
859 &self,
860 allocation_rate: f64,
861 deallocation_rate: f64,
862 fragmentation_ratio: f64,
863 ) -> f64 {
864 gc_pressure_score(allocation_rate, deallocation_rate, fragmentation_ratio)
865 }
866
867 fn calculate_allocation_rates(&self, timeline: &VecDeque<MemorySnapshot>) -> (f64, f64) {
868 allocation_rates_from_timeline(timeline)
869 }
870
871 fn detect_leak_pattern(&self) -> Result<AllocationPattern> {
874 let leaks = self.detect_leaks()?;
875 let high_severity_leaks = leaks
876 .iter()
877 .filter(|l| l.severity == LeakSeverity::High || l.severity == LeakSeverity::Critical)
878 .count();
879
880 let confidence = if leaks.len() > 10 { 0.9 } else { 0.5 };
881 let impact_score = (high_severity_leaks as f64 / (leaks.len().max(1)) as f64).min(1.0);
882
883 Ok(AllocationPattern {
884 pattern_type: PatternType::MemoryLeak,
885 description: format!("Detected {} potential memory leaks", leaks.len()),
886 confidence,
887 impact_score,
888 recommendations: vec![
889 "Review long-lived allocations for proper cleanup".to_string(),
890 "Implement RAII patterns for automatic resource management".to_string(),
891 ],
892 examples: leaks
893 .into_iter()
894 .take(3)
895 .map(|leak| {
896 AllocationRecord {
899 id: leak.allocation_id,
900 size: leak.size,
901 timestamp: leak.timestamp,
902 stack_trace: leak.stack_trace,
903 allocation_type: leak.allocation_type,
904 freed: false,
905 freed_at: None,
906 tags: leak.tags,
907 }
908 })
909 .collect(),
910 })
911 }
912
913 fn detect_churn_pattern(&self) -> Result<AllocationPattern> {
916 let allocations = self.allocations.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
917 let short_lived: Vec<AllocationRecord> = allocations
918 .values()
919 .filter(|record| {
920 if let (Some(_freed_at), false) = (record.freed_at, record.freed) {
921 false } else if record.freed {
923 if let Some(freed_at) = record.freed_at {
924 freed_at.duration_since(record.timestamp).unwrap_or(Duration::from_secs(0))
925 < SHORT_LIVED_THRESHOLD
926 } else {
927 false
928 }
929 } else {
930 false
931 }
932 })
933 .cloned()
934 .collect();
935 let short_lived_count = short_lived.len();
936
937 let total_count = allocations.len();
938 let churn_ratio = if total_count > 0 {
939 short_lived_count as f64 / total_count as f64
940 } else {
941 0.0
942 };
943
944 Ok(AllocationPattern {
945 pattern_type: PatternType::ChurningAllocations,
946 description: format!(
947 "High allocation churn detected: {:.1}% short-lived allocations",
948 churn_ratio * 100.0
949 ),
950 confidence: if churn_ratio > 0.5 { 0.8 } else { 0.4 },
951 impact_score: churn_ratio,
952 recommendations: vec![
953 "Consider object pooling for frequently allocated objects".to_string(),
954 "Reduce temporary object creation in hot paths".to_string(),
955 ],
956 examples: {
959 let mut examples = short_lived;
960 examples.sort_by_key(|record| std::cmp::Reverse(record.size));
961 examples.truncate(3);
962 examples
963 },
964 })
965 }
966
967 fn detect_large_allocation_pattern(&self) -> Result<AllocationPattern> {
968 let allocations = self.allocations.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
969 let large_allocations: Vec<_> = allocations
970 .values()
971 .filter(|record| record.size > self.config.large_allocation_threshold)
972 .cloned()
973 .collect();
974
975 let impact_score = if !allocations.is_empty() {
976 large_allocations.len() as f64 / allocations.len() as f64
977 } else {
978 0.0
979 };
980
981 Ok(AllocationPattern {
982 pattern_type: PatternType::LargeAllocations,
983 description: format!(
984 "Found {} large allocations (>{}MB)",
985 large_allocations.len(),
986 self.config.large_allocation_threshold / 1024 / 1024
987 ),
988 confidence: if large_allocations.len() > 5 { 0.9 } else { 0.6 },
989 impact_score,
990 recommendations: vec![
991 "Review large allocations for optimization opportunities".to_string(),
992 "Consider streaming or chunked processing for large data".to_string(),
993 ],
994 examples: large_allocations.into_iter().take(3).collect(),
995 })
996 }
997
998 fn detect_fragmentation_pattern(&self) -> Result<AllocationPattern> {
999 let fragmentation = self.analyze_fragmentation()?;
1000
1001 Ok(AllocationPattern {
1002 pattern_type: PatternType::FragmentationCausing,
1003 description: format!(
1004 "Memory fragmentation at {:.1}%",
1005 fragmentation.fragmentation_ratio * 100.0
1006 ),
1007 confidence: 0.8,
1008 impact_score: fragmentation.fragmentation_ratio,
1009 recommendations: fragmentation.recommendations,
1010 examples: Vec::new(),
1014 })
1015 }
1016}
1017
1018fn size_bucket(size: usize) -> String {
1027 match size {
1028 0..=1024 => "0-1KB".to_string(),
1029 1025..=10240 => "1-10KB".to_string(),
1030 10241..=102400 => "10-100KB".to_string(),
1031 102401..=1048576 => "100KB-1MB".to_string(),
1032 1048577..=10485760 => "1-10MB".to_string(),
1033 _ => ">10MB".to_string(),
1034 }
1035}
1036
1037fn gc_pressure_score(
1044 allocation_rate: f64,
1045 deallocation_rate: f64,
1046 fragmentation_ratio: f64,
1047) -> f64 {
1048 let churn = allocation_rate.min(deallocation_rate).max(0.0);
1049 let churn_component = (churn / 1000.0).min(1.0);
1050 let fragmentation_component = fragmentation_ratio.clamp(0.0, 1.0);
1051 (0.7 * churn_component + 0.3 * fragmentation_component).clamp(0.0, 1.0)
1052}
1053
1054fn allocation_rates_from_timeline(timeline: &VecDeque<MemorySnapshot>) -> (f64, f64) {
1058 if timeline.len() < 2 {
1059 return (0.0, 0.0);
1060 }
1061
1062 let first = &timeline[0];
1063 let last = &timeline[timeline.len() - 1];
1064
1065 let duration = last
1066 .timestamp
1067 .duration_since(first.timestamp)
1068 .unwrap_or(Duration::from_secs(1))
1069 .as_secs_f64()
1070 .max(f64::EPSILON);
1071
1072 let allocation_rate = (last.allocation_count as f64 - first.allocation_count as f64) / duration;
1073 let deallocation_rate = (last.free_count as f64 - first.free_count as f64) / duration;
1074
1075 (allocation_rate.max(0.0), deallocation_rate.max(0.0))
1076}
1077
1078fn snapshot_from_allocations(
1082 allocations: &HashMap<Uuid, AllocationRecord>,
1083 allocation_rate: f64,
1084 deallocation_rate: f64,
1085) -> MemorySnapshot {
1086 let mut total_heap = 0usize;
1087 let mut used_heap = 0usize;
1088 let mut allocation_count = 0usize;
1089 let mut free_count = 0usize;
1090 let mut allocations_by_type: HashMap<AllocationType, usize> = HashMap::new();
1091 let mut allocations_by_size: HashMap<String, usize> = HashMap::new();
1092
1093 for record in allocations.values() {
1094 total_heap += record.size;
1095
1096 if !record.freed {
1097 used_heap += record.size;
1098 allocation_count += 1;
1099 *allocations_by_type.entry(record.allocation_type.clone()).or_insert(0) += record.size;
1100 *allocations_by_size.entry(size_bucket(record.size)).or_insert(0) += 1;
1101 } else {
1102 free_count += 1;
1103 }
1104 }
1105
1106 let free_heap = total_heap.saturating_sub(used_heap);
1107 let fragmentation_ratio =
1108 if total_heap > 0 { free_heap as f64 / total_heap as f64 } else { 0.0 };
1109 let gc_pressure_score =
1110 gc_pressure_score(allocation_rate, deallocation_rate, fragmentation_ratio);
1111
1112 MemorySnapshot {
1113 timestamp: SystemTime::now(),
1114 total_heap_bytes: total_heap,
1115 used_heap_bytes: used_heap,
1116 free_heap_bytes: free_heap,
1117 peak_heap_bytes: used_heap, allocation_count,
1119 free_count,
1120 fragmentation_ratio,
1121 gc_pressure_score,
1122 allocations_by_type,
1123 allocations_by_size,
1124 }
1125}
1126
1127fn parse_backtrace_frames(backtrace: &std::backtrace::Backtrace) -> Vec<String> {
1135 let rendered = format!("{backtrace:?}");
1136 const NEEDLE: &str = "fn: \"";
1137 let mut frames = Vec::new();
1138 let mut rest = rendered.as_str();
1139 while let Some(start) = rest.find(NEEDLE) {
1140 rest = &rest[start + NEEDLE.len()..];
1141 match rest.find('"') {
1142 Some(end) => {
1143 frames.push(rest[..end].to_string());
1144 rest = &rest[end + 1..];
1145 },
1146 None => break,
1147 }
1148 }
1149 frames
1150}
1151
1152impl PartialOrd for LeakSeverity {
1153 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
1154 Some(self.cmp(other))
1155 }
1156}
1157
1158impl Ord for LeakSeverity {
1159 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
1160 let self_val = match self {
1161 LeakSeverity::Low => 0,
1162 LeakSeverity::Medium => 1,
1163 LeakSeverity::High => 2,
1164 LeakSeverity::Critical => 3,
1165 };
1166 let other_val = match other {
1167 LeakSeverity::Low => 0,
1168 LeakSeverity::Medium => 1,
1169 LeakSeverity::High => 2,
1170 LeakSeverity::Critical => 3,
1171 };
1172 self_val.cmp(&other_val)
1173 }
1174}
1175
1176#[cfg(test)]
1177mod tests {
1178 use super::*;
1179
1180 #[test]
1183 fn churn_pattern_carries_real_short_lived_allocation_examples() {
1184 let profiler = MemoryProfiler::new(MemoryProfilingConfig::default());
1185 let now = std::time::SystemTime::now();
1186 {
1187 let mut allocations = profiler.allocations.lock().unwrap_or_else(|p| p.into_inner());
1188 for (index, size) in [(0_usize, 4096_usize), (1, 64), (2, 1024)] {
1189 let id = Uuid::new_v4();
1190 allocations.insert(
1191 id,
1192 AllocationRecord {
1193 id,
1194 size,
1195 timestamp: now,
1196 stack_trace: Vec::new(),
1197 allocation_type: AllocationType::Tensor,
1198 freed: true,
1199 freed_at: Some(now + Duration::from_millis(10)),
1200 tags: vec![format!("alloc_{index}")],
1201 },
1202 );
1203 }
1204 }
1205
1206 let pattern = profiler.detect_churn_pattern().expect("pattern");
1207 assert_eq!(
1209 pattern.examples.len(),
1210 3,
1211 "all three short-lived allocations qualify"
1212 );
1213 assert_eq!(pattern.examples[0].size, 4096, "largest first");
1214 assert!(pattern.examples.windows(2).all(|w| w[0].size >= w[1].size));
1215 assert!(
1216 (pattern.impact_score - 1.0).abs() < 1e-9,
1217 "every allocation churned"
1218 );
1219 }
1220 use tokio;
1221
1222 #[tokio::test(flavor = "multi_thread")]
1223 async fn test_memory_profiler_basic() -> Result<()> {
1224 let config = MemoryProfilingConfig {
1225 sampling_interval_ms: 1000, ..Default::default()
1227 };
1228 let mut profiler = MemoryProfiler::new(config);
1229
1230 let test_result = tokio::time::timeout(Duration::from_secs(60), async {
1244 profiler.start().await?;
1245
1246 let alloc_id1 = profiler.record_allocation(
1248 1024,
1249 AllocationType::Tensor,
1250 vec!["test".to_string()],
1251 )?;
1252
1253 let _alloc_id2 = profiler.record_allocation(
1254 2048,
1255 AllocationType::Buffer,
1256 vec!["test".to_string()],
1257 )?;
1258
1259 profiler.record_deallocation(alloc_id1)?;
1261
1262 tokio::time::sleep(Duration::from_millis(1)).await;
1264
1265 let report = profiler.stop().await?;
1266
1267 assert_eq!(report.total_allocations, 2);
1268 assert_eq!(report.total_deallocations, 1);
1269 assert_eq!(report.net_allocations, 1);
1270
1271 Ok::<(), anyhow::Error>(())
1272 })
1273 .await;
1274
1275 match test_result {
1276 Ok(result) => result,
1277 Err(_) => Err(anyhow::anyhow!("Test timed out after 5s")),
1278 }
1279 }
1280
1281 #[tokio::test]
1282 async fn test_leak_detection() -> Result<()> {
1283 let config = MemoryProfilingConfig {
1284 leak_detection_threshold_secs: 1, ..Default::default()
1286 };
1287
1288 let mut profiler = MemoryProfiler::new(config);
1289 profiler.start().await?; profiler.record_allocation(1024, AllocationType::Tensor, vec!["leak_test".to_string()])?;
1293
1294 tokio::time::sleep(Duration::from_secs(2)).await;
1295
1296 let leaks = profiler.detect_leaks()?;
1297 assert!(!leaks.is_empty());
1298
1299 Ok(())
1300 }
1301
1302 #[test]
1303 fn test_size_buckets() {
1304 assert_eq!(size_bucket(512), "0-1KB");
1305 assert_eq!(size_bucket(5120), "1-10KB");
1306 assert_eq!(size_bucket(51200), "10-100KB");
1307 assert_eq!(size_bucket(512000), "100KB-1MB");
1308 assert_eq!(size_bucket(5120000), "1-10MB");
1309 assert_eq!(size_bucket(51200000), ">10MB");
1310 }
1311
1312 #[test]
1313 fn test_leak_severity_classification() {
1314 let config = MemoryProfilingConfig::default();
1315 let profiler = MemoryProfiler::new(config);
1316
1317 assert_eq!(
1319 profiler.classify_leak_severity(1024, 60.0),
1320 LeakSeverity::Low
1321 );
1322
1323 assert_eq!(
1325 profiler.classify_leak_severity(10485760, 3700.0),
1326 LeakSeverity::Critical
1327 );
1328
1329 assert_eq!(
1331 profiler.classify_leak_severity(524288, 1900.0),
1332 LeakSeverity::Medium
1333 );
1334 }
1335
1336 #[test]
1337 fn test_capture_stack_trace_is_real_not_the_old_hardcoded_placeholder() {
1338 let config = MemoryProfilingConfig::default();
1339 let profiler = MemoryProfiler::new(config);
1340
1341 let frames = profiler.capture_stack_trace();
1342
1343 assert_ne!(
1346 frames,
1347 vec![
1348 "function_a".to_string(),
1349 "function_b".to_string(),
1350 "main".to_string()
1351 ]
1352 );
1353 assert!(
1354 !frames.is_empty(),
1355 "a captured backtrace must contain at least one frame"
1356 );
1357 assert!(
1358 frames.iter().all(|f| !f.trim().is_empty()),
1359 "no frame string should be empty"
1360 );
1361 }
1362
1363 #[test]
1364 fn test_gc_pressure_score_is_computed_from_real_signals_not_a_constant() {
1365 let idle = gc_pressure_score(0.0, 0.0, 0.0);
1367 let busy = gc_pressure_score(2000.0, 2000.0, 0.9);
1368
1369 assert_eq!(
1370 idle, 0.0,
1371 "no churn and no fragmentation must score zero pressure"
1372 );
1373 assert!(
1374 busy > idle,
1375 "high churn + high fragmentation must score higher than idle"
1376 );
1377 assert!((0.0..=1.0).contains(&busy));
1378 assert_ne!(idle, 0.3, "must not be the old fabricated constant");
1379 assert_ne!(busy, 0.3, "must not be the old fabricated constant");
1380 }
1381
1382 #[test]
1383 fn test_allocation_rates_from_timeline_uses_real_deltas() {
1384 let mut timeline = VecDeque::new();
1385 let t0 = SystemTime::now();
1386 timeline.push_back(MemorySnapshot {
1387 timestamp: t0,
1388 total_heap_bytes: 0,
1389 used_heap_bytes: 0,
1390 free_heap_bytes: 0,
1391 peak_heap_bytes: 0,
1392 allocation_count: 0,
1393 free_count: 0,
1394 fragmentation_ratio: 0.0,
1395 gc_pressure_score: 0.0,
1396 allocations_by_type: HashMap::new(),
1397 allocations_by_size: HashMap::new(),
1398 });
1399 timeline.push_back(MemorySnapshot {
1400 timestamp: t0 + Duration::from_secs(2),
1401 total_heap_bytes: 0,
1402 used_heap_bytes: 0,
1403 free_heap_bytes: 0,
1404 peak_heap_bytes: 0,
1405 allocation_count: 20,
1406 free_count: 10,
1407 fragmentation_ratio: 0.0,
1408 gc_pressure_score: 0.0,
1409 allocations_by_type: HashMap::new(),
1410 allocations_by_size: HashMap::new(),
1411 });
1412
1413 let (alloc_rate, dealloc_rate) = allocation_rates_from_timeline(&timeline);
1414 assert!(
1415 (alloc_rate - 10.0).abs() < 1e-9,
1416 "20 allocations over 2s => 10/s, got {alloc_rate}"
1417 );
1418 assert!(
1419 (dealloc_rate - 5.0).abs() < 1e-9,
1420 "10 frees over 2s => 5/s, got {dealloc_rate}"
1421 );
1422 }
1423
1424 #[tokio::test(flavor = "multi_thread")]
1425 async fn test_sampling_accuracy_is_none_when_tracking_disabled() -> Result<()> {
1426 let config = MemoryProfilingConfig {
1427 enable_heap_tracking: false,
1428 ..Default::default()
1429 };
1430 let mut profiler = MemoryProfiler::new(config);
1431 profiler.start().await?;
1432 tokio::time::sleep(Duration::from_millis(20)).await;
1433 let report = profiler.stop().await?;
1434
1435 assert_eq!(report.sampling_accuracy, None);
1438 Ok(())
1439 }
1440
1441 #[tokio::test(flavor = "multi_thread")]
1442 async fn test_sampling_accuracy_is_real_when_tracking_enabled() -> Result<()> {
1443 let config = MemoryProfilingConfig {
1444 enable_heap_tracking: true,
1445 sampling_interval_ms: 20,
1446 ..Default::default()
1447 };
1448 let mut profiler = MemoryProfiler::new(config);
1449 profiler.start().await?;
1450 tokio::time::sleep(Duration::from_millis(220)).await;
1452 let report = profiler.stop().await?;
1453
1454 let accuracy = report.sampling_accuracy.expect("tracking was enabled");
1455 assert!((0.0..=1.0).contains(&accuracy));
1456 assert_ne!(accuracy, 0.95, "must not be the old fabricated constant");
1457 assert!(
1459 !report.memory_timeline.is_empty(),
1460 "background sampler must record real snapshots, not do nothing"
1461 );
1462 Ok(())
1463 }
1464}