Skip to main content

ipfrs_tensorlogic/
memory_profiler.rs

1//! Memory profiling utilities for tracking allocations and memory usage.
2//!
3//! This module provides tools for:
4//! - Tracking heap allocations
5//! - Monitoring shared memory usage
6//! - Detecting potential memory leaks
7//! - Measuring peak memory consumption
8//!
9//! # Examples
10//!
11//! ```
12//! use ipfrs_tensorlogic::MemoryProfiler;
13//!
14//! let profiler = MemoryProfiler::new();
15//!
16//! {
17//!     let _guard = profiler.start_tracking("my_operation");
18//!     // Your operation here
19//!     let data = vec![0u8; 1024 * 1024]; // 1 MB allocation
20//!     drop(data);
21//! }
22//!
23//! let stats = profiler.get_stats("my_operation").expect("example: should succeed in docs");
24//! println!("Peak memory: {} bytes", stats.peak_bytes);
25//! ```
26
27use parking_lot::RwLock;
28use std::collections::HashMap;
29use std::sync::Arc;
30use std::time::{Duration, Instant};
31
32/// Memory usage statistics for a tracked operation
33#[derive(Debug, Clone)]
34pub struct MemoryStats {
35    /// Number of times this operation was tracked
36    pub track_count: usize,
37    /// Total bytes allocated (cumulative across all tracks)
38    pub total_bytes: usize,
39    /// Peak bytes used during any single track
40    pub peak_bytes: usize,
41    /// Average bytes per track
42    pub avg_bytes: usize,
43    /// Total duration tracked
44    pub total_duration: Duration,
45    /// Average duration per track
46    pub avg_duration: Duration,
47}
48
49impl MemoryStats {
50    fn new() -> Self {
51        Self {
52            track_count: 0,
53            total_bytes: 0,
54            peak_bytes: 0,
55            avg_bytes: 0,
56            total_duration: Duration::ZERO,
57            avg_duration: Duration::ZERO,
58        }
59    }
60
61    fn update(&mut self, bytes: usize, duration: Duration) {
62        self.track_count += 1;
63        self.total_bytes += bytes;
64        self.peak_bytes = self.peak_bytes.max(bytes);
65        self.total_duration += duration;
66
67        self.avg_bytes = self.total_bytes.checked_div(self.track_count).unwrap_or(0);
68        self.avg_duration = self
69            .total_duration
70            .checked_div(self.track_count as u32)
71            .unwrap_or(Duration::ZERO);
72    }
73}
74
75/// A guard that tracks memory usage for the duration of its lifetime
76pub struct MemoryTrackingGuard {
77    profiler: Arc<MemoryProfiler>,
78    operation: String,
79    start_time: Instant,
80    initial_memory: usize,
81}
82
83impl Drop for MemoryTrackingGuard {
84    fn drop(&mut self) {
85        let duration = self.start_time.elapsed();
86        let current_memory = self.profiler.get_current_memory_usage();
87        let bytes_used = current_memory.saturating_sub(self.initial_memory);
88
89        let mut stats = self.profiler.stats.write();
90        let entry = stats
91            .entry(self.operation.clone())
92            .or_insert_with(MemoryStats::new);
93        entry.update(bytes_used, duration);
94    }
95}
96
97/// Memory profiler for tracking allocations and usage
98pub struct MemoryProfiler {
99    stats: Arc<RwLock<HashMap<String, MemoryStats>>>,
100}
101
102impl MemoryProfiler {
103    /// Create a new memory profiler
104    pub fn new() -> Arc<Self> {
105        Arc::new(Self {
106            stats: Arc::new(RwLock::new(HashMap::new())),
107        })
108    }
109
110    /// Start tracking memory usage for an operation
111    ///
112    /// Returns a guard that will record statistics when dropped.
113    pub fn start_tracking(self: &Arc<Self>, operation: &str) -> MemoryTrackingGuard {
114        MemoryTrackingGuard {
115            profiler: Arc::clone(self),
116            operation: operation.to_string(),
117            start_time: Instant::now(),
118            initial_memory: self.get_current_memory_usage(),
119        }
120    }
121
122    /// Get statistics for a specific operation
123    pub fn get_stats(&self, operation: &str) -> Option<MemoryStats> {
124        self.stats.read().get(operation).cloned()
125    }
126
127    /// Get all tracked statistics
128    pub fn get_all_stats(&self) -> HashMap<String, MemoryStats> {
129        self.stats.read().clone()
130    }
131
132    /// Clear all statistics
133    pub fn clear(&self) {
134        self.stats.write().clear();
135    }
136
137    /// Get current memory usage in bytes
138    ///
139    /// This is a platform-specific approximation based on available system information.
140    #[cfg(target_os = "linux")]
141    fn get_current_memory_usage(&self) -> usize {
142        // On Linux, read from /proc/self/statm
143        if let Ok(contents) = std::fs::read_to_string("/proc/self/statm") {
144            if let Some(first) = contents.split_whitespace().next() {
145                if let Ok(pages) = first.parse::<usize>() {
146                    // Each page is typically 4096 bytes
147                    return pages * 4096;
148                }
149            }
150        }
151        0
152    }
153
154    #[cfg(not(target_os = "linux"))]
155    fn get_current_memory_usage(&self) -> usize {
156        // For non-Linux systems, we can't easily get RSS without platform-specific code
157        // Return 0 as a placeholder
158        0
159    }
160
161    /// Generate a memory profiling report
162    pub fn generate_report(&self) -> MemoryProfilingReport {
163        let stats = self.get_all_stats();
164        let total_operations = stats.len();
165        let total_tracked = stats.values().map(|s| s.track_count).sum();
166        let total_bytes: usize = stats.values().map(|s| s.total_bytes).sum();
167        let max_peak = stats.values().map(|s| s.peak_bytes).max().unwrap_or(0);
168
169        let mut operations: Vec<_> = stats.into_iter().collect();
170        operations.sort_by_key(|a| std::cmp::Reverse(a.1.peak_bytes));
171
172        MemoryProfilingReport {
173            total_operations,
174            total_tracked,
175            total_bytes,
176            max_peak_bytes: max_peak,
177            operations,
178        }
179    }
180}
181
182impl Default for MemoryProfiler {
183    fn default() -> Self {
184        Self {
185            stats: Arc::new(RwLock::new(HashMap::new())),
186        }
187    }
188}
189
190/// A comprehensive memory profiling report
191#[derive(Debug)]
192pub struct MemoryProfilingReport {
193    /// Total number of distinct operations tracked
194    pub total_operations: usize,
195    /// Total number of tracking instances
196    pub total_tracked: usize,
197    /// Total bytes allocated across all operations
198    pub total_bytes: usize,
199    /// Maximum peak memory usage across all operations
200    pub max_peak_bytes: usize,
201    /// Operations sorted by peak memory usage (descending)
202    pub operations: Vec<(String, MemoryStats)>,
203}
204
205impl MemoryProfilingReport {
206    /// Print a formatted report to stdout
207    pub fn print(&self) {
208        println!("=== Memory Profiling Report ===");
209        println!("Total operations: {}", self.total_operations);
210        println!("Total tracks: {}", self.total_tracked);
211        println!(
212            "Total bytes: {} ({:.2} MB)",
213            self.total_bytes,
214            self.total_bytes as f64 / 1024.0 / 1024.0
215        );
216        println!(
217            "Max peak: {} ({:.2} MB)",
218            self.max_peak_bytes,
219            self.max_peak_bytes as f64 / 1024.0 / 1024.0
220        );
221        println!("\nTop memory-consuming operations:");
222        println!(
223            "{:<40} {:>12} {:>12} {:>12} {:>10}",
224            "Operation", "Tracks", "Peak", "Avg", "Avg Time"
225        );
226        println!(
227            "{:-<40} {:-<12} {:-<12} {:-<12} {:-<10}",
228            "", "", "", "", ""
229        );
230
231        for (i, (name, stats)) in self.operations.iter().enumerate().take(10) {
232            println!(
233                "{:<40} {:>12} {:>12} {:>12} {:>10?}",
234                if name.len() > 40 {
235                    format!("{}...", &name[..37])
236                } else {
237                    name.clone()
238                },
239                stats.track_count,
240                format_bytes(stats.peak_bytes),
241                format_bytes(stats.avg_bytes),
242                stats.avg_duration
243            );
244            if i >= 9 {
245                break;
246            }
247        }
248    }
249}
250
251/// Format bytes in human-readable form
252fn format_bytes(bytes: usize) -> String {
253    if bytes < 1024 {
254        format!("{} B", bytes)
255    } else if bytes < 1024 * 1024 {
256        format!("{:.1} KB", bytes as f64 / 1024.0)
257    } else if bytes < 1024 * 1024 * 1024 {
258        format!("{:.1} MB", bytes as f64 / 1024.0 / 1024.0)
259    } else {
260        format!("{:.1} GB", bytes as f64 / 1024.0 / 1024.0 / 1024.0)
261    }
262}
263
264#[cfg(test)]
265mod tests {
266    use super::*;
267
268    #[test]
269    fn test_memory_profiler_basic() {
270        let profiler = MemoryProfiler::new();
271
272        {
273            let _guard = profiler.start_tracking("test_operation");
274            // Simulate some work
275            std::thread::sleep(Duration::from_millis(10));
276        }
277
278        let stats = profiler.get_stats("test_operation");
279        assert!(stats.is_some());
280
281        let stats = stats.expect("test: should succeed");
282        assert_eq!(stats.track_count, 1);
283        assert!(stats.total_duration >= Duration::from_millis(10));
284    }
285
286    #[test]
287    fn test_memory_profiler_multiple_tracks() {
288        let profiler = MemoryProfiler::new();
289
290        for _ in 0..5 {
291            let _guard = profiler.start_tracking("repeated_op");
292            std::thread::sleep(Duration::from_millis(5));
293        }
294
295        let stats = profiler
296            .get_stats("repeated_op")
297            .expect("test: should succeed");
298        assert_eq!(stats.track_count, 5);
299        assert!(stats.avg_duration >= Duration::from_millis(5));
300    }
301
302    #[test]
303    fn test_memory_profiler_multiple_operations() {
304        let profiler = MemoryProfiler::new();
305
306        {
307            let _guard1 = profiler.start_tracking("op1");
308            std::thread::sleep(Duration::from_millis(5));
309        }
310
311        {
312            let _guard2 = profiler.start_tracking("op2");
313            std::thread::sleep(Duration::from_millis(10));
314        }
315
316        let all_stats = profiler.get_all_stats();
317        assert_eq!(all_stats.len(), 2);
318        assert!(all_stats.contains_key("op1"));
319        assert!(all_stats.contains_key("op2"));
320    }
321
322    #[test]
323    fn test_memory_profiler_clear() {
324        let profiler = MemoryProfiler::new();
325
326        {
327            let _guard = profiler.start_tracking("test");
328        }
329
330        assert_eq!(profiler.get_all_stats().len(), 1);
331
332        profiler.clear();
333        assert_eq!(profiler.get_all_stats().len(), 0);
334    }
335
336    #[test]
337    fn test_memory_profiler_report() {
338        let profiler = MemoryProfiler::new();
339
340        {
341            let _guard = profiler.start_tracking("op1");
342        }
343
344        {
345            let _guard = profiler.start_tracking("op2");
346        }
347
348        let report = profiler.generate_report();
349        assert_eq!(report.total_operations, 2);
350        assert_eq!(report.total_tracked, 2);
351    }
352
353    #[test]
354    fn test_format_bytes() {
355        assert_eq!(format_bytes(512), "512 B");
356        assert_eq!(format_bytes(1024), "1.0 KB");
357        assert_eq!(format_bytes(1024 * 1024), "1.0 MB");
358        assert_eq!(format_bytes(1024 * 1024 * 1024), "1.0 GB");
359    }
360}