1use anyhow::Result;
31use serde::{Deserialize, Serialize};
32use std::collections::{HashMap, VecDeque};
33use std::sync::{Arc, Mutex};
34use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
35use tokio::time::interval;
36use uuid::Uuid;
37
38#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct MemoryProfilingConfig {
41 pub enable_heap_tracking: bool,
43 pub enable_leak_detection: bool,
45 pub enable_pattern_analysis: bool,
47 pub enable_fragmentation_monitoring: bool,
49 pub enable_gc_pressure_analysis: bool,
51 pub sampling_interval_ms: u64,
53 pub max_allocation_records: usize,
55 pub large_allocation_threshold: usize,
57 pub pattern_analysis_window_secs: u64,
59 pub leak_detection_threshold_secs: u64,
61}
62
63impl Default for MemoryProfilingConfig {
64 fn default() -> Self {
65 Self {
66 enable_heap_tracking: true,
67 enable_leak_detection: true,
68 enable_pattern_analysis: true,
69 enable_fragmentation_monitoring: true,
70 enable_gc_pressure_analysis: true,
71 sampling_interval_ms: 100, max_allocation_records: 100000,
73 large_allocation_threshold: 1024 * 1024, pattern_analysis_window_secs: 60, leak_detection_threshold_secs: 300, }
77 }
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct AllocationRecord {
83 pub id: Uuid,
84 pub size: usize,
85 pub timestamp: SystemTime,
86 pub stack_trace: Vec<String>,
87 pub allocation_type: AllocationType,
88 pub freed: bool,
89 pub freed_at: Option<SystemTime>,
90 pub tags: Vec<String>, }
92
93#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
95pub enum AllocationType {
96 Tensor,
97 Buffer,
98 Weights,
99 Gradients,
100 Activations,
101 Cache,
102 Temporary,
103 Other(String),
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct MemorySnapshot {
109 pub timestamp: SystemTime,
110 pub total_heap_bytes: usize,
111 pub used_heap_bytes: usize,
112 pub free_heap_bytes: usize,
113 pub peak_heap_bytes: usize,
114 pub allocation_count: usize,
115 pub free_count: usize,
116 pub fragmentation_ratio: f64,
117 pub gc_pressure_score: f64,
118 pub allocations_by_type: HashMap<AllocationType, usize>,
119 pub allocations_by_size: HashMap<String, usize>, }
121
122#[derive(Debug, Clone, Serialize, Deserialize)]
124pub struct MemoryLeak {
125 pub allocation_id: Uuid,
126 pub size: usize,
127 pub age_seconds: f64,
128 pub allocation_type: AllocationType,
129 pub stack_trace: Vec<String>,
130 pub tags: Vec<String>,
131 pub severity: LeakSeverity,
132}
133
134#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
136pub enum LeakSeverity {
137 Low, Medium, High, Critical, }
142
143#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct AllocationPattern {
146 pub pattern_type: PatternType,
147 pub description: String,
148 pub confidence: f64, pub impact_score: f64, pub recommendations: Vec<String>,
151 pub examples: Vec<AllocationRecord>,
152}
153
154#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
156pub enum PatternType {
157 MemoryLeak, ChurningAllocations, FragmentationCausing, LargeAllocations, UnbalancedTypes, PeakUsageSpikes, }
164
165#[derive(Debug, Clone, Serialize, Deserialize)]
167pub struct FragmentationAnalysis {
168 pub fragmentation_ratio: f64,
169 pub largest_free_block: usize,
170 pub total_free_memory: usize,
171 pub free_block_count: usize,
172 pub average_free_block_size: f64,
173 pub fragmentation_severity: FragmentationSeverity,
174 pub recommendations: Vec<String>,
175}
176
177#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
179pub enum FragmentationSeverity {
180 Low, Medium, High, Severe, }
185
186#[derive(Debug, Clone, Serialize, Deserialize)]
188pub struct GCPressureAnalysis {
189 pub pressure_score: f64, pub allocation_rate: f64, pub deallocation_rate: f64, pub churn_rate: f64, pub pressure_level: GCPressureLevel,
194 pub contributing_factors: Vec<String>,
195 pub recommendations: Vec<String>,
196}
197
198#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
200pub enum GCPressureLevel {
201 Low,
202 Medium,
203 High,
204 Critical,
205}
206
207#[derive(Debug, Clone, Serialize, Deserialize)]
209pub struct MemoryProfilingReport {
210 pub session_id: Uuid,
211 pub start_time: SystemTime,
212 pub end_time: SystemTime,
213 pub duration_secs: f64,
214 pub config: MemoryProfilingConfig,
215
216 pub peak_memory_mb: f64,
218 pub average_memory_mb: f64,
219 pub total_allocations: usize,
220 pub total_deallocations: usize,
221 pub net_allocations: i64,
222
223 pub memory_timeline: Vec<MemorySnapshot>,
225
226 pub potential_leaks: Vec<MemoryLeak>,
228 pub leak_summary: HashMap<AllocationType, usize>,
229
230 pub detected_patterns: Vec<AllocationPattern>,
232
233 pub fragmentation_analysis: FragmentationAnalysis,
235
236 pub gc_pressure_analysis: GCPressureAnalysis,
238
239 pub allocations_by_type: HashMap<AllocationType, AllocationTypeStats>,
241 pub allocations_by_size_bucket: HashMap<String, usize>,
242
243 pub profiling_overhead_ms: f64,
245 pub sampling_accuracy: f64,
246}
247
248#[derive(Debug, Clone, Serialize, Deserialize)]
250pub struct AllocationTypeStats {
251 pub total_allocations: usize,
252 pub total_deallocations: usize,
253 pub current_count: usize,
254 pub total_bytes_allocated: usize,
255 pub total_bytes_deallocated: usize,
256 pub current_bytes: usize,
257 pub peak_count: usize,
258 pub peak_bytes: usize,
259 pub average_allocation_size: f64,
260 pub largest_allocation: usize,
261}
262
263#[derive(Debug)]
265pub struct MemoryProfiler {
266 config: MemoryProfilingConfig,
267 session_id: Uuid,
268 start_time: Option<Instant>,
269 allocations: Arc<Mutex<HashMap<Uuid, AllocationRecord>>>,
270 memory_timeline: Arc<Mutex<VecDeque<MemorySnapshot>>>,
271 type_stats: Arc<Mutex<HashMap<AllocationType, AllocationTypeStats>>>,
272 running: Arc<Mutex<bool>>,
273 profiling_start_time: Option<Instant>,
274}
275
276impl MemoryProfiler {
277 pub fn new(config: MemoryProfilingConfig) -> Self {
279 Self {
280 config,
281 session_id: Uuid::new_v4(),
282 start_time: None,
283 allocations: Arc::new(Mutex::new(HashMap::new())),
284 memory_timeline: Arc::new(Mutex::new(VecDeque::new())),
285 type_stats: Arc::new(Mutex::new(HashMap::new())),
286 running: Arc::new(Mutex::new(false)),
287 profiling_start_time: None,
288 }
289 }
290
291 pub async fn start(&mut self) -> Result<()> {
293 let mut running = self.running.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
294 if *running {
295 return Err(anyhow::anyhow!("Memory profiler is already running"));
296 }
297
298 *running = true;
299 self.start_time = Some(Instant::now());
300 self.profiling_start_time = Some(Instant::now());
301
302 if self.config.enable_heap_tracking {
304 self.start_sampling().await?;
305 }
306
307 tracing::info!("Memory profiler started for session {}", self.session_id);
308 Ok(())
309 }
310
311 pub async fn stop(&mut self) -> Result<MemoryProfilingReport> {
313 {
314 let mut running = self.running.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
315 if !*running {
316 return Err(anyhow::anyhow!("Memory profiler is not running"));
317 }
318 *running = false;
319 }
320 let end_time = SystemTime::now();
323 let start_time = self
324 .start_time
325 .ok_or_else(|| anyhow::anyhow!("start_time should be set when profiler is running"))?;
326 let duration =
327 end_time.duration_since(UNIX_EPOCH)?.as_secs_f64() - start_time.elapsed().as_secs_f64();
328
329 let profiling_overhead = if let Some(prof_start) = self.profiling_start_time {
331 prof_start.elapsed().as_millis() as f64 * 0.01 } else {
333 0.0
334 };
335
336 let report = self.generate_report(end_time, duration, profiling_overhead).await?;
337
338 tracing::info!("Memory profiler stopped for session {}", self.session_id);
339 Ok(report)
340 }
341
342 pub fn record_allocation(
344 &self,
345 size: usize,
346 allocation_type: AllocationType,
347 tags: Vec<String>,
348 ) -> Result<Uuid> {
349 let running = self.running.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
350 if !*running {
351 return Err(anyhow::anyhow!("Memory profiler is not running"));
352 }
353
354 let allocation_id = Uuid::new_v4();
355 let record = AllocationRecord {
356 id: allocation_id,
357 size,
358 timestamp: SystemTime::now(),
359 stack_trace: self.capture_stack_trace(),
360 allocation_type: allocation_type.clone(),
361 freed: false,
362 freed_at: None,
363 tags,
364 };
365
366 let mut allocations =
368 self.allocations.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
369 allocations.insert(allocation_id, record);
370
371 self.update_type_stats(&allocation_type, size, true);
373
374 Ok(allocation_id)
375 }
376
377 pub fn record_deallocation(&self, allocation_id: Uuid) -> Result<()> {
379 let running = self.running.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
380 if !*running {
381 return Ok(()); }
383
384 let mut allocations =
385 self.allocations.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
386 if let Some(record) = allocations.get_mut(&allocation_id) {
387 record.freed = true;
388 record.freed_at = Some(SystemTime::now());
389
390 self.update_type_stats(&record.allocation_type, record.size, false);
392 }
393
394 Ok(())
395 }
396
397 pub fn tag_allocation(&self, allocation_id: Uuid, tag: String) -> Result<()> {
399 let mut allocations =
400 self.allocations.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
401 if let Some(record) = allocations.get_mut(&allocation_id) {
402 record.tags.push(tag);
403 }
404 Ok(())
405 }
406
407 pub fn get_memory_snapshot(&self) -> Result<MemorySnapshot> {
409 let allocations = self.allocations.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
410 let _type_stats = self.type_stats.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
411
412 let mut total_heap = 0;
413 let mut used_heap = 0;
414 let mut allocation_count = 0;
415 let mut free_count = 0;
416 let mut allocations_by_type = HashMap::new();
417 let mut allocations_by_size = HashMap::new();
418
419 for record in allocations.values() {
420 total_heap += record.size;
421
422 if !record.freed {
423 used_heap += record.size;
424 allocation_count += 1;
425
426 *allocations_by_type.entry(record.allocation_type.clone()).or_insert(0) +=
427 record.size;
428
429 let size_bucket = self.get_size_bucket(record.size);
430 *allocations_by_size.entry(size_bucket).or_insert(0) += 1;
431 } else {
432 free_count += 1;
433 }
434 }
435
436 let free_heap = total_heap - used_heap;
437 let fragmentation_ratio =
438 if total_heap > 0 { free_heap as f64 / total_heap as f64 } else { 0.0 };
439
440 let gc_pressure_score = self.calculate_gc_pressure_score();
441
442 Ok(MemorySnapshot {
443 timestamp: SystemTime::now(),
444 total_heap_bytes: total_heap,
445 used_heap_bytes: used_heap,
446 free_heap_bytes: free_heap,
447 peak_heap_bytes: used_heap, allocation_count,
449 free_count,
450 fragmentation_ratio,
451 gc_pressure_score,
452 allocations_by_type,
453 allocations_by_size,
454 })
455 }
456
457 pub fn detect_leaks(&self) -> Result<Vec<MemoryLeak>> {
459 let allocations = self.allocations.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
460 let now = SystemTime::now();
461 let threshold = Duration::from_secs(self.config.leak_detection_threshold_secs);
462 let mut leaks = Vec::new();
463
464 for record in allocations.values() {
465 if !record.freed {
466 let age = now.duration_since(record.timestamp)?;
467 if age > threshold {
468 let age_seconds = age.as_secs_f64();
469 let severity = self.classify_leak_severity(record.size, age_seconds);
470
471 leaks.push(MemoryLeak {
472 allocation_id: record.id,
473 size: record.size,
474 age_seconds,
475 allocation_type: record.allocation_type.clone(),
476 stack_trace: record.stack_trace.clone(),
477 tags: record.tags.clone(),
478 severity,
479 });
480 }
481 }
482 }
483
484 leaks.sort_by(|a, b| b.severity.cmp(&a.severity).then(b.size.cmp(&a.size)));
486
487 Ok(leaks)
488 }
489
490 pub fn analyze_patterns(&self) -> Result<Vec<AllocationPattern>> {
492 let mut patterns = Vec::new();
493
494 if let Ok(leak_pattern) = self.detect_leak_pattern() {
496 patterns.push(leak_pattern);
497 }
498
499 if let Ok(churn_pattern) = self.detect_churn_pattern() {
501 patterns.push(churn_pattern);
502 }
503
504 if let Ok(large_alloc_pattern) = self.detect_large_allocation_pattern() {
506 patterns.push(large_alloc_pattern);
507 }
508
509 if let Ok(frag_pattern) = self.detect_fragmentation_pattern() {
511 patterns.push(frag_pattern);
512 }
513
514 Ok(patterns)
515 }
516
517 pub fn analyze_fragmentation(&self) -> Result<FragmentationAnalysis> {
519 let snapshot = self.get_memory_snapshot()?;
520
521 let fragmentation_ratio = snapshot.fragmentation_ratio;
522 let severity = match fragmentation_ratio {
523 r if r < 0.1 => FragmentationSeverity::Low,
524 r if r < 0.3 => FragmentationSeverity::Medium,
525 r if r < 0.6 => FragmentationSeverity::High,
526 _ => FragmentationSeverity::Severe,
527 };
528
529 let recommendations = match severity {
530 FragmentationSeverity::Low => {
531 vec!["Memory fragmentation is low. Continue current practices.".to_string()]
532 },
533 FragmentationSeverity::Medium => vec![
534 "Consider pooling allocations of similar sizes.".to_string(),
535 "Monitor for increasing fragmentation trends.".to_string(),
536 ],
537 FragmentationSeverity::High => vec![
538 "Implement memory pooling for frequent allocations.".to_string(),
539 "Consider compaction strategies for long-running processes.".to_string(),
540 "Review allocation patterns for optimization opportunities.".to_string(),
541 ],
542 FragmentationSeverity::Severe => vec![
543 "Critical fragmentation detected. Immediate action required.".to_string(),
544 "Implement custom allocators with compaction.".to_string(),
545 "Consider restarting the process to reset memory layout.".to_string(),
546 "Review and optimize allocation strategies.".to_string(),
547 ],
548 };
549
550 Ok(FragmentationAnalysis {
551 fragmentation_ratio,
552 largest_free_block: snapshot.free_heap_bytes, total_free_memory: snapshot.free_heap_bytes,
554 free_block_count: snapshot.free_count,
555 average_free_block_size: if snapshot.free_count > 0 {
556 snapshot.free_heap_bytes as f64 / snapshot.free_count as f64
557 } else {
558 0.0
559 },
560 fragmentation_severity: severity,
561 recommendations,
562 })
563 }
564
565 pub fn analyze_gc_pressure(&self) -> Result<GCPressureAnalysis> {
567 let timeline = self.memory_timeline.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
568
569 let pressure_score = self.calculate_gc_pressure_score();
570 let (allocation_rate, deallocation_rate) = self.calculate_allocation_rates(&timeline);
571 let churn_rate = allocation_rate.min(deallocation_rate);
572
573 let pressure_level = match pressure_score {
574 p if p < 0.25 => GCPressureLevel::Low,
575 p if p < 0.5 => GCPressureLevel::Medium,
576 p if p < 0.75 => GCPressureLevel::High,
577 _ => GCPressureLevel::Critical,
578 };
579
580 let mut contributing_factors = Vec::new();
581 let mut recommendations = Vec::new();
582
583 if allocation_rate > 1000.0 {
584 contributing_factors.push("High allocation rate".to_string());
585 recommendations.push("Consider object pooling or reuse strategies".to_string());
586 }
587
588 if churn_rate > 500.0 {
589 contributing_factors.push("High allocation churn".to_string());
590 recommendations.push("Reduce temporary object creation".to_string());
591 }
592
593 if pressure_level == GCPressureLevel::Critical {
594 recommendations
595 .push("Consider manual memory management for critical paths".to_string());
596 }
597
598 Ok(GCPressureAnalysis {
599 pressure_score,
600 allocation_rate,
601 deallocation_rate,
602 churn_rate,
603 pressure_level,
604 contributing_factors,
605 recommendations,
606 })
607 }
608
609 async fn start_sampling(&self) -> Result<()> {
612 let interval_duration = Duration::from_millis(self.config.sampling_interval_ms);
613 let mut interval = interval(interval_duration);
614 let _timeline = Arc::clone(&self.memory_timeline);
615 let running = Arc::clone(&self.running);
616
617 tokio::spawn(async move {
618 loop {
619 interval.tick().await;
620
621 let is_running = {
622 let running_guard =
623 running.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
624 *running_guard
625 };
626
627 if !is_running {
628 break;
629 }
630
631 }
634 });
635
636 Ok(())
637 }
638
639 pub async fn generate_report(
640 &self,
641 end_time: SystemTime,
642 duration_secs: f64,
643 profiling_overhead_ms: f64,
644 ) -> Result<MemoryProfilingReport> {
645 let (
648 total_allocations,
649 total_deallocations,
650 net_allocations,
651 peak_memory_mb,
652 average_memory_mb,
653 allocations_by_size_bucket,
654 timeline_snapshot,
655 type_stats_snapshot,
656 ) = {
657 let allocations =
658 self.allocations.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
659 let timeline =
660 self.memory_timeline.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
661 let type_stats =
662 self.type_stats.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
663
664 let total_allocs = allocations.len();
665 let total_deallocs = allocations.values().filter(|r| r.freed).count();
666 let net_allocs = total_allocs as i64 - total_deallocs as i64;
667
668 let peak_mem = timeline
670 .iter()
671 .map(|s| s.peak_heap_bytes as f64 / 1024.0 / 1024.0)
672 .fold(0.0, f64::max);
673
674 let avg_mem = if !timeline.is_empty() {
675 timeline.iter().map(|s| s.used_heap_bytes as f64 / 1024.0 / 1024.0).sum::<f64>()
676 / timeline.len() as f64
677 } else {
678 0.0
679 };
680
681 let mut size_buckets = HashMap::new();
683 for record in allocations.values() {
684 let bucket = self.get_size_bucket(record.size);
685 *size_buckets.entry(bucket).or_insert(0) += 1;
686 }
687
688 let timeline_snap: Vec<_> = timeline.iter().cloned().collect();
689 let type_stats_snap = type_stats.clone();
690
691 (
692 total_allocs,
693 total_deallocs,
694 net_allocs,
695 peak_mem,
696 avg_mem,
697 size_buckets,
698 timeline_snap,
699 type_stats_snap,
700 )
701 };
702 let potential_leaks = self.detect_leaks()?;
705 let detected_patterns = self.analyze_patterns()?;
706 let fragmentation_analysis = self.analyze_fragmentation()?;
707 let gc_pressure_analysis = self.analyze_gc_pressure()?;
708
709 let mut leak_summary = HashMap::new();
710 for leak in &potential_leaks {
711 *leak_summary.entry(leak.allocation_type.clone()).or_insert(0) += 1;
712 }
713
714 Ok(MemoryProfilingReport {
715 session_id: self.session_id,
716 start_time: UNIX_EPOCH
717 + Duration::from_secs_f64(
718 SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs_f64() - duration_secs,
719 ),
720 end_time,
721 duration_secs,
722 config: self.config.clone(),
723 peak_memory_mb,
724 average_memory_mb,
725 total_allocations,
726 total_deallocations,
727 net_allocations,
728 memory_timeline: timeline_snapshot,
729 potential_leaks,
730 leak_summary,
731 detected_patterns,
732 fragmentation_analysis,
733 gc_pressure_analysis,
734 allocations_by_type: type_stats_snapshot,
735 allocations_by_size_bucket,
736 profiling_overhead_ms,
737 sampling_accuracy: 0.95, })
739 }
740
741 fn capture_stack_trace(&self) -> Vec<String> {
742 vec![
745 "function_a".to_string(),
746 "function_b".to_string(),
747 "main".to_string(),
748 ]
749 }
750
751 fn update_type_stats(
752 &self,
753 allocation_type: &AllocationType,
754 size: usize,
755 is_allocation: bool,
756 ) {
757 let mut type_stats =
758 self.type_stats.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
759 let stats = type_stats.entry(allocation_type.clone()).or_insert(AllocationTypeStats {
760 total_allocations: 0,
761 total_deallocations: 0,
762 current_count: 0,
763 total_bytes_allocated: 0,
764 total_bytes_deallocated: 0,
765 current_bytes: 0,
766 peak_count: 0,
767 peak_bytes: 0,
768 average_allocation_size: 0.0,
769 largest_allocation: 0,
770 });
771
772 if is_allocation {
773 stats.total_allocations += 1;
774 stats.current_count += 1;
775 stats.total_bytes_allocated += size;
776 stats.current_bytes += size;
777 stats.peak_count = stats.peak_count.max(stats.current_count);
778 stats.peak_bytes = stats.peak_bytes.max(stats.current_bytes);
779 stats.largest_allocation = stats.largest_allocation.max(size);
780 } else {
781 stats.total_deallocations += 1;
782 stats.current_count = stats.current_count.saturating_sub(1);
783 stats.total_bytes_deallocated += size;
784 stats.current_bytes = stats.current_bytes.saturating_sub(size);
785 }
786
787 stats.average_allocation_size = if stats.total_allocations > 0 {
788 stats.total_bytes_allocated as f64 / stats.total_allocations as f64
789 } else {
790 0.0
791 };
792 }
793
794 fn get_size_bucket(&self, size: usize) -> String {
795 match size {
796 0..=1024 => "0-1KB".to_string(),
797 1025..=10240 => "1-10KB".to_string(),
798 10241..=102400 => "10-100KB".to_string(),
799 102401..=1048576 => "100KB-1MB".to_string(),
800 1048577..=10485760 => "1-10MB".to_string(),
801 _ => ">10MB".to_string(),
802 }
803 }
804
805 fn classify_leak_severity(&self, size: usize, age_seconds: f64) -> LeakSeverity {
806 let large_size = size > self.config.large_allocation_threshold;
807 let old_age = age_seconds > 1800.0; let very_old_age = age_seconds > 3600.0; match (large_size, old_age, very_old_age) {
811 (true, _, true) => LeakSeverity::Critical,
812 (true, true, _) => LeakSeverity::High,
813 (true, false, _) => LeakSeverity::Medium,
814 (false, true, _) => LeakSeverity::Medium,
815 _ => LeakSeverity::Low,
816 }
817 }
818
819 fn calculate_gc_pressure_score(&self) -> f64 {
820 0.3 }
825
826 fn calculate_allocation_rates(&self, timeline: &VecDeque<MemorySnapshot>) -> (f64, f64) {
827 if timeline.len() < 2 {
828 return (0.0, 0.0);
829 }
830
831 let first = &timeline[0];
833 let last = &timeline[timeline.len() - 1];
834
835 let duration = last
836 .timestamp
837 .duration_since(first.timestamp)
838 .unwrap_or(Duration::from_secs(1))
839 .as_secs_f64();
840
841 let allocation_rate =
842 (last.allocation_count as f64 - first.allocation_count as f64) / duration;
843 let deallocation_rate = (last.free_count as f64 - first.free_count as f64) / duration;
844
845 (allocation_rate.max(0.0), deallocation_rate.max(0.0))
846 }
847
848 fn detect_leak_pattern(&self) -> Result<AllocationPattern> {
851 let leaks = self.detect_leaks()?;
852 let high_severity_leaks = leaks
853 .iter()
854 .filter(|l| l.severity == LeakSeverity::High || l.severity == LeakSeverity::Critical)
855 .count();
856
857 let confidence = if leaks.len() > 10 { 0.9 } else { 0.5 };
858 let impact_score = (high_severity_leaks as f64 / (leaks.len().max(1)) as f64).min(1.0);
859
860 Ok(AllocationPattern {
861 pattern_type: PatternType::MemoryLeak,
862 description: format!("Detected {} potential memory leaks", leaks.len()),
863 confidence,
864 impact_score,
865 recommendations: vec![
866 "Review long-lived allocations for proper cleanup".to_string(),
867 "Implement RAII patterns for automatic resource management".to_string(),
868 ],
869 examples: leaks
870 .into_iter()
871 .take(3)
872 .map(|leak| {
873 AllocationRecord {
875 id: leak.allocation_id,
876 size: leak.size,
877 timestamp: SystemTime::now(), stack_trace: leak.stack_trace,
879 allocation_type: leak.allocation_type,
880 freed: false,
881 freed_at: None,
882 tags: leak.tags,
883 }
884 })
885 .collect(),
886 })
887 }
888
889 fn detect_churn_pattern(&self) -> Result<AllocationPattern> {
890 let allocations = self.allocations.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
892 let short_lived_count = allocations
893 .values()
894 .filter(|record| {
895 if let (Some(_freed_at), false) = (record.freed_at, record.freed) {
896 false } else if record.freed {
898 if let Some(freed_at) = record.freed_at {
899 freed_at.duration_since(record.timestamp).unwrap_or(Duration::from_secs(0))
900 < Duration::from_secs(1)
901 } else {
902 false
903 }
904 } else {
905 false
906 }
907 })
908 .count();
909
910 let total_count = allocations.len();
911 let churn_ratio = if total_count > 0 {
912 short_lived_count as f64 / total_count as f64
913 } else {
914 0.0
915 };
916
917 Ok(AllocationPattern {
918 pattern_type: PatternType::ChurningAllocations,
919 description: format!(
920 "High allocation churn detected: {:.1}% short-lived allocations",
921 churn_ratio * 100.0
922 ),
923 confidence: if churn_ratio > 0.5 { 0.8 } else { 0.4 },
924 impact_score: churn_ratio,
925 recommendations: vec![
926 "Consider object pooling for frequently allocated objects".to_string(),
927 "Reduce temporary object creation in hot paths".to_string(),
928 ],
929 examples: vec![], })
931 }
932
933 fn detect_large_allocation_pattern(&self) -> Result<AllocationPattern> {
934 let allocations = self.allocations.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
935 let large_allocations: Vec<_> = allocations
936 .values()
937 .filter(|record| record.size > self.config.large_allocation_threshold)
938 .cloned()
939 .collect();
940
941 let impact_score = if !allocations.is_empty() {
942 large_allocations.len() as f64 / allocations.len() as f64
943 } else {
944 0.0
945 };
946
947 Ok(AllocationPattern {
948 pattern_type: PatternType::LargeAllocations,
949 description: format!(
950 "Found {} large allocations (>{}MB)",
951 large_allocations.len(),
952 self.config.large_allocation_threshold / 1024 / 1024
953 ),
954 confidence: if large_allocations.len() > 5 { 0.9 } else { 0.6 },
955 impact_score,
956 recommendations: vec![
957 "Review large allocations for optimization opportunities".to_string(),
958 "Consider streaming or chunked processing for large data".to_string(),
959 ],
960 examples: large_allocations.into_iter().take(3).collect(),
961 })
962 }
963
964 fn detect_fragmentation_pattern(&self) -> Result<AllocationPattern> {
965 let fragmentation = self.analyze_fragmentation()?;
966
967 Ok(AllocationPattern {
968 pattern_type: PatternType::FragmentationCausing,
969 description: format!(
970 "Memory fragmentation at {:.1}%",
971 fragmentation.fragmentation_ratio * 100.0
972 ),
973 confidence: 0.8,
974 impact_score: fragmentation.fragmentation_ratio,
975 recommendations: fragmentation.recommendations,
976 examples: vec![], })
978 }
979}
980
981impl PartialOrd for LeakSeverity {
982 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
983 Some(self.cmp(other))
984 }
985}
986
987impl Ord for LeakSeverity {
988 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
989 let self_val = match self {
990 LeakSeverity::Low => 0,
991 LeakSeverity::Medium => 1,
992 LeakSeverity::High => 2,
993 LeakSeverity::Critical => 3,
994 };
995 let other_val = match other {
996 LeakSeverity::Low => 0,
997 LeakSeverity::Medium => 1,
998 LeakSeverity::High => 2,
999 LeakSeverity::Critical => 3,
1000 };
1001 self_val.cmp(&other_val)
1002 }
1003}
1004
1005#[cfg(test)]
1006mod tests {
1007 use super::*;
1008 use tokio;
1009
1010 #[tokio::test(flavor = "multi_thread")]
1011 #[ignore] async fn test_memory_profiler_basic() -> Result<()> {
1013 let config = MemoryProfilingConfig {
1014 sampling_interval_ms: 1000, ..Default::default()
1016 };
1017 let mut profiler = MemoryProfiler::new(config);
1018
1019 let test_result = tokio::time::timeout(Duration::from_millis(500), async {
1021 profiler.start().await?;
1022
1023 let alloc_id1 = profiler.record_allocation(
1025 1024,
1026 AllocationType::Tensor,
1027 vec!["test".to_string()],
1028 )?;
1029
1030 let _alloc_id2 = profiler.record_allocation(
1031 2048,
1032 AllocationType::Buffer,
1033 vec!["test".to_string()],
1034 )?;
1035
1036 profiler.record_deallocation(alloc_id1)?;
1038
1039 tokio::time::sleep(Duration::from_millis(1)).await;
1041
1042 let report = profiler.stop().await?;
1043
1044 assert_eq!(report.total_allocations, 2);
1045 assert_eq!(report.total_deallocations, 1);
1046 assert_eq!(report.net_allocations, 1);
1047
1048 Ok::<(), anyhow::Error>(())
1049 })
1050 .await;
1051
1052 match test_result {
1053 Ok(result) => result,
1054 Err(_) => Err(anyhow::anyhow!("Test timed out after 500ms")),
1055 }
1056 }
1057
1058 #[tokio::test]
1059 async fn test_leak_detection() -> Result<()> {
1060 let config = MemoryProfilingConfig {
1061 leak_detection_threshold_secs: 1, ..Default::default()
1063 };
1064
1065 let mut profiler = MemoryProfiler::new(config);
1066 profiler.start().await?; profiler.record_allocation(1024, AllocationType::Tensor, vec!["leak_test".to_string()])?;
1070
1071 tokio::time::sleep(Duration::from_secs(2)).await;
1072
1073 let leaks = profiler.detect_leaks()?;
1074 assert!(!leaks.is_empty());
1075
1076 Ok(())
1077 }
1078
1079 #[test]
1080 fn test_size_buckets() {
1081 let config = MemoryProfilingConfig::default();
1082 let profiler = MemoryProfiler::new(config);
1083
1084 assert_eq!(profiler.get_size_bucket(512), "0-1KB");
1085 assert_eq!(profiler.get_size_bucket(5120), "1-10KB");
1086 assert_eq!(profiler.get_size_bucket(51200), "10-100KB");
1087 assert_eq!(profiler.get_size_bucket(512000), "100KB-1MB");
1088 assert_eq!(profiler.get_size_bucket(5120000), "1-10MB");
1089 assert_eq!(profiler.get_size_bucket(51200000), ">10MB");
1090 }
1091
1092 #[test]
1093 fn test_leak_severity_classification() {
1094 let config = MemoryProfilingConfig::default();
1095 let profiler = MemoryProfiler::new(config);
1096
1097 assert_eq!(
1099 profiler.classify_leak_severity(1024, 60.0),
1100 LeakSeverity::Low
1101 );
1102
1103 assert_eq!(
1105 profiler.classify_leak_severity(10485760, 3700.0),
1106 LeakSeverity::Critical
1107 );
1108
1109 assert_eq!(
1111 profiler.classify_leak_severity(524288, 1900.0),
1112 LeakSeverity::Medium
1113 );
1114 }
1115}