ipfrs_network/
memory_monitor.rs

1//! Memory usage monitoring for network components
2//!
3//! This module provides memory tracking and monitoring capabilities for:
4//! - Peer store memory usage
5//! - Connection buffer sizes
6//! - Cache memory consumption
7//! - DHT routing table memory
8//! - Memory budgets and limits
9
10use parking_lot::RwLock;
11use std::collections::HashMap;
12use std::sync::Arc;
13use std::time::{Duration, Instant};
14use thiserror::Error;
15
16/// Errors that can occur during memory monitoring
17#[derive(Error, Debug, Clone)]
18pub enum MemoryMonitorError {
19    #[error("Memory budget exceeded: {0} bytes over limit")]
20    BudgetExceeded(usize),
21
22    #[error("Invalid configuration: {0}")]
23    InvalidConfig(String),
24
25    #[error("Component not found: {0}")]
26    ComponentNotFound(String),
27}
28
29/// Configuration for memory monitoring
30#[derive(Debug, Clone)]
31pub struct MemoryMonitorConfig {
32    /// Enable memory monitoring
33    pub enabled: bool,
34
35    /// Total memory budget in bytes (None = unlimited)
36    pub total_budget: Option<usize>,
37
38    /// Per-component memory budgets
39    pub component_budgets: HashMap<String, usize>,
40
41    /// Enable automatic memory cleanup when approaching limits
42    pub enable_auto_cleanup: bool,
43
44    /// Cleanup threshold (fraction of budget, 0.0-1.0)
45    pub cleanup_threshold: f64,
46
47    /// Monitoring interval
48    pub monitoring_interval: Duration,
49
50    /// Enable memory leak detection
51    pub enable_leak_detection: bool,
52
53    /// Growth rate threshold for leak detection (bytes per second)
54    pub leak_detection_threshold: f64,
55}
56
57impl Default for MemoryMonitorConfig {
58    fn default() -> Self {
59        Self {
60            enabled: true,
61            total_budget: None,
62            component_budgets: HashMap::new(),
63            enable_auto_cleanup: true,
64            cleanup_threshold: 0.9,
65            monitoring_interval: Duration::from_secs(10),
66            enable_leak_detection: false,
67            leak_detection_threshold: 1_000_000.0, // 1 MB/s growth
68        }
69    }
70}
71
72impl MemoryMonitorConfig {
73    /// Configuration for low-memory devices (128 MB budget)
74    pub fn low_memory() -> Self {
75        let mut budgets = HashMap::new();
76        budgets.insert("peer_store".to_string(), 10 * 1024 * 1024); // 10 MB
77        budgets.insert("dht_cache".to_string(), 20 * 1024 * 1024); // 20 MB
78        budgets.insert("provider_cache".to_string(), 10 * 1024 * 1024); // 10 MB
79        budgets.insert("connections".to_string(), 30 * 1024 * 1024); // 30 MB
80        budgets.insert("other".to_string(), 58 * 1024 * 1024); // 58 MB
81
82        Self {
83            enabled: true,
84            total_budget: Some(128 * 1024 * 1024), // 128 MB
85            component_budgets: budgets,
86            enable_auto_cleanup: true,
87            cleanup_threshold: 0.85,
88            monitoring_interval: Duration::from_secs(5),
89            enable_leak_detection: true,
90            leak_detection_threshold: 100_000.0,
91        }
92    }
93
94    /// Configuration for IoT devices (64 MB budget)
95    pub fn iot() -> Self {
96        let mut budgets = HashMap::new();
97        budgets.insert("peer_store".to_string(), 5 * 1024 * 1024); // 5 MB
98        budgets.insert("dht_cache".to_string(), 10 * 1024 * 1024); // 10 MB
99        budgets.insert("provider_cache".to_string(), 5 * 1024 * 1024); // 5 MB
100        budgets.insert("connections".to_string(), 20 * 1024 * 1024); // 20 MB
101        budgets.insert("other".to_string(), 24 * 1024 * 1024); // 24 MB
102
103        Self {
104            enabled: true,
105            total_budget: Some(64 * 1024 * 1024), // 64 MB
106            component_budgets: budgets,
107            enable_auto_cleanup: true,
108            cleanup_threshold: 0.8,
109            monitoring_interval: Duration::from_secs(3),
110            enable_leak_detection: true,
111            leak_detection_threshold: 50_000.0,
112        }
113    }
114
115    /// Configuration for mobile devices (256 MB budget)
116    pub fn mobile() -> Self {
117        let mut budgets = HashMap::new();
118        budgets.insert("peer_store".to_string(), 20 * 1024 * 1024); // 20 MB
119        budgets.insert("dht_cache".to_string(), 50 * 1024 * 1024); // 50 MB
120        budgets.insert("provider_cache".to_string(), 20 * 1024 * 1024); // 20 MB
121        budgets.insert("connections".to_string(), 100 * 1024 * 1024); // 100 MB
122        budgets.insert("other".to_string(), 66 * 1024 * 1024); // 66 MB
123
124        Self {
125            enabled: true,
126            total_budget: Some(256 * 1024 * 1024), // 256 MB
127            component_budgets: budgets,
128            enable_auto_cleanup: true,
129            cleanup_threshold: 0.9,
130            monitoring_interval: Duration::from_secs(10),
131            enable_leak_detection: true,
132            leak_detection_threshold: 500_000.0,
133        }
134    }
135
136    /// Validate the configuration
137    pub fn validate(&self) -> Result<(), MemoryMonitorError> {
138        if self.cleanup_threshold < 0.0 || self.cleanup_threshold > 1.0 {
139            return Err(MemoryMonitorError::InvalidConfig(
140                "cleanup_threshold must be in [0.0, 1.0]".to_string(),
141            ));
142        }
143
144        if let Some(total) = self.total_budget {
145            let component_total: usize = self.component_budgets.values().sum();
146            if component_total > total {
147                return Err(MemoryMonitorError::InvalidConfig(format!(
148                    "Component budgets ({}) exceed total budget ({})",
149                    component_total, total
150                )));
151            }
152        }
153
154        Ok(())
155    }
156}
157
158/// Memory usage for a component
159#[derive(Debug, Clone, Default)]
160pub struct ComponentMemory {
161    /// Component name
162    pub name: String,
163    /// Current memory usage in bytes
164    pub current_usage: usize,
165    /// Peak memory usage
166    pub peak_usage: usize,
167    /// Number of allocations
168    pub allocation_count: u64,
169    /// Last update time
170    pub last_updated: Option<Instant>,
171    /// Memory budget for this component
172    pub budget: Option<usize>,
173}
174
175impl ComponentMemory {
176    fn new(name: String, budget: Option<usize>) -> Self {
177        Self {
178            name,
179            current_usage: 0,
180            peak_usage: 0,
181            allocation_count: 0,
182            last_updated: Some(Instant::now()),
183            budget,
184        }
185    }
186
187    /// Check if over budget
188    pub fn is_over_budget(&self) -> bool {
189        if let Some(budget) = self.budget {
190            self.current_usage > budget
191        } else {
192            false
193        }
194    }
195
196    /// Get budget utilization (0.0-1.0+)
197    pub fn budget_utilization(&self) -> Option<f64> {
198        self.budget
199            .map(|budget| self.current_usage as f64 / budget as f64)
200    }
201}
202
203/// Memory monitoring state
204struct MonitorState {
205    /// Memory usage per component
206    components: HashMap<String, ComponentMemory>,
207    /// Total memory usage
208    total_usage: usize,
209    /// Peak total usage
210    peak_total_usage: usize,
211    /// Last cleanup time
212    last_cleanup: Instant,
213    /// Memory samples for leak detection
214    memory_samples: Vec<(Instant, usize)>,
215    /// Number of cleanup operations performed
216    cleanup_count: u64,
217}
218
219impl MonitorState {
220    fn new() -> Self {
221        Self {
222            components: HashMap::new(),
223            total_usage: 0,
224            peak_total_usage: 0,
225            last_cleanup: Instant::now(),
226            memory_samples: Vec::new(),
227            cleanup_count: 0,
228        }
229    }
230}
231
232/// Memory monitor for network components
233pub struct MemoryMonitor {
234    config: MemoryMonitorConfig,
235    state: Arc<RwLock<MonitorState>>,
236}
237
238impl MemoryMonitor {
239    /// Create a new memory monitor
240    pub fn new(config: MemoryMonitorConfig) -> Result<Self, MemoryMonitorError> {
241        config.validate()?;
242
243        let mut state = MonitorState::new();
244
245        // Initialize component budgets
246        for (name, budget) in &config.component_budgets {
247            state.components.insert(
248                name.clone(),
249                ComponentMemory::new(name.clone(), Some(*budget)),
250            );
251        }
252
253        Ok(Self {
254            config,
255            state: Arc::new(RwLock::new(state)),
256        })
257    }
258
259    /// Record memory usage for a component
260    pub fn record_usage(&self, component: &str, bytes: usize) -> Result<(), MemoryMonitorError> {
261        if !self.config.enabled {
262            return Ok(());
263        }
264
265        let mut state = self.state.write();
266        let now = Instant::now();
267
268        // Get or create component
269        let comp = state
270            .components
271            .entry(component.to_string())
272            .or_insert_with(|| {
273                let budget = self.config.component_budgets.get(component).copied();
274                ComponentMemory::new(component.to_string(), budget)
275            });
276
277        // Get values we need before mutating
278        let old_usage = comp.current_usage;
279        let comp_budget = comp.budget;
280
281        // Update component stats
282        comp.current_usage = bytes;
283        comp.peak_usage = comp.peak_usage.max(bytes);
284        comp.allocation_count += 1;
285        comp.last_updated = Some(now);
286
287        // Update total
288        let old_total = state.total_usage;
289        state.total_usage = old_total - old_usage + bytes;
290        state.peak_total_usage = state.peak_total_usage.max(state.total_usage);
291
292        // Check budgets
293        if let Some(budget) = comp_budget {
294            if bytes > budget {
295                return Err(MemoryMonitorError::BudgetExceeded(bytes - budget));
296            }
297        }
298
299        if let Some(total_budget) = self.config.total_budget {
300            if state.total_usage > total_budget {
301                return Err(MemoryMonitorError::BudgetExceeded(
302                    state.total_usage - total_budget,
303                ));
304            }
305        }
306
307        // Record sample for leak detection
308        if self.config.enable_leak_detection {
309            let total_usage = state.total_usage;
310            state.memory_samples.push((now, total_usage));
311            // Keep last 100 samples
312            if state.memory_samples.len() > 100 {
313                state.memory_samples.remove(0);
314            }
315        }
316
317        Ok(())
318    }
319
320    /// Get current memory usage for a component
321    pub fn get_usage(&self, component: &str) -> Result<usize, MemoryMonitorError> {
322        let state = self.state.read();
323        state
324            .components
325            .get(component)
326            .map(|c| c.current_usage)
327            .ok_or_else(|| MemoryMonitorError::ComponentNotFound(component.to_string()))
328    }
329
330    /// Get total memory usage
331    pub fn total_usage(&self) -> usize {
332        self.state.read().total_usage
333    }
334
335    /// Check if cleanup is needed
336    pub fn needs_cleanup(&self) -> bool {
337        if !self.config.enable_auto_cleanup {
338            return false;
339        }
340
341        let state = self.state.read();
342
343        if let Some(total_budget) = self.config.total_budget {
344            let usage_ratio = state.total_usage as f64 / total_budget as f64;
345            if usage_ratio >= self.config.cleanup_threshold {
346                return true;
347            }
348        }
349
350        // Check component budgets
351        for comp in state.components.values() {
352            if let Some(util) = comp.budget_utilization() {
353                if util >= self.config.cleanup_threshold {
354                    return true;
355                }
356            }
357        }
358
359        false
360    }
361
362    /// Detect memory leaks based on growth rate
363    pub fn detect_leak(&self) -> Option<f64> {
364        if !self.config.enable_leak_detection {
365            return None;
366        }
367
368        let state = self.state.read();
369
370        if state.memory_samples.len() < 10 {
371            return None; // Not enough data
372        }
373
374        // Calculate growth rate (linear regression)
375        let samples = &state.memory_samples;
376        let n = samples.len();
377        let first = &samples[0];
378        let last = &samples[n - 1];
379
380        let time_diff = last.0.duration_since(first.0).as_secs_f64();
381        if time_diff < 1.0 {
382            return None;
383        }
384
385        let growth = (last.1 as i64 - first.1 as i64) as f64;
386        let growth_rate = growth / time_diff;
387
388        if growth_rate.abs() > self.config.leak_detection_threshold {
389            Some(growth_rate)
390        } else {
391            None
392        }
393    }
394
395    /// Get memory statistics
396    pub fn stats(&self) -> MemoryStats {
397        let state = self.state.read();
398
399        let components: Vec<ComponentMemory> = state.components.values().cloned().collect();
400
401        MemoryStats {
402            total_usage: state.total_usage,
403            peak_usage: state.peak_total_usage,
404            total_budget: self.config.total_budget,
405            components,
406            cleanup_count: state.cleanup_count,
407            potential_leak: self.detect_leak(),
408        }
409    }
410
411    /// Mark that cleanup was performed
412    pub fn mark_cleanup(&self) {
413        let mut state = self.state.write();
414        state.last_cleanup = Instant::now();
415        state.cleanup_count += 1;
416    }
417
418    /// Reset statistics
419    pub fn reset_stats(&self) {
420        let mut state = self.state.write();
421        for comp in state.components.values_mut() {
422            comp.peak_usage = comp.current_usage;
423            comp.allocation_count = 0;
424        }
425        state.peak_total_usage = state.total_usage;
426        state.memory_samples.clear();
427    }
428
429    /// Get component names
430    pub fn component_names(&self) -> Vec<String> {
431        self.state.read().components.keys().cloned().collect()
432    }
433}
434
435/// Memory usage statistics
436#[derive(Debug, Clone)]
437pub struct MemoryStats {
438    /// Total memory usage
439    pub total_usage: usize,
440    /// Peak memory usage
441    pub peak_usage: usize,
442    /// Total memory budget
443    pub total_budget: Option<usize>,
444    /// Per-component stats
445    pub components: Vec<ComponentMemory>,
446    /// Number of cleanups performed
447    pub cleanup_count: u64,
448    /// Potential memory leak (bytes per second)
449    pub potential_leak: Option<f64>,
450}
451
452impl MemoryStats {
453    /// Get budget utilization (0.0-1.0+)
454    pub fn budget_utilization(&self) -> Option<f64> {
455        self.total_budget
456            .map(|budget| self.total_usage as f64 / budget as f64)
457    }
458
459    /// Check if any component is over budget
460    pub fn has_budget_violation(&self) -> bool {
461        if let Some(budget) = self.total_budget {
462            if self.total_usage > budget {
463                return true;
464            }
465        }
466
467        self.components.iter().any(|c| c.is_over_budget())
468    }
469
470    /// Format memory size as human-readable string
471    pub fn format_bytes(bytes: usize) -> String {
472        const KB: usize = 1024;
473        const MB: usize = KB * 1024;
474        const GB: usize = MB * 1024;
475
476        if bytes >= GB {
477            format!("{:.2} GB", bytes as f64 / GB as f64)
478        } else if bytes >= MB {
479            format!("{:.2} MB", bytes as f64 / MB as f64)
480        } else if bytes >= KB {
481            format!("{:.2} KB", bytes as f64 / KB as f64)
482        } else {
483            format!("{} B", bytes)
484        }
485    }
486}
487
488#[cfg(test)]
489mod tests {
490    use super::*;
491
492    #[test]
493    fn test_config_default() {
494        let config = MemoryMonitorConfig::default();
495        assert!(config.validate().is_ok());
496        assert!(config.enabled);
497    }
498
499    #[test]
500    fn test_config_low_memory() {
501        let config = MemoryMonitorConfig::low_memory();
502        assert!(config.validate().is_ok());
503        assert_eq!(config.total_budget, Some(128 * 1024 * 1024));
504    }
505
506    #[test]
507    fn test_config_iot() {
508        let config = MemoryMonitorConfig::iot();
509        assert!(config.validate().is_ok());
510        assert_eq!(config.total_budget, Some(64 * 1024 * 1024));
511    }
512
513    #[test]
514    fn test_config_mobile() {
515        let config = MemoryMonitorConfig::mobile();
516        assert!(config.validate().is_ok());
517        assert_eq!(config.total_budget, Some(256 * 1024 * 1024));
518    }
519
520    #[test]
521    fn test_record_usage() {
522        let config = MemoryMonitorConfig::default();
523        let monitor = MemoryMonitor::new(config).unwrap();
524
525        let result = monitor.record_usage("test", 1000);
526        assert!(result.is_ok());
527
528        assert_eq!(monitor.get_usage("test").unwrap(), 1000);
529        assert_eq!(monitor.total_usage(), 1000);
530    }
531
532    #[test]
533    fn test_budget_exceeded() {
534        let mut config = MemoryMonitorConfig::default();
535        config.component_budgets.insert("test".to_string(), 500);
536        let monitor = MemoryMonitor::new(config).unwrap();
537
538        let result = monitor.record_usage("test", 1000);
539        assert!(matches!(result, Err(MemoryMonitorError::BudgetExceeded(_))));
540    }
541
542    #[test]
543    fn test_total_budget_exceeded() {
544        let config = MemoryMonitorConfig {
545            total_budget: Some(1000),
546            ..Default::default()
547        };
548        let monitor = MemoryMonitor::new(config).unwrap();
549
550        monitor.record_usage("test1", 500).unwrap();
551        let result = monitor.record_usage("test2", 600);
552        assert!(matches!(result, Err(MemoryMonitorError::BudgetExceeded(_))));
553    }
554
555    #[test]
556    fn test_needs_cleanup() {
557        let config = MemoryMonitorConfig {
558            total_budget: Some(1000),
559            cleanup_threshold: 0.8,
560            ..Default::default()
561        };
562        let monitor = MemoryMonitor::new(config).unwrap();
563
564        assert!(!monitor.needs_cleanup());
565
566        monitor.record_usage("test", 850).unwrap();
567        assert!(monitor.needs_cleanup());
568    }
569
570    #[test]
571    fn test_component_utilization() {
572        let mut comp = ComponentMemory::new("test".to_string(), Some(1000));
573        comp.current_usage = 500;
574
575        assert_eq!(comp.budget_utilization(), Some(0.5));
576        assert!(!comp.is_over_budget());
577
578        comp.current_usage = 1500;
579        assert!(comp.is_over_budget());
580    }
581
582    #[test]
583    fn test_stats() {
584        let config = MemoryMonitorConfig::default();
585        let monitor = MemoryMonitor::new(config).unwrap();
586
587        monitor.record_usage("test1", 500).unwrap();
588        monitor.record_usage("test2", 300).unwrap();
589
590        let stats = monitor.stats();
591        assert_eq!(stats.total_usage, 800);
592        assert_eq!(stats.components.len(), 2);
593    }
594
595    #[test]
596    fn test_format_bytes() {
597        assert_eq!(MemoryStats::format_bytes(500), "500 B");
598        assert_eq!(MemoryStats::format_bytes(2048), "2.00 KB");
599        assert_eq!(MemoryStats::format_bytes(2 * 1024 * 1024), "2.00 MB");
600        assert_eq!(MemoryStats::format_bytes(3 * 1024 * 1024 * 1024), "3.00 GB");
601    }
602
603    #[test]
604    fn test_component_names() {
605        let config = MemoryMonitorConfig::low_memory();
606        let monitor = MemoryMonitor::new(config).unwrap();
607
608        let names = monitor.component_names();
609        assert!(names.contains(&"peer_store".to_string()));
610        assert!(names.contains(&"dht_cache".to_string()));
611    }
612
613    #[test]
614    fn test_reset_stats() {
615        let config = MemoryMonitorConfig::default();
616        let monitor = MemoryMonitor::new(config).unwrap();
617
618        monitor.record_usage("test", 1000).unwrap();
619        let stats1 = monitor.stats();
620        assert_eq!(stats1.peak_usage, 1000);
621
622        monitor.reset_stats();
623        let stats2 = monitor.stats();
624        assert_eq!(stats2.peak_usage, 1000); // Current usage, not reset
625    }
626
627    #[test]
628    fn test_mark_cleanup() {
629        let config = MemoryMonitorConfig::default();
630        let monitor = MemoryMonitor::new(config).unwrap();
631
632        let stats1 = monitor.stats();
633        assert_eq!(stats1.cleanup_count, 0);
634
635        monitor.mark_cleanup();
636        let stats2 = monitor.stats();
637        assert_eq!(stats2.cleanup_count, 1);
638    }
639}