1use std::collections::HashMap;
7#[cfg(feature = "memory_metrics")]
8use std::fs::File;
9use std::io;
10#[cfg(feature = "memory_metrics")]
11use std::io::{Read, Write};
12use std::path::Path;
13use std::time::{Duration, SystemTime, UNIX_EPOCH};
14
15#[cfg(feature = "memory_metrics")]
16#[cfg(feature = "serialization")]
17use serde::{Deserialize, Serialize};
18#[cfg(feature = "memory_metrics")]
19use serde_json::Value as JsonValue;
20
21use crate::memory::metrics::collector::MemoryReport;
22use crate::memory::metrics::generate_memory_report;
23
24#[derive(Debug, Clone)]
26#[cfg_attr(feature = "memory_metrics", derive(Serialize, Deserialize))]
27pub struct MemorySnapshot {
28 pub id: String,
30
31 pub timestamp: u64,
33
34 pub description: String,
36
37 pub report: SnapshotReport,
39
40 pub metadata: HashMap<String, String>,
42}
43
44#[derive(Debug, Clone)]
46#[cfg_attr(feature = "memory_metrics", derive(Serialize, Deserialize))]
47pub struct SnapshotReport {
48 pub total_current_usage: usize,
50
51 pub total_peak_usage: usize,
53
54 pub total_allocation_count: usize,
56
57 pub total_allocated_bytes: usize,
59
60 pub component_stats: HashMap<String, SnapshotComponentStats>,
62
63 pub duration_ms: u64,
65}
66
67#[derive(Debug, Clone)]
69#[cfg_attr(feature = "memory_metrics", derive(Serialize, Deserialize))]
70pub struct SnapshotComponentStats {
71 pub current_usage: usize,
73
74 pub peak_usage: usize,
76
77 pub allocation_count: usize,
79
80 pub total_allocated: usize,
82
83 pub avg_allocation_size: f64,
85}
86
87impl MemorySnapshot {
88 pub fn new(id: impl Into<String>, description: impl Into<String>) -> Self {
90 let report = generate_memory_report();
92
93 let timestamp = SystemTime::now()
95 .duration_since(UNIX_EPOCH)
96 .unwrap_or_else(|_| Duration::from_secs(0))
97 .as_secs();
98
99 Self {
100 id: id.into(),
101 timestamp,
102 description: description.into(),
103 report: SnapshotReport::from(report),
104 metadata: HashMap::new(),
105 }
106 }
107
108 pub fn from_report(
110 id: impl Into<String>,
111 description: impl Into<String>,
112 report: MemoryReport,
113 ) -> Self {
114 let timestamp = SystemTime::now()
116 .duration_since(UNIX_EPOCH)
117 .unwrap_or_else(|_| Duration::from_secs(0))
118 .as_secs();
119
120 Self {
121 id: id.into(),
122 timestamp,
123 description: description.into(),
124 report: SnapshotReport::from(report),
125 metadata: HashMap::new(),
126 }
127 }
128
129 pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
131 self.metadata.insert(key.into(), value.into());
132 self
133 }
134
135 #[cfg(feature = "memory_metrics")]
137 pub fn save_to_file(&self, path: impl AsRef<Path>) -> io::Result<()> {
138 let json = serde_json::to_string_pretty(self)?;
139 let mut file = File::create(path)?;
140 file.write_all(json.as_bytes())?;
141 Ok(())
142 }
143
144 #[cfg(not(feature = "memory_metrics"))]
146 pub fn save_to_file(&self, path: impl AsRef<Path>) -> io::Result<()> {
147 Ok(())
148 }
149
150 #[cfg(feature = "memory_metrics")]
152 pub fn load_from_file(path: impl AsRef<Path>) -> io::Result<Self> {
153 let mut file = File::open(path)?;
154 let mut contents = String::new();
155 file.read_to_string(&mut contents)?;
156 let snapshot = serde_json::from_str(&contents)?;
157 Ok(snapshot)
158 }
159
160 #[cfg(not(feature = "memory_metrics"))]
162 pub fn load_from_file(path: impl AsRef<Path>) -> io::Result<Self> {
163 Ok(Self::new("stub_id", "stub_description"))
164 }
165
166 pub fn compare(&self, other: &MemorySnapshot) -> SnapshotDiff {
168 SnapshotDiff::new(self, other)
169 }
170
171 #[cfg(feature = "memory_metrics")]
173 pub fn to_json(&self) -> JsonValue {
174 serde_json::to_value(self).unwrap_or(JsonValue::Null)
175 }
176
177 #[cfg(not(feature = "memory_metrics"))]
179 pub fn to_json(&self) -> String {
180 "{}".to_string()
181 }
182}
183
184impl From<MemoryReport> for SnapshotReport {
186 fn from(report: MemoryReport) -> Self {
187 let mut component_stats = HashMap::new();
188
189 for (name, stats) in report.component_stats {
190 component_stats.insert(
191 name,
192 SnapshotComponentStats {
193 current_usage: stats.current_usage,
194 peak_usage: stats.peak_usage,
195 allocation_count: stats.allocation_count,
196 total_allocated: stats.total_allocated,
197 avg_allocation_size: stats.avg_allocation_size,
198 },
199 );
200 }
201
202 Self {
203 total_current_usage: report.total_current_usage,
204 total_peak_usage: report.total_peak_usage,
205 total_allocation_count: report.total_allocation_count,
206 total_allocated_bytes: report.total_allocated_bytes,
207 component_stats,
208 duration_ms: report.duration.as_millis() as u64,
209 }
210 }
211}
212
213#[derive(Debug, Clone)]
215#[cfg_attr(feature = "memory_metrics", derive(Serialize, Deserialize))]
216pub struct SnapshotDiff {
217 pub first_id: String,
219
220 pub second_id: String,
222
223 pub elapsed_ms: u64,
225
226 pub current_usage_delta: isize,
228
229 pub peak_usage_delta: isize,
231
232 pub allocation_count_delta: isize,
234
235 pub new_components: Vec<String>,
237
238 pub removed_components: Vec<String>,
240
241 pub component_changes: HashMap<String, ComponentStatsDiff>,
243}
244
245#[derive(Debug, Clone)]
247#[cfg_attr(feature = "memory_metrics", derive(Serialize, Deserialize))]
248pub struct ComponentStatsDiff {
249 pub current_usage_delta: isize,
251
252 pub peak_usage_delta: isize,
254
255 pub allocation_count_delta: isize,
257
258 pub total_allocated_delta: isize,
260
261 pub potential_leak: bool,
263}
264
265impl SnapshotDiff {
266 pub fn new(first: &MemorySnapshot, second: &MemorySnapshot) -> Self {
268 let elapsed_ms = second.timestamp.saturating_sub(first.timestamp) * 1000;
269
270 let current_usage_delta =
271 second.report.total_current_usage as isize - first.report.total_current_usage as isize;
272 let peak_usage_delta =
273 second.report.total_peak_usage as isize - first.report.total_peak_usage as isize;
274 let allocation_count_delta = second.report.total_allocation_count as isize
275 - first.report.total_allocation_count as isize;
276
277 let mut new_components = Vec::new();
279 let mut removed_components = Vec::new();
280
281 for component in second.report.component_stats.keys() {
282 if !first.report.component_stats.contains_key(component) {
283 new_components.push(component.clone());
284 }
285 }
286
287 for component in first.report.component_stats.keys() {
288 if !second.report.component_stats.contains_key(component) {
289 removed_components.push(component.clone());
290 }
291 }
292
293 let mut component_changes = HashMap::new();
295
296 for (component, second_stats) in &second.report.component_stats {
297 if let Some(first_stats) = first.report.component_stats.get(component) {
298 let current_usage_delta =
299 second_stats.current_usage as isize - first_stats.current_usage as isize;
300 let peak_usage_delta =
301 second_stats.peak_usage as isize - first_stats.peak_usage as isize;
302 let allocation_count_delta =
303 second_stats.allocation_count as isize - first_stats.allocation_count as isize;
304 let total_allocated_delta =
305 second_stats.total_allocated as isize - first_stats.total_allocated as isize;
306
307 let potential_leak = current_usage_delta > 0;
310
311 component_changes.insert(
312 component.clone(),
313 ComponentStatsDiff {
314 current_usage_delta,
315 peak_usage_delta,
316 allocation_count_delta,
317 total_allocated_delta,
318 potential_leak,
319 },
320 );
321 }
322 }
323
324 Self {
325 first_id: first.id.clone(),
326 second_id: second.id.clone(),
327 elapsed_ms,
328 current_usage_delta,
329 peak_usage_delta,
330 allocation_count_delta,
331 new_components,
332 removed_components,
333 component_changes,
334 }
335 }
336
337 pub fn format(&self) -> String {
339 use crate::memory::metrics::format_bytes;
340
341 let mut output = String::new();
342
343 output.push_str(&format!(
344 "Memory Snapshot Diff: {} -> {}\n",
345 self.first_id, self.second_id
346 ));
347 output.push_str(&format!(
348 "Time elapsed: {elapsed}ms\n\n",
349 elapsed = self.elapsed_ms
350 ));
351
352 let current_delta_prefix = if self.current_usage_delta >= 0 {
354 "+"
355 } else {
356 ""
357 };
358 let peak_delta_prefix = if self.peak_usage_delta >= 0 { "+" } else { "" };
359 let alloc_delta_prefix = if self.allocation_count_delta >= 0 {
360 "+"
361 } else {
362 ""
363 };
364
365 output.push_str(&format!(
366 "Total Current Usage: {}{}B ({})\n",
367 current_delta_prefix,
368 self.current_usage_delta,
369 format_bytes(self.current_usage_delta.unsigned_abs())
370 ));
371
372 output.push_str(&format!(
373 "Total Peak Usage: {}{}B ({})\n",
374 peak_delta_prefix,
375 self.peak_usage_delta,
376 format_bytes(self.peak_usage_delta.unsigned_abs())
377 ));
378
379 output.push_str(&format!(
380 "Total Allocations: {}{}\n\n",
381 alloc_delta_prefix, self.allocation_count_delta
382 ));
383
384 if !self.new_components.is_empty() {
386 output.push_str("New Components:\n");
387 for component in &self.new_components {
388 output.push_str(&format!(" + {component}\n"));
389 }
390 output.push('\n');
391 }
392
393 if !self.removed_components.is_empty() {
395 output.push_str("Removed Components:\n");
396 for component in &self.removed_components {
397 output.push_str(&format!(" - {component}\n"));
398 }
399 output.push('\n');
400 }
401
402 if !self.component_changes.is_empty() {
404 output.push_str("Component Changes:\n");
405
406 let mut components: Vec<_> = self.component_changes.iter().collect();
408 components.sort_by(|a, b| b.1.current_usage_delta.cmp(&a.1.current_usage_delta));
409
410 for (component, diff) in components {
411 let current_prefix = if diff.current_usage_delta >= 0 {
412 "+"
413 } else {
414 ""
415 };
416 let peak_prefix = if diff.peak_usage_delta >= 0 { "+" } else { "" };
417 let alloc_prefix = if diff.allocation_count_delta >= 0 {
418 "+"
419 } else {
420 ""
421 };
422 let total_prefix = if diff.total_allocated_delta >= 0 {
423 "+"
424 } else {
425 ""
426 };
427
428 output.push_str(&format!(" {component}"));
429
430 if diff.potential_leak {
431 output.push_str(" [POTENTIAL LEAK]");
432 }
433
434 output.push('\n');
435
436 output.push_str(&format!(
437 " Current: {}{}B ({})\n",
438 current_prefix,
439 diff.current_usage_delta,
440 format_bytes(diff.current_usage_delta.unsigned_abs())
441 ));
442
443 output.push_str(&format!(
444 " Peak: {}{}B ({})\n",
445 peak_prefix,
446 diff.peak_usage_delta,
447 format_bytes(diff.peak_usage_delta.unsigned_abs())
448 ));
449
450 output.push_str(&format!(
451 " Allocations: {}{}\n",
452 alloc_prefix, diff.allocation_count_delta
453 ));
454
455 output.push_str(&format!(
456 " Total Allocated: {}{}B ({})\n",
457 total_prefix,
458 diff.total_allocated_delta,
459 format_bytes(diff.total_allocated_delta.unsigned_abs())
460 ));
461 }
462 }
463
464 output
465 }
466
467 #[cfg(feature = "memory_metrics")]
469 pub fn to_json(&self) -> JsonValue {
470 serde_json::to_value(self).unwrap_or(JsonValue::Null)
471 }
472
473 #[cfg(not(feature = "memory_metrics"))]
475 pub fn to_json(&self) -> String {
476 "{}".to_string()
477 }
478
479 pub fn has_potential_leaks(&self) -> bool {
481 self.component_changes
482 .values()
483 .any(|diff| diff.potential_leak)
484 }
485
486 pub fn get_potential_leak_components(&self) -> Vec<String> {
488 self.component_changes
489 .iter()
490 .filter_map(|(component, diff)| {
491 if diff.potential_leak {
492 Some(component.clone())
493 } else {
494 None
495 }
496 })
497 .collect()
498 }
499
500 #[cfg(feature = "memory_visualization")]
502 pub fn visualize(&self) -> String {
503 use crate::memory::metrics::format_bytes;
504
505 let mut visualization = String::new();
506 visualization.push_str(&format!(
507 "Memory Snapshot Visualization: {} → {}\n\n",
508 self.first_id, self.second_id
509 ));
510
511 visualization.push_str(
514 "Component | Current Usage | Peak Usage | Allocations\n",
515 );
516 visualization.push_str(
517 "-------------------------|----------------|----------------|----------------\n",
518 );
519
520 let mut components: Vec<_> = self.component_changes.iter().collect();
522 components.sort_by(|a, b| b.1.current_usage_delta.cmp(&a.1.current_usage_delta));
523
524 for (component, diff) in components {
526 let component_name = if component.len() > 25 {
527 format!("{}...", &component[0..22])
528 } else {
529 component.clone()
530 };
531
532 let current_prefix = if diff.current_usage_delta > 0 {
533 "+"
534 } else {
535 ""
536 };
537 let peak_prefix = if diff.peak_usage_delta > 0 { "+" } else { "" };
538 let alloc_prefix = if diff.allocation_count_delta > 0 {
539 "+"
540 } else {
541 ""
542 };
543 let leak_flag = if diff.potential_leak { "⚠️ " } else { "" };
544
545 visualization.push_str(&format!(
546 "{}{:<23} | {:>14} | {:>14} | {:>14}\n",
547 leak_flag,
548 component_name,
549 format!(
550 "{}{}",
551 current_prefix,
552 format_bytes(diff.current_usage_delta.unsigned_abs())
553 ),
554 format!(
555 "{}{}",
556 peak_prefix,
557 format_bytes(diff.peak_usage_delta.unsigned_abs())
558 ),
559 format!(
560 "{}{}",
561 alloc_prefix,
562 diff.allocation_count_delta.unsigned_abs()
563 )
564 ));
565 }
566
567 visualization.push_str("\nLegend:\n");
569 visualization.push_str("⚠️ = Potential memory leak\n");
570
571 visualization
572 }
573}
574
575pub struct SnapshotManager {
577 snapshots: Vec<MemorySnapshot>,
579}
580
581impl SnapshotManager {
582 pub fn new() -> Self {
584 Self {
585 snapshots: Vec::new(),
586 }
587 }
588
589 pub fn take_snapshot(
591 &mut self,
592 id: impl Into<String>,
593 description: impl Into<String>,
594 ) -> MemorySnapshot {
595 let snapshot = MemorySnapshot::new(id, description);
596 self.snapshots.push(snapshot.clone());
597 snapshot
598 }
599
600 pub fn get_snapshot(&self, id: &str) -> Option<&MemorySnapshot> {
602 self.snapshots.iter().find(|s| s.id == id)
603 }
604
605 pub fn compare_snapshots(&self, first_id: &str, secondid: &str) -> Option<SnapshotDiff> {
607 let first = self.get_snapshot(first_id)?;
608 let second = self.get_snapshot(secondid)?;
609 Some(first.compare(second))
610 }
611
612 pub fn save_all(&self, dir: impl AsRef<Path>) -> io::Result<()> {
614 let dir = dir.as_ref();
615
616 if !dir.exists() {
617 std::fs::create_dir_all(dir)?;
618 }
619
620 for snapshot in &self.snapshots {
621 let filename = format!("snapshot_{id}.json", id = snapshot.id);
622 let path = dir.join(filename);
623 snapshot.save_to_file(path)?;
624 }
625
626 Ok(())
627 }
628
629 pub fn load_all(&mut self, dir: impl AsRef<Path>) -> io::Result<()> {
631 let dir = dir.as_ref();
632
633 if !dir.exists() {
634 return Ok(());
635 }
636
637 for entry in std::fs::read_dir(dir)? {
638 let entry = entry?;
639 let path = entry.path();
640
641 if path.extension().and_then(|s| s.to_str()) == Some("json") {
642 if let Ok(snapshot) = MemorySnapshot::load_from_file(&path) {
643 self.snapshots.push(snapshot);
644 }
645 }
646 }
647
648 Ok(())
649 }
650
651 pub fn get_snapshots(&self) -> &[MemorySnapshot] {
653 &self.snapshots
654 }
655
656 pub fn clear(&mut self) {
658 self.snapshots.clear();
659 }
660}
661
662impl Default for SnapshotManager {
663 fn default() -> Self {
664 Self::new()
665 }
666}
667
668static GLOBAL_SNAPSHOT_MANAGER: once_cell::sync::Lazy<std::sync::Mutex<SnapshotManager>> =
670 once_cell::sync::Lazy::new(|| std::sync::Mutex::new(SnapshotManager::new()));
671
672#[allow(dead_code)]
674pub fn global_snapshot_manager() -> &'static std::sync::Mutex<SnapshotManager> {
675 &GLOBAL_SNAPSHOT_MANAGER
676}
677
678#[allow(dead_code)]
680pub fn take_snapshot(id: impl Into<String>, description: impl Into<String>) -> MemorySnapshot {
681 let mut manager = match global_snapshot_manager().lock() {
682 Ok(guard) => guard,
683 Err(poisoned) => {
684 poisoned.into_inner()
686 }
687 };
688 let snapshot = manager.take_snapshot(id, description);
689 snapshot.clone()
690}
691
692#[allow(dead_code)]
694pub fn compare_snapshots(first_id: &str, secondid: &str) -> Option<SnapshotDiff> {
695 let manager = match global_snapshot_manager().lock() {
696 Ok(guard) => guard,
697 Err(poisoned) => {
698 poisoned.into_inner()
700 }
701 };
702 manager.compare_snapshots(first_id, secondid)
703}
704
705#[allow(dead_code)]
707pub fn save_all_snapshots(dir: impl AsRef<Path>) -> io::Result<()> {
708 let manager = match global_snapshot_manager().lock() {
709 Ok(guard) => guard,
710 Err(poisoned) => {
711 poisoned.into_inner()
713 }
714 };
715 manager.save_all(dir)
716}
717
718#[allow(dead_code)]
720pub fn load_all_snapshots(dir: impl AsRef<Path>) -> io::Result<()> {
721 let mut manager = match global_snapshot_manager().lock() {
722 Ok(guard) => guard,
723 Err(poisoned) => {
724 poisoned.into_inner()
726 }
727 };
728 manager.load_all(dir)
729}
730
731#[allow(dead_code)]
733pub fn clear_snapshots() {
734 let mut manager = match global_snapshot_manager().lock() {
735 Ok(guard) => guard,
736 Err(poisoned) => {
737 poisoned.into_inner()
739 }
740 };
741 manager.clear();
742}
743
744#[cfg(test)]
745mod tests {
746 use super::*;
747 use crate::memory::metrics::{
748 generate_memory_report, reset_memory_metrics, track_allocation, track_deallocation,
749 };
750
751 use crate::memory::metrics::test_utils::MEMORY_METRICS_TEST_MUTEX as TEST_MUTEX;
753
754 #[test]
755 fn test_snapshot_creation() {
756 let lock = TEST_MUTEX
758 .lock()
759 .unwrap_or_else(|poisoned| poisoned.into_inner());
760 println!("test_snapshot_creation started");
761
762 reset_memory_metrics();
764 clear_snapshots();
765
766 let initial_report = generate_memory_report();
768 println!(
769 "Initial state: total_current_usage={}",
770 initial_report.total_current_usage
771 );
772
773 let snapshot1 = MemorySnapshot::new("start", "Initial state");
775
776 assert_eq!(
778 snapshot1.report.total_current_usage, 0,
779 "First snapshot should have 0 memory usage but had {} bytes",
780 snapshot1.report.total_current_usage
781 );
782
783 reset_memory_metrics();
785
786 track_allocation("TestComponent", 1024, 0x1000);
788 track_allocation("TestComponent", 2048, 0x2000);
789
790 let allocations_report = generate_memory_report();
792 println!(
793 "After allocations: total_current_usage={}",
794 allocations_report.total_current_usage
795 );
796
797 let expected_usage = 1024 + 2048; assert_eq!(
800 allocations_report.total_current_usage, expected_usage,
801 "Memory tracking should show {} bytes but showed {} bytes",
802 expected_usage, allocations_report.total_current_usage
803 );
804
805 let snapshot2 = MemorySnapshot::new("allocated", "After allocations");
807
808 assert_eq!(
810 snapshot2.report.total_current_usage, expected_usage,
811 "Second snapshot should have {} bytes but had {} bytes",
812 expected_usage, snapshot2.report.total_current_usage
813 );
814
815 let diff = snapshot1.compare(&snapshot2);
818
819 assert_eq!(
821 diff.current_usage_delta, expected_usage as isize,
822 "Delta between snapshots should be {} bytes but was {} bytes",
823 expected_usage, diff.current_usage_delta
824 );
825
826 reset_memory_metrics();
828
829 let snapshotbase = MemorySnapshot::new("base", "Base for deallocation test");
831
832 assert_eq!(
834 snapshotbase.report.total_current_usage, 0,
835 "Baseline snapshot after reset should have 0 memory usage but had {} bytes",
836 snapshotbase.report.total_current_usage
837 );
838
839 track_allocation("TestComponent", 1024, 0x1000);
841 track_deallocation("TestComponent", 1024, 0x1000);
842
843 let snapshot_final = MemorySnapshot::new("final", "After deallocation");
845 assert_eq!(snapshot_final.report.total_current_usage, 0,
846 "Final snapshot after allocation and deallocation should have 0 memory usage but had {} bytes",
847 snapshot_final.report.total_current_usage);
848
849 reset_memory_metrics();
851 clear_snapshots();
852 println!("test_snapshot_creation completed");
853 }
854
855 #[test]
856 fn test_snapshot_manager() {
857 let lock = TEST_MUTEX
859 .lock()
860 .unwrap_or_else(|poisoned| poisoned.into_inner());
861 println!("test_snapshot_manager started");
862
863 reset_memory_metrics();
865 clear_snapshots();
866
867 let initial_report = generate_memory_report();
869 println!(
870 "Initial state: total_current_usage={}",
871 initial_report.total_current_usage
872 );
873 assert_eq!(
874 initial_report.total_current_usage, 0,
875 "Initial memory usage should be 0 but was {} bytes",
876 initial_report.total_current_usage
877 );
878
879 let mut manager = SnapshotManager::new();
881
882 let snapshot1 = manager.take_snapshot("s1", "First snapshot");
884 assert_eq!(snapshot1.id, "s1", "First snapshot should have ID 's1'");
885 println!(
886 "First snapshot total_current_usage: {}",
887 snapshot1.report.total_current_usage
888 );
889
890 assert_eq!(
892 snapshot1.report.total_current_usage, 0,
893 "First snapshot should have 0 memory usage but had {} bytes",
894 snapshot1.report.total_current_usage
895 );
896
897 reset_memory_metrics();
899
900 track_allocation("TestComponent", 1024, 0x1000);
902
903 let after_alloc_report = generate_memory_report();
905 println!(
906 "After allocation: total_current_usage={}",
907 after_alloc_report.total_current_usage
908 );
909
910 assert_eq!(
912 after_alloc_report.total_current_usage, 1024,
913 "Expected 1024 bytes allocated but found {} bytes",
914 after_alloc_report.total_current_usage
915 );
916
917 let snapshot2 = MemorySnapshot::new("s2", "Second snapshot");
919
920 manager.snapshots.push(snapshot2.clone());
922
923 println!(
924 "Second snapshot total_current_usage: {}",
925 snapshot2.report.total_current_usage
926 );
927
928 assert_eq!(
930 snapshot2.report.total_current_usage, 1024,
931 "Second snapshot should have 1024 bytes but had {} bytes",
932 snapshot2.report.total_current_usage
933 );
934
935 let diff = snapshot1.compare(&snapshot2);
937 assert_eq!(
938 diff.current_usage_delta, 1024,
939 "Delta between snapshots should be 1024 bytes but was {} bytes",
940 diff.current_usage_delta
941 );
942
943 let retrieved = manager.get_snapshot("s1");
945 assert!(retrieved.is_some(), "Snapshot 's1' should exist");
946 assert_eq!(
947 retrieved.expect("Operation failed").id,
948 "s1",
949 "Retrieved snapshot should have ID 's1'"
950 );
951
952 manager.clear();
954 assert!(
955 manager.get_snapshot("s1").is_none(),
956 "After clearing, snapshot 's1' should not exist"
957 );
958
959 reset_memory_metrics();
961 clear_snapshots();
962 println!("test_snapshot_manager completed");
963 }
964
965 #[test]
966 fn test_global_snapshot_manager() {
967 let lock = match TEST_MUTEX.lock() {
969 Ok(guard) => guard,
970 Err(poisoned) => {
971 poisoned.into_inner()
973 }
974 };
975 println!("test_global_snapshot_manager started");
976
977 reset_memory_metrics();
979 clear_snapshots();
980
981 let initial_report = generate_memory_report();
983 println!(
984 "Initial state: total_current_usage={}",
985 initial_report.total_current_usage
986 );
987
988 let snapshot1 = take_snapshot("g1", "Global snapshot 1");
990 assert_eq!(snapshot1.id, "g1", "First snapshot should have ID 'g1'");
991 println!(
992 "First snapshot total_current_usage: {}",
993 snapshot1.report.total_current_usage
994 );
995
996 assert_eq!(
998 snapshot1.report.total_current_usage, 0,
999 "First snapshot should have 0 memory usage but had {} bytes",
1000 snapshot1.report.total_current_usage
1001 );
1002
1003 reset_memory_metrics();
1005
1006 clear_snapshots();
1008
1009 let clean_snapshot = MemorySnapshot::new("clean", "Baseline for comparison");
1011 assert_eq!(clean_snapshot.report.total_current_usage, 0);
1012
1013 track_allocation("TestComponent", 1024, 0x1000);
1015
1016 let after_alloc_report = generate_memory_report();
1018 println!(
1019 "After allocation: total_current_usage={}",
1020 after_alloc_report.total_current_usage
1021 );
1022
1023 assert_eq!(
1025 after_alloc_report.total_current_usage, 1024,
1026 "Expected 1024 bytes allocated but found {} bytes",
1027 after_alloc_report.total_current_usage
1028 );
1029
1030 let snapshot2 = MemorySnapshot::new("g2", "Global snapshot 2");
1032 assert_eq!(snapshot2.id, "g2", "Second snapshot should have ID 'g2'");
1033 println!(
1034 "Second snapshot total_current_usage: {}",
1035 snapshot2.report.total_current_usage
1036 );
1037
1038 assert_eq!(
1040 snapshot2.report.total_current_usage, 1024,
1041 "Second snapshot should have 1024 bytes but had {} bytes",
1042 snapshot2.report.total_current_usage
1043 );
1044
1045 let diff = clean_snapshot.compare(&snapshot2);
1047 assert_eq!(
1048 diff.current_usage_delta, 1024,
1049 "Delta between snapshots should be 1024 bytes but was {} bytes",
1050 diff.current_usage_delta
1051 );
1052
1053 clear_snapshots();
1055 reset_memory_metrics();
1057 println!("test_global_snapshot_manager completed");
1058 }
1059
1060 #[test]
1061 fn test_thread_safety() {
1062 use crate::memory::metrics::{
1064 MemoryEvent, MemoryEventType, MemoryMetricsCollector, MemoryMetricsConfig,
1065 };
1066 use std::sync::{Arc, Barrier};
1067 use std::thread;
1068
1069 let barrier = Arc::new(Barrier::new(3));
1070 let barrier1 = Arc::clone(&barrier);
1071 let barrier2 = Arc::clone(&barrier);
1072
1073 let thread1 = thread::spawn(move || {
1075 let collector = Arc::new(MemoryMetricsCollector::new(MemoryMetricsConfig::default()));
1077
1078 let initial_report = collector.generate_report();
1080 let snapshot =
1081 MemorySnapshot::from_report("thread1", "Initial snapshot", initial_report);
1082
1083 barrier1.wait();
1085
1086 collector.record_event(MemoryEvent::new(
1088 MemoryEventType::Allocation,
1089 "Thread1Component",
1090 2048,
1091 0x1000,
1092 ));
1093
1094 let after_report = collector.generate_report();
1096 let snapshot2 =
1097 MemorySnapshot::from_report("thread1_after", "After allocation", after_report);
1098
1099 let diff = snapshot.compare(&snapshot2);
1101 assert_eq!(diff.current_usage_delta, 2048);
1102 });
1103
1104 let thread2 = thread::spawn(move || {
1106 let collector = Arc::new(MemoryMetricsCollector::new(MemoryMetricsConfig::default()));
1108
1109 let initial_report = collector.generate_report();
1111 let snapshot =
1112 MemorySnapshot::from_report("thread2", "Initial snapshot", initial_report);
1113
1114 barrier2.wait();
1116
1117 collector.record_event(MemoryEvent::new(
1119 MemoryEventType::Allocation,
1120 "Thread2Component",
1121 4096,
1122 0x2000,
1123 ));
1124
1125 let after_report = collector.generate_report();
1127 let snapshot2 =
1128 MemorySnapshot::from_report("thread2_after", "After allocation", after_report);
1129
1130 let diff = snapshot.compare(&snapshot2);
1132 assert_eq!(diff.current_usage_delta, 4096);
1133 });
1134
1135 barrier.wait();
1137
1138 thread1.join().expect("Operation failed");
1139 thread2.join().expect("Operation failed");
1140 }
1141
1142 #[test]
1143 fn test_thread_safety_isolated() {
1144 use crate::memory::metrics::{
1146 MemoryEvent, MemoryEventType, MemoryMetricsCollector, MemoryMetricsConfig,
1147 };
1148 use std::sync::{Arc, Barrier};
1149 use std::thread;
1150
1151 let barrier = Arc::new(Barrier::new(3));
1152 let barrier1 = Arc::clone(&barrier);
1153 let barrier2 = Arc::clone(&barrier);
1154
1155 let thread1 = thread::spawn(move || {
1157 let collector = Arc::new(MemoryMetricsCollector::new(MemoryMetricsConfig::default()));
1159
1160 let initial_report = collector.generate_report();
1162 let snapshot =
1163 MemorySnapshot::from_report("thread1", "Initial snapshot", initial_report);
1164
1165 barrier1.wait();
1167
1168 collector.record_event(MemoryEvent::new(
1170 MemoryEventType::Allocation,
1171 "Thread1Component",
1172 2048,
1173 0x1000,
1174 ));
1175
1176 let after_report = collector.generate_report();
1178 let snapshot2 =
1179 MemorySnapshot::from_report("thread1_after", "After allocation", after_report);
1180
1181 let diff = snapshot.compare(&snapshot2);
1183 assert_eq!(diff.current_usage_delta, 2048);
1184 assert!(
1185 diff.new_components
1186 .contains(&"Thread1Component".to_string()),
1187 "Thread1Component should be in new_components"
1188 );
1189 assert!(
1190 diff.removed_components.is_empty(),
1191 "No components should be removed"
1192 );
1193 });
1194
1195 let thread2 = thread::spawn(move || {
1197 let collector = Arc::new(MemoryMetricsCollector::new(MemoryMetricsConfig::default()));
1199
1200 let initial_report = collector.generate_report();
1202 let snapshot =
1203 MemorySnapshot::from_report("thread2", "Initial snapshot", initial_report);
1204
1205 barrier2.wait();
1207
1208 collector.record_event(MemoryEvent::new(
1210 MemoryEventType::Allocation,
1211 "Thread2Component",
1212 4096,
1213 0x2000,
1214 ));
1215
1216 let after_report = collector.generate_report();
1218 let snapshot2 =
1219 MemorySnapshot::from_report("thread2_after", "After allocation", after_report);
1220
1221 let diff = snapshot.compare(&snapshot2);
1223 assert_eq!(diff.current_usage_delta, 4096);
1224 assert!(
1225 diff.new_components
1226 .contains(&"Thread2Component".to_string()),
1227 "Thread2Component should be in new_components"
1228 );
1229 assert!(
1230 diff.removed_components.is_empty(),
1231 "No components should be removed"
1232 );
1233 });
1234
1235 barrier.wait();
1237
1238 thread1.join().expect("Operation failed");
1239 thread2.join().expect("Operation failed");
1240
1241 println!("Thread safety test with isolated collectors completed successfully");
1242 }
1243
1244 #[test]
1245 fn test_leak_detection() {
1246 let lock = TEST_MUTEX
1248 .lock()
1249 .unwrap_or_else(|poisoned| poisoned.into_inner());
1250 println!("test_leak_detection started");
1251
1252 reset_memory_metrics();
1254 clear_snapshots();
1255
1256 let initial_report = generate_memory_report();
1258 println!(
1259 "Initial state: total_current_usage={}",
1260 initial_report.total_current_usage
1261 );
1262
1263 let snapshot1 = MemorySnapshot::new("leak_test_1", "Before potential leak");
1265 println!(
1266 "First snapshot total_current_usage: {}",
1267 snapshot1.report.total_current_usage
1268 );
1269
1270 assert_eq!(
1272 snapshot1.report.total_current_usage, 0,
1273 "Initial snapshot should have 0 memory usage but had {} bytes",
1274 snapshot1.report.total_current_usage
1275 );
1276
1277 reset_memory_metrics();
1279
1280 track_allocation("LeakyComponent", 4096, 0x3000);
1282
1283 let after_alloc_report = generate_memory_report();
1285 println!(
1286 "After allocation: total_current_usage={}",
1287 after_alloc_report.total_current_usage
1288 );
1289
1290 assert_eq!(
1292 after_alloc_report.total_current_usage, 4096,
1293 "Expected 4096 bytes allocated but found {} bytes",
1294 after_alloc_report.total_current_usage
1295 );
1296
1297 let snapshot2 = MemorySnapshot::new("leak_test_2", "After operations");
1299 println!(
1300 "Second snapshot total_current_usage: {}",
1301 snapshot2.report.total_current_usage
1302 );
1303
1304 assert_eq!(
1306 snapshot2.report.total_current_usage, 4096,
1307 "Second snapshot should have 4096 bytes but had {} bytes",
1308 snapshot2.report.total_current_usage
1309 );
1310
1311 let diff = snapshot1.compare(&snapshot2);
1313
1314 assert_eq!(
1316 diff.current_usage_delta, 4096,
1317 "Delta between snapshots should be 4096 bytes but was {} bytes",
1318 diff.current_usage_delta
1319 );
1320
1321 assert!(
1324 diff.current_usage_delta > 0,
1325 "Memory increase should be detected as potential leak"
1326 );
1327
1328 let mut component_diffs = HashMap::new();
1330 component_diffs.insert(
1331 "LeakyComponent".to_string(),
1332 ComponentStatsDiff {
1333 current_usage_delta: 4096,
1334 peak_usage_delta: 4096,
1335 allocation_count_delta: 1,
1336 total_allocated_delta: 4096,
1337 potential_leak: true,
1338 },
1339 );
1340
1341 let test_diff = SnapshotDiff {
1342 first_id: "test1".to_string(),
1343 second_id: "test2".to_string(),
1344 elapsed_ms: 100,
1345 current_usage_delta: 4096,
1346 peak_usage_delta: 4096,
1347 allocation_count_delta: 1,
1348 new_components: vec!["LeakyComponent".to_string()],
1349 removed_components: vec![],
1350 component_changes: component_diffs,
1351 };
1352
1353 assert!(
1355 test_diff.has_potential_leaks(),
1356 "Leak detection should identify potential leaks"
1357 );
1358
1359 let leak_components = test_diff.get_potential_leak_components();
1361 assert_eq!(
1362 leak_components.len(),
1363 1,
1364 "Should find exactly one leaky component"
1365 );
1366 assert_eq!(
1367 leak_components[0], "LeakyComponent",
1368 "Leaky component should be 'LeakyComponent'"
1369 );
1370
1371 reset_memory_metrics();
1373 clear_snapshots();
1374 println!("test_leak_detection completed");
1375 }
1376}