Skip to main content

mabi_modbus/testing/
memory.rs

1//! Memory profiling utilities for detecting leaks and measuring usage.
2//!
3//! This module provides tools for:
4//! - Tracking memory allocations
5//! - Detecting memory leaks
6//! - Profiling memory usage over time
7//! - Generating memory reports
8
9use std::sync::atomic::{AtomicU64, Ordering};
10use std::sync::Arc;
11use std::time::{Duration, Instant};
12
13use parking_lot::RwLock;
14
15/// Memory snapshot at a point in time.
16#[derive(Debug, Clone)]
17pub struct MemorySnapshot {
18    /// Timestamp when snapshot was taken.
19    pub timestamp: Instant,
20    /// Total allocated bytes.
21    pub allocated_bytes: u64,
22    /// Total number of allocations.
23    pub allocation_count: u64,
24    /// Total freed bytes.
25    pub freed_bytes: u64,
26    /// Number of deallocations.
27    pub deallocation_count: u64,
28    /// Current live allocations (allocated - freed).
29    pub live_bytes: u64,
30    /// Current live allocation count.
31    pub live_count: u64,
32    /// Peak memory usage.
33    pub peak_bytes: u64,
34    /// Resident set size (from OS).
35    pub rss_bytes: Option<u64>,
36    /// Heap usage (from allocator if available).
37    pub heap_bytes: Option<u64>,
38}
39
40impl MemorySnapshot {
41    /// Get memory usage in megabytes.
42    pub fn live_mb(&self) -> f64 {
43        self.live_bytes as f64 / (1024.0 * 1024.0)
44    }
45
46    /// Get peak memory in megabytes.
47    pub fn peak_mb(&self) -> f64 {
48        self.peak_bytes as f64 / (1024.0 * 1024.0)
49    }
50
51    /// Get RSS in megabytes.
52    pub fn rss_mb(&self) -> Option<f64> {
53        self.rss_bytes.map(|b| b as f64 / (1024.0 * 1024.0))
54    }
55}
56
57/// Memory profiler for tracking allocations.
58pub struct MemoryProfiler {
59    /// Start time of profiling.
60    start_time: Instant,
61    /// Collected snapshots.
62    snapshots: Arc<RwLock<Vec<MemorySnapshot>>>,
63    /// Allocation tracker.
64    tracker: Arc<AllocationTracker>,
65    /// Sampling interval.
66    sample_interval: Duration,
67    /// Whether profiling is active.
68    active: Arc<AtomicBool>,
69}
70
71use std::sync::atomic::AtomicBool;
72
73impl MemoryProfiler {
74    /// Create a new memory profiler.
75    pub fn new() -> Self {
76        Self {
77            start_time: Instant::now(),
78            snapshots: Arc::new(RwLock::new(Vec::new())),
79            tracker: Arc::new(AllocationTracker::new()),
80            sample_interval: Duration::from_secs(1),
81            active: Arc::new(AtomicBool::new(false)),
82        }
83    }
84
85    /// Set the sampling interval.
86    pub fn with_sample_interval(mut self, interval: Duration) -> Self {
87        self.sample_interval = interval;
88        self
89    }
90
91    /// Start profiling with background sampling.
92    pub fn start(&self) -> MemoryProfilerGuard {
93        self.active.store(true, Ordering::SeqCst);
94
95        let snapshots = self.snapshots.clone();
96        let tracker = self.tracker.clone();
97        let interval = self.sample_interval;
98        let active = self.active.clone();
99
100        // Spawn background sampler
101        tokio::spawn(async move {
102            while active.load(Ordering::Relaxed) {
103                let snapshot = Self::take_snapshot_internal(&tracker);
104                snapshots.write().push(snapshot);
105                tokio::time::sleep(interval).await;
106            }
107        });
108
109        MemoryProfilerGuard {
110            profiler: self,
111        }
112    }
113
114    /// Take a memory snapshot.
115    pub fn snapshot(&self) -> MemorySnapshot {
116        Self::take_snapshot_internal(&self.tracker)
117    }
118
119    fn take_snapshot_internal(tracker: &AllocationTracker) -> MemorySnapshot {
120        let allocated = tracker.allocated_bytes.load(Ordering::Relaxed);
121        let alloc_count = tracker.allocation_count.load(Ordering::Relaxed);
122        let freed = tracker.freed_bytes.load(Ordering::Relaxed);
123        let free_count = tracker.deallocation_count.load(Ordering::Relaxed);
124        let peak = tracker.peak_bytes.load(Ordering::Relaxed);
125
126        let live_bytes = allocated.saturating_sub(freed);
127        let live_count = alloc_count.saturating_sub(free_count);
128
129        MemorySnapshot {
130            timestamp: Instant::now(),
131            allocated_bytes: allocated,
132            allocation_count: alloc_count,
133            freed_bytes: freed,
134            deallocation_count: free_count,
135            live_bytes,
136            live_count,
137            peak_bytes: peak,
138            rss_bytes: Self::get_rss(),
139            heap_bytes: None,
140        }
141    }
142
143    /// Get resident set size from OS.
144    #[cfg(target_os = "linux")]
145    fn get_rss() -> Option<u64> {
146        std::fs::read_to_string("/proc/self/statm")
147            .ok()
148            .and_then(|s| {
149                let parts: Vec<&str> = s.split_whitespace().collect();
150                parts.get(1)?.parse::<u64>().ok().map(|pages| pages * 4096)
151            })
152    }
153
154    #[cfg(target_os = "macos")]
155    fn get_rss() -> Option<u64> {
156        use std::process::Command;
157
158        let output = Command::new("ps")
159            .args(["-o", "rss=", "-p", &std::process::id().to_string()])
160            .output()
161            .ok()?;
162
163        let rss_kb: u64 = String::from_utf8_lossy(&output.stdout)
164            .trim()
165            .parse()
166            .ok()?;
167
168        Some(rss_kb * 1024)
169    }
170
171    #[cfg(not(any(target_os = "linux", target_os = "macos")))]
172    fn get_rss() -> Option<u64> {
173        None
174    }
175
176    /// Generate a memory report.
177    pub fn report(&self) -> MemoryReport {
178        let snapshots = self.snapshots.read().clone();
179        let current = self.snapshot();
180
181        let duration = self.start_time.elapsed();
182
183        // Calculate statistics
184        let peak_bytes = snapshots.iter()
185            .map(|s| s.live_bytes)
186            .max()
187            .unwrap_or(current.live_bytes);
188
189        let avg_bytes = if !snapshots.is_empty() {
190            snapshots.iter().map(|s| s.live_bytes).sum::<u64>() / snapshots.len() as u64
191        } else {
192            current.live_bytes
193        };
194
195        // Detect potential leaks (monotonically increasing memory)
196        let potential_leak = if snapshots.len() >= 10 {
197            let recent: Vec<_> = snapshots.iter().rev().take(10).collect();
198            let increasing = recent.windows(2).all(|w| w[0].live_bytes >= w[1].live_bytes);
199            let growth = recent.first().map(|f| f.live_bytes).unwrap_or(0)
200                .saturating_sub(recent.last().map(|l| l.live_bytes).unwrap_or(0));
201            increasing && growth > 1024 * 1024 // More than 1MB growth
202        } else {
203            false
204        };
205
206        MemoryReport {
207            duration,
208            current_snapshot: current,
209            peak_bytes,
210            avg_bytes,
211            snapshots,
212            potential_leak,
213            allocation_rate: self.tracker.allocation_count.load(Ordering::Relaxed) as f64
214                / duration.as_secs_f64(),
215        }
216    }
217
218    /// Get the allocation tracker for custom tracking.
219    pub fn tracker(&self) -> &Arc<AllocationTracker> {
220        &self.tracker
221    }
222}
223
224impl Default for MemoryProfiler {
225    fn default() -> Self {
226        Self::new()
227    }
228}
229
230/// Guard that stops profiling when dropped.
231pub struct MemoryProfilerGuard<'a> {
232    profiler: &'a MemoryProfiler,
233}
234
235impl<'a> Drop for MemoryProfilerGuard<'a> {
236    fn drop(&mut self) {
237        self.profiler.active.store(false, Ordering::SeqCst);
238    }
239}
240
241/// Allocation tracker for monitoring memory usage.
242pub struct AllocationTracker {
243    pub allocated_bytes: AtomicU64,
244    pub freed_bytes: AtomicU64,
245    pub allocation_count: AtomicU64,
246    pub deallocation_count: AtomicU64,
247    pub peak_bytes: AtomicU64,
248    current_bytes: AtomicU64,
249}
250
251impl AllocationTracker {
252    /// Create a new allocation tracker.
253    pub fn new() -> Self {
254        Self {
255            allocated_bytes: AtomicU64::new(0),
256            freed_bytes: AtomicU64::new(0),
257            allocation_count: AtomicU64::new(0),
258            deallocation_count: AtomicU64::new(0),
259            peak_bytes: AtomicU64::new(0),
260            current_bytes: AtomicU64::new(0),
261        }
262    }
263
264    /// Record an allocation.
265    pub fn record_alloc(&self, size: usize) {
266        self.allocated_bytes.fetch_add(size as u64, Ordering::Relaxed);
267        self.allocation_count.fetch_add(1, Ordering::Relaxed);
268
269        let current = self.current_bytes.fetch_add(size as u64, Ordering::Relaxed) + size as u64;
270
271        // Update peak if necessary
272        let mut peak = self.peak_bytes.load(Ordering::Relaxed);
273        while current > peak {
274            match self.peak_bytes.compare_exchange_weak(
275                peak,
276                current,
277                Ordering::Relaxed,
278                Ordering::Relaxed,
279            ) {
280                Ok(_) => break,
281                Err(p) => peak = p,
282            }
283        }
284    }
285
286    /// Record a deallocation.
287    pub fn record_dealloc(&self, size: usize) {
288        self.freed_bytes.fetch_add(size as u64, Ordering::Relaxed);
289        self.deallocation_count.fetch_add(1, Ordering::Relaxed);
290        self.current_bytes.fetch_sub(size as u64, Ordering::Relaxed);
291    }
292
293    /// Get current live bytes.
294    pub fn live_bytes(&self) -> u64 {
295        self.current_bytes.load(Ordering::Relaxed)
296    }
297
298    /// Get current live allocation count.
299    pub fn live_count(&self) -> u64 {
300        self.allocation_count.load(Ordering::Relaxed)
301            .saturating_sub(self.deallocation_count.load(Ordering::Relaxed))
302    }
303
304    /// Reset all counters.
305    pub fn reset(&self) {
306        self.allocated_bytes.store(0, Ordering::Relaxed);
307        self.freed_bytes.store(0, Ordering::Relaxed);
308        self.allocation_count.store(0, Ordering::Relaxed);
309        self.deallocation_count.store(0, Ordering::Relaxed);
310        self.peak_bytes.store(0, Ordering::Relaxed);
311        self.current_bytes.store(0, Ordering::Relaxed);
312    }
313}
314
315impl Default for AllocationTracker {
316    fn default() -> Self {
317        Self::new()
318    }
319}
320
321/// Memory profiling report.
322#[derive(Debug, Clone)]
323pub struct MemoryReport {
324    /// Total profiling duration.
325    pub duration: Duration,
326    /// Current memory snapshot.
327    pub current_snapshot: MemorySnapshot,
328    /// Peak memory usage in bytes.
329    pub peak_bytes: u64,
330    /// Average memory usage in bytes.
331    pub avg_bytes: u64,
332    /// All collected snapshots.
333    pub snapshots: Vec<MemorySnapshot>,
334    /// Whether a potential memory leak was detected.
335    pub potential_leak: bool,
336    /// Allocation rate (allocations per second).
337    pub allocation_rate: f64,
338}
339
340impl MemoryReport {
341    /// Get peak memory in megabytes.
342    pub fn peak_mb(&self) -> f64 {
343        self.peak_bytes as f64 / (1024.0 * 1024.0)
344    }
345
346    /// Get average memory in megabytes.
347    pub fn avg_mb(&self) -> f64 {
348        self.avg_bytes as f64 / (1024.0 * 1024.0)
349    }
350
351    /// Get current memory in megabytes.
352    pub fn current_mb(&self) -> f64 {
353        self.current_snapshot.live_mb()
354    }
355
356    /// Format as a human-readable string.
357    pub fn format(&self) -> String {
358        let mut output = String::new();
359
360        output.push_str("=== Memory Profiling Report ===\n\n");
361        output.push_str(&format!("Duration: {:?}\n", self.duration));
362        output.push_str(&format!("Current Memory: {:.2} MB\n", self.current_mb()));
363        output.push_str(&format!("Peak Memory: {:.2} MB\n", self.peak_mb()));
364        output.push_str(&format!("Average Memory: {:.2} MB\n", self.avg_mb()));
365        output.push_str(&format!("Allocation Rate: {:.2}/sec\n", self.allocation_rate));
366        output.push_str(&format!("Total Allocations: {}\n", self.current_snapshot.allocation_count));
367        output.push_str(&format!("Live Allocations: {}\n", self.current_snapshot.live_count));
368
369        if let Some(rss) = self.current_snapshot.rss_mb() {
370            output.push_str(&format!("RSS: {:.2} MB\n", rss));
371        }
372
373        if self.potential_leak {
374            output.push_str("\n⚠️  POTENTIAL MEMORY LEAK DETECTED!\n");
375            output.push_str("Memory usage has been monotonically increasing.\n");
376        }
377
378        output
379    }
380
381    /// Check if memory usage is within acceptable limits.
382    pub fn check_limits(&self, max_mb: f64) -> bool {
383        self.peak_mb() <= max_mb
384    }
385}
386
387/// Estimate memory usage for a given number of devices and points.
388pub fn estimate_memory_usage(devices: usize, points_per_device: usize) -> MemoryEstimate {
389    // Estimated sizes based on actual struct layouts
390    const DEVICE_OVERHEAD: usize = 512;      // DeviceInfo, state, etc.
391    const POINT_SIZE: usize = 128;           // DataPoint definition
392    const VALUE_SIZE: usize = 32;            // Stored value
393    const REGISTER_ENTRY: usize = 24;        // HashMap/DashMap entry overhead
394
395    let device_memory = devices * DEVICE_OVERHEAD;
396    let point_memory = devices * points_per_device * POINT_SIZE;
397    let value_memory = devices * points_per_device * VALUE_SIZE;
398    let overhead = devices * points_per_device * REGISTER_ENTRY;
399
400    let total = device_memory + point_memory + value_memory + overhead;
401
402    MemoryEstimate {
403        devices,
404        points_per_device,
405        device_memory_bytes: device_memory,
406        point_memory_bytes: point_memory,
407        value_memory_bytes: value_memory,
408        overhead_bytes: overhead,
409        total_bytes: total,
410    }
411}
412
413/// Memory usage estimate.
414#[derive(Debug, Clone)]
415pub struct MemoryEstimate {
416    pub devices: usize,
417    pub points_per_device: usize,
418    pub device_memory_bytes: usize,
419    pub point_memory_bytes: usize,
420    pub value_memory_bytes: usize,
421    pub overhead_bytes: usize,
422    pub total_bytes: usize,
423}
424
425impl MemoryEstimate {
426    /// Get total memory in megabytes.
427    pub fn total_mb(&self) -> f64 {
428        self.total_bytes as f64 / (1024.0 * 1024.0)
429    }
430
431    /// Get total memory in gigabytes.
432    pub fn total_gb(&self) -> f64 {
433        self.total_bytes as f64 / (1024.0 * 1024.0 * 1024.0)
434    }
435
436    /// Format as human-readable string.
437    pub fn format(&self) -> String {
438        format!(
439            "Memory Estimate for {} devices with {} points each:\n\
440             - Device overhead: {:.2} MB\n\
441             - Point definitions: {:.2} MB\n\
442             - Stored values: {:.2} MB\n\
443             - Data structure overhead: {:.2} MB\n\
444             - Total: {:.2} MB ({:.2} GB)",
445            self.devices,
446            self.points_per_device,
447            self.device_memory_bytes as f64 / (1024.0 * 1024.0),
448            self.point_memory_bytes as f64 / (1024.0 * 1024.0),
449            self.value_memory_bytes as f64 / (1024.0 * 1024.0),
450            self.overhead_bytes as f64 / (1024.0 * 1024.0),
451            self.total_mb(),
452            self.total_gb(),
453        )
454    }
455}
456
457#[cfg(test)]
458mod tests {
459    use super::*;
460
461    #[test]
462    fn test_allocation_tracker() {
463        let tracker = AllocationTracker::new();
464
465        tracker.record_alloc(1000);
466        assert_eq!(tracker.live_bytes(), 1000);
467        assert_eq!(tracker.live_count(), 1);
468
469        tracker.record_alloc(500);
470        assert_eq!(tracker.live_bytes(), 1500);
471        assert_eq!(tracker.live_count(), 2);
472
473        tracker.record_dealloc(1000);
474        assert_eq!(tracker.live_bytes(), 500);
475        assert_eq!(tracker.live_count(), 1);
476    }
477
478    #[test]
479    fn test_memory_estimate() {
480        let estimate = estimate_memory_usage(10_000, 100);
481        assert!(estimate.total_mb() > 0.0);
482        println!("{}", estimate.format());
483
484        let large_estimate = estimate_memory_usage(50_000, 100);
485        assert!(large_estimate.total_gb() < 8.0, "Should fit in 8GB limit");
486    }
487
488    #[test]
489    fn test_memory_snapshot() {
490        let snapshot = MemorySnapshot {
491            timestamp: Instant::now(),
492            allocated_bytes: 100 * 1024 * 1024,
493            allocation_count: 10000,
494            freed_bytes: 50 * 1024 * 1024,
495            deallocation_count: 5000,
496            live_bytes: 50 * 1024 * 1024,
497            live_count: 5000,
498            peak_bytes: 75 * 1024 * 1024,
499            rss_bytes: Some(80 * 1024 * 1024),
500            heap_bytes: None,
501        };
502
503        assert!((snapshot.live_mb() - 50.0).abs() < 0.01);
504        assert!((snapshot.peak_mb() - 75.0).abs() < 0.01);
505    }
506}