Skip to main content

scirs2_core/profiling/
memory_profiling.rs

1//! # Advanced Memory Profiling for SciRS2 v0.2.0
2//!
3//! This module provides comprehensive memory profiling capabilities using jemalloc.
4//! It enables heap profiling, memory leak detection, and allocation pattern analysis.
5//!
6//! # Features
7//!
8//! - **Heap Profiling**: Track memory allocations and deallocations
9//! - **Leak Detection**: Identify memory leaks
10//! - **Allocation Patterns**: Analyze allocation patterns
11//! - **Statistics**: Detailed memory statistics
12//! - **Zero Overhead**: Disabled by default, minimal overhead when enabled
13//!
14//! # Example
15//!
16//! ```rust,no_run
17//! use scirs2_core::profiling::memory_profiling::{MemoryProfiler, enable_profiling};
18//!
19//! // Enable memory profiling
20//! enable_profiling().expect("Failed to enable profiling");
21//!
22//! // ... perform allocations ...
23//!
24//! // Get memory statistics
25//! let stats = MemoryProfiler::get_stats().expect("Failed to get stats");
26//! println!("Allocated: {} bytes", stats.allocated);
27//! println!("Resident: {} bytes", stats.resident);
28//! ```
29
30#[cfg(feature = "profiling_memory")]
31use crate::CoreResult;
32#[cfg(feature = "profiling_memory")]
33use std::collections::HashMap;
34#[cfg(feature = "profiling_memory")]
35use tikv_jemalloc_ctl::{epoch, stats};
36
37/// Memory statistics from jemalloc
38#[cfg(feature = "profiling_memory")]
39#[derive(Debug, Clone)]
40pub struct MemoryStats {
41    /// Total allocated memory (bytes)
42    pub allocated: usize,
43    /// Resident memory (bytes)
44    pub resident: usize,
45    /// Mapped memory (bytes)
46    pub mapped: usize,
47    /// Metadata memory (bytes)
48    pub metadata: usize,
49    /// Retained memory (bytes)
50    pub retained: usize,
51}
52
53#[cfg(feature = "profiling_memory")]
54impl MemoryStats {
55    /// Get current memory statistics
56    pub fn current() -> CoreResult<Self> {
57        // Update the epoch to get fresh statistics
58        epoch::mib()
59            .map_err(|e| {
60                crate::CoreError::ConfigError(crate::error::ErrorContext::new(format!(
61                    "Failed to get epoch: {}",
62                    e
63                )))
64            })?
65            .advance()
66            .map_err(|e| {
67                crate::CoreError::ConfigError(crate::error::ErrorContext::new(format!(
68                    "Failed to advance epoch: {}",
69                    e
70                )))
71            })?;
72
73        let allocated = stats::allocated::mib()
74            .map_err(|e| {
75                crate::CoreError::ConfigError(crate::error::ErrorContext::new(format!(
76                    "Failed to get allocated: {}",
77                    e
78                )))
79            })?
80            .read()
81            .map_err(|e| {
82                crate::CoreError::ConfigError(crate::error::ErrorContext::new(format!(
83                    "Failed to read allocated: {}",
84                    e
85                )))
86            })?;
87
88        let resident = stats::resident::mib()
89            .map_err(|e| {
90                crate::CoreError::ConfigError(crate::error::ErrorContext::new(format!(
91                    "Failed to get resident: {}",
92                    e
93                )))
94            })?
95            .read()
96            .map_err(|e| {
97                crate::CoreError::ConfigError(crate::error::ErrorContext::new(format!(
98                    "Failed to read resident: {}",
99                    e
100                )))
101            })?;
102
103        let mapped = stats::mapped::mib()
104            .map_err(|e| {
105                crate::CoreError::ConfigError(crate::error::ErrorContext::new(format!(
106                    "Failed to get mapped: {}",
107                    e
108                )))
109            })?
110            .read()
111            .map_err(|e| {
112                crate::CoreError::ConfigError(crate::error::ErrorContext::new(format!(
113                    "Failed to read mapped: {}",
114                    e
115                )))
116            })?;
117
118        let metadata = stats::metadata::mib()
119            .map_err(|e| {
120                crate::CoreError::ConfigError(crate::error::ErrorContext::new(format!(
121                    "Failed to get metadata: {}",
122                    e
123                )))
124            })?
125            .read()
126            .map_err(|e| {
127                crate::CoreError::ConfigError(crate::error::ErrorContext::new(format!(
128                    "Failed to read metadata: {}",
129                    e
130                )))
131            })?;
132
133        let retained = stats::retained::mib()
134            .map_err(|e| {
135                crate::CoreError::ConfigError(crate::error::ErrorContext::new(format!(
136                    "Failed to get retained: {}",
137                    e
138                )))
139            })?
140            .read()
141            .map_err(|e| {
142                crate::CoreError::ConfigError(crate::error::ErrorContext::new(format!(
143                    "Failed to read retained: {}",
144                    e
145                )))
146            })?;
147
148        Ok(Self {
149            allocated,
150            resident,
151            mapped,
152            metadata,
153            retained,
154        })
155    }
156
157    /// Calculate memory overhead (metadata / allocated)
158    pub fn overhead_ratio(&self) -> f64 {
159        if self.allocated == 0 {
160            0.0
161        } else {
162            self.metadata as f64 / self.allocated as f64
163        }
164    }
165
166    /// Calculate memory utilization (allocated / resident)
167    pub fn utilization_ratio(&self) -> f64 {
168        if self.resident == 0 {
169            0.0
170        } else {
171            self.allocated as f64 / self.resident as f64
172        }
173    }
174
175    /// Format as human-readable string
176    pub fn format(&self) -> String {
177        format!(
178            "Memory Stats:\n\
179             - Allocated: {} MB\n\
180             - Resident:  {} MB\n\
181             - Mapped:    {} MB\n\
182             - Metadata:  {} MB\n\
183             - Retained:  {} MB\n\
184             - Overhead:  {:.2}%\n\
185             - Utilization: {:.2}%",
186            self.allocated / 1_048_576,
187            self.resident / 1_048_576,
188            self.mapped / 1_048_576,
189            self.metadata / 1_048_576,
190            self.retained / 1_048_576,
191            self.overhead_ratio() * 100.0,
192            self.utilization_ratio() * 100.0
193        )
194    }
195}
196
197/// Memory profiler
198#[cfg(feature = "profiling_memory")]
199pub struct MemoryProfiler {
200    baseline: Option<MemoryStats>,
201}
202
203#[cfg(feature = "profiling_memory")]
204impl MemoryProfiler {
205    /// Create a new memory profiler
206    pub fn new() -> Self {
207        Self { baseline: None }
208    }
209
210    /// Set the baseline memory statistics
211    pub fn set_baseline(&mut self) -> CoreResult<()> {
212        self.baseline = Some(MemoryStats::current()?);
213        Ok(())
214    }
215
216    /// Get current memory statistics
217    pub fn get_stats() -> CoreResult<MemoryStats> {
218        MemoryStats::current()
219    }
220
221    /// Get memory delta from baseline
222    pub fn get_delta(&self) -> CoreResult<Option<MemoryDelta>> {
223        if let Some(ref baseline) = self.baseline {
224            let current = MemoryStats::current()?;
225            Ok(Some(MemoryDelta {
226                allocated_delta: current.allocated as i64 - baseline.allocated as i64,
227                resident_delta: current.resident as i64 - baseline.resident as i64,
228                mapped_delta: current.mapped as i64 - baseline.mapped as i64,
229                metadata_delta: current.metadata as i64 - baseline.metadata as i64,
230                retained_delta: current.retained as i64 - baseline.retained as i64,
231            }))
232        } else {
233            Ok(None)
234        }
235    }
236
237    /// Print memory statistics
238    pub fn print_stats() -> CoreResult<()> {
239        let stats = Self::get_stats()?;
240        println!("{}", stats.format());
241        Ok(())
242    }
243}
244
245#[cfg(feature = "profiling_memory")]
246impl Default for MemoryProfiler {
247    fn default() -> Self {
248        Self::new()
249    }
250}
251
252/// Memory delta from baseline
253#[cfg(feature = "profiling_memory")]
254#[derive(Debug, Clone)]
255pub struct MemoryDelta {
256    pub allocated_delta: i64,
257    pub resident_delta: i64,
258    pub mapped_delta: i64,
259    pub metadata_delta: i64,
260    pub retained_delta: i64,
261}
262
263#[cfg(feature = "profiling_memory")]
264impl MemoryDelta {
265    /// Format as human-readable string
266    pub fn format(&self) -> String {
267        format!(
268            "Memory Delta:\n\
269             - Allocated: {:+} MB\n\
270             - Resident:  {:+} MB\n\
271             - Mapped:    {:+} MB\n\
272             - Metadata:  {:+} MB\n\
273             - Retained:  {:+} MB",
274            self.allocated_delta / 1_048_576,
275            self.resident_delta / 1_048_576,
276            self.mapped_delta / 1_048_576,
277            self.metadata_delta / 1_048_576,
278            self.retained_delta / 1_048_576
279        )
280    }
281}
282
283/// Allocation tracker for detecting patterns
284#[cfg(feature = "profiling_memory")]
285pub struct AllocationTracker {
286    snapshots: Vec<(String, MemoryStats)>,
287}
288
289#[cfg(feature = "profiling_memory")]
290impl AllocationTracker {
291    /// Create a new allocation tracker
292    pub fn new() -> Self {
293        Self {
294            snapshots: Vec::new(),
295        }
296    }
297
298    /// Take a snapshot with a label
299    pub fn snapshot(&mut self, label: impl Into<String>) -> CoreResult<()> {
300        let stats = MemoryStats::current()?;
301        self.snapshots.push((label.into(), stats));
302        Ok(())
303    }
304
305    /// Get all snapshots
306    pub fn snapshots(&self) -> &[(String, MemoryStats)] {
307        &self.snapshots
308    }
309
310    /// Analyze allocation patterns
311    pub fn analyze(&self) -> AllocationAnalysis {
312        if self.snapshots.is_empty() {
313            return AllocationAnalysis {
314                total_allocated: 0,
315                peak_allocated: 0,
316                total_snapshots: 0,
317                largest_increase: None,
318                patterns: HashMap::new(),
319            };
320        }
321
322        let mut peak_allocated = 0;
323        let mut largest_increase: Option<(String, i64)> = None;
324
325        for i in 0..self.snapshots.len() {
326            let (ref label, ref stats) = self.snapshots[i];
327
328            if stats.allocated > peak_allocated {
329                peak_allocated = stats.allocated;
330            }
331
332            if i > 0 {
333                let prev_stats = &self.snapshots[i - 1].1;
334                let increase = stats.allocated as i64 - prev_stats.allocated as i64;
335
336                if let Some((_, max_increase)) = largest_increase {
337                    if increase > max_increase {
338                        largest_increase = Some((label.clone(), increase));
339                    }
340                } else {
341                    largest_increase = Some((label.clone(), increase));
342                }
343            }
344        }
345
346        let last_stats = &self
347            .snapshots
348            .last()
349            .expect("Expected at least one snapshot")
350            .1;
351
352        AllocationAnalysis {
353            total_allocated: last_stats.allocated,
354            peak_allocated,
355            total_snapshots: self.snapshots.len(),
356            largest_increase,
357            patterns: HashMap::new(),
358        }
359    }
360
361    /// Clear all snapshots
362    pub fn clear(&mut self) {
363        self.snapshots.clear();
364    }
365}
366
367#[cfg(feature = "profiling_memory")]
368impl Default for AllocationTracker {
369    fn default() -> Self {
370        Self::new()
371    }
372}
373
374/// Allocation pattern analysis
375#[cfg(feature = "profiling_memory")]
376#[derive(Debug, Clone)]
377pub struct AllocationAnalysis {
378    pub total_allocated: usize,
379    pub peak_allocated: usize,
380    pub total_snapshots: usize,
381    pub largest_increase: Option<(String, i64)>,
382    pub patterns: HashMap<String, usize>,
383}
384
385/// Enable memory profiling
386#[cfg(feature = "profiling_memory")]
387pub fn enable_profiling() -> CoreResult<()> {
388    // In jemalloc, profiling is controlled at compile time or via environment variables
389    // This is a no-op but provided for API consistency
390    Ok(())
391}
392
393/// Disable memory profiling
394#[cfg(feature = "profiling_memory")]
395pub fn disable_profiling() -> CoreResult<()> {
396    // This is a no-op but provided for API consistency
397    Ok(())
398}
399
400/// Stub implementations when profiling_memory feature is disabled
401#[cfg(not(feature = "profiling_memory"))]
402use crate::CoreResult;
403
404#[cfg(not(feature = "profiling_memory"))]
405#[derive(Debug, Clone)]
406pub struct MemoryStats {
407    pub allocated: usize,
408    pub resident: usize,
409    pub mapped: usize,
410    pub metadata: usize,
411    pub retained: usize,
412}
413
414#[cfg(not(feature = "profiling_memory"))]
415impl MemoryStats {
416    pub fn current() -> CoreResult<Self> {
417        Ok(Self {
418            allocated: 0,
419            resident: 0,
420            mapped: 0,
421            metadata: 0,
422            retained: 0,
423        })
424    }
425
426    pub fn format(&self) -> String {
427        "Memory profiling not enabled".to_string()
428    }
429}
430
431#[cfg(not(feature = "profiling_memory"))]
432pub struct MemoryProfiler;
433
434#[cfg(not(feature = "profiling_memory"))]
435impl MemoryProfiler {
436    pub fn new() -> Self {
437        Self
438    }
439    pub fn get_stats() -> CoreResult<MemoryStats> {
440        MemoryStats::current()
441    }
442    pub fn print_stats() -> CoreResult<()> {
443        Ok(())
444    }
445}
446
447#[cfg(not(feature = "profiling_memory"))]
448pub fn enable_profiling() -> CoreResult<()> {
449    Ok(())
450}
451
452#[cfg(test)]
453#[cfg(feature = "profiling_memory")]
454mod tests {
455    use super::*;
456
457    #[test]
458    fn test_memory_stats() {
459        let stats = MemoryStats::current();
460        assert!(stats.is_ok());
461
462        if let Ok(s) = stats {
463            println!("{}", s.format());
464        }
465    }
466
467    #[test]
468    fn test_memory_profiler() {
469        let mut profiler = MemoryProfiler::new();
470        assert!(profiler.set_baseline().is_ok());
471
472        // Allocate some memory
473        let _vec: Vec<u8> = vec![0; 1_000_000];
474
475        let delta = profiler.get_delta();
476        assert!(delta.is_ok());
477    }
478
479    #[test]
480    fn test_allocation_tracker() {
481        let mut tracker = AllocationTracker::new();
482
483        assert!(tracker.snapshot("baseline").is_ok());
484
485        // Allocate some memory
486        let _vec: Vec<u8> = vec![0; 1_000_000];
487
488        assert!(tracker.snapshot("after_alloc").is_ok());
489
490        let analysis = tracker.analyze();
491        assert_eq!(analysis.total_snapshots, 2);
492    }
493
494    #[test]
495    fn test_memory_delta() {
496        let delta = MemoryDelta {
497            allocated_delta: 1_048_576,
498            resident_delta: 2_097_152,
499            mapped_delta: 0,
500            metadata_delta: 0,
501            retained_delta: 0,
502        };
503
504        let formatted = delta.format();
505        assert!(formatted.contains("Allocated"));
506    }
507}