1use parking_lot::RwLock;
11use std::collections::HashMap;
12use std::sync::Arc;
13use std::time::{Duration, Instant};
14use thiserror::Error;
15
16#[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#[derive(Debug, Clone)]
31pub struct MemoryMonitorConfig {
32 pub enabled: bool,
34
35 pub total_budget: Option<usize>,
37
38 pub component_budgets: HashMap<String, usize>,
40
41 pub enable_auto_cleanup: bool,
43
44 pub cleanup_threshold: f64,
46
47 pub monitoring_interval: Duration,
49
50 pub enable_leak_detection: bool,
52
53 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, }
69 }
70}
71
72impl MemoryMonitorConfig {
73 pub fn low_memory() -> Self {
75 let mut budgets = HashMap::new();
76 budgets.insert("peer_store".to_string(), 10 * 1024 * 1024); budgets.insert("dht_cache".to_string(), 20 * 1024 * 1024); budgets.insert("provider_cache".to_string(), 10 * 1024 * 1024); budgets.insert("connections".to_string(), 30 * 1024 * 1024); budgets.insert("other".to_string(), 58 * 1024 * 1024); Self {
83 enabled: true,
84 total_budget: Some(128 * 1024 * 1024), 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 pub fn iot() -> Self {
96 let mut budgets = HashMap::new();
97 budgets.insert("peer_store".to_string(), 5 * 1024 * 1024); budgets.insert("dht_cache".to_string(), 10 * 1024 * 1024); budgets.insert("provider_cache".to_string(), 5 * 1024 * 1024); budgets.insert("connections".to_string(), 20 * 1024 * 1024); budgets.insert("other".to_string(), 24 * 1024 * 1024); Self {
104 enabled: true,
105 total_budget: Some(64 * 1024 * 1024), 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 pub fn mobile() -> Self {
117 let mut budgets = HashMap::new();
118 budgets.insert("peer_store".to_string(), 20 * 1024 * 1024); budgets.insert("dht_cache".to_string(), 50 * 1024 * 1024); budgets.insert("provider_cache".to_string(), 20 * 1024 * 1024); budgets.insert("connections".to_string(), 100 * 1024 * 1024); budgets.insert("other".to_string(), 66 * 1024 * 1024); Self {
125 enabled: true,
126 total_budget: Some(256 * 1024 * 1024), 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 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#[derive(Debug, Clone, Default)]
160pub struct ComponentMemory {
161 pub name: String,
163 pub current_usage: usize,
165 pub peak_usage: usize,
167 pub allocation_count: u64,
169 pub last_updated: Option<Instant>,
171 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 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 pub fn budget_utilization(&self) -> Option<f64> {
198 self.budget
199 .map(|budget| self.current_usage as f64 / budget as f64)
200 }
201}
202
203struct MonitorState {
205 components: HashMap<String, ComponentMemory>,
207 total_usage: usize,
209 peak_total_usage: usize,
211 last_cleanup: Instant,
213 memory_samples: Vec<(Instant, usize)>,
215 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
232pub struct MemoryMonitor {
234 config: MemoryMonitorConfig,
235 state: Arc<RwLock<MonitorState>>,
236}
237
238impl MemoryMonitor {
239 pub fn new(config: MemoryMonitorConfig) -> Result<Self, MemoryMonitorError> {
241 config.validate()?;
242
243 let mut state = MonitorState::new();
244
245 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 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 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 let old_usage = comp.current_usage;
279 let comp_budget = comp.budget;
280
281 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 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 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 if self.config.enable_leak_detection {
309 let total_usage = state.total_usage;
310 state.memory_samples.push((now, total_usage));
311 if state.memory_samples.len() > 100 {
313 state.memory_samples.remove(0);
314 }
315 }
316
317 Ok(())
318 }
319
320 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 pub fn total_usage(&self) -> usize {
332 self.state.read().total_usage
333 }
334
335 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 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 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; }
373
374 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 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 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 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 pub fn component_names(&self) -> Vec<String> {
431 self.state.read().components.keys().cloned().collect()
432 }
433}
434
435#[derive(Debug, Clone)]
437pub struct MemoryStats {
438 pub total_usage: usize,
440 pub peak_usage: usize,
442 pub total_budget: Option<usize>,
444 pub components: Vec<ComponentMemory>,
446 pub cleanup_count: u64,
448 pub potential_leak: Option<f64>,
450}
451
452impl MemoryStats {
453 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 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 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); }
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}