scirs2_core/memory/metrics/
snapshot.rs

1//! Memory metrics snapshot system
2//!
3//! This module provides functionality for capturing, saving, and comparing
4//! memory usage snapshots to track changes and identify potential leaks.
5
6use 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/// A snapshot of memory usage at a point in time
25#[derive(Debug, Clone)]
26#[cfg_attr(feature = "memory_metrics", derive(Serialize, Deserialize))]
27pub struct MemorySnapshot {
28    /// Unique identifier for the snapshot
29    pub id: String,
30
31    /// Timestamp when the snapshot was taken
32    pub timestamp: u64,
33
34    /// Description of the snapshot
35    pub description: String,
36
37    /// Memory report at the time of the snapshot
38    pub report: SnapshotReport,
39
40    /// Additional metadata
41    pub metadata: HashMap<String, String>,
42}
43
44/// A simplified version of MemoryReport for snapshots
45#[derive(Debug, Clone)]
46#[cfg_attr(feature = "memory_metrics", derive(Serialize, Deserialize))]
47pub struct SnapshotReport {
48    /// Total current memory usage across all components
49    pub total_current_usage: usize,
50
51    /// Total peak memory usage across all components
52    pub total_peak_usage: usize,
53
54    /// Total number of allocations across all components
55    pub total_allocation_count: usize,
56
57    /// Total bytes allocated across all components
58    pub total_allocated_bytes: usize,
59
60    /// Component-specific statistics
61    pub component_stats: HashMap<String, SnapshotComponentStats>,
62
63    /// Duration since tracking started
64    pub duration_ms: u64,
65}
66
67/// A simplified version of ComponentMemoryStats for snapshots
68#[derive(Debug, Clone)]
69#[cfg_attr(feature = "memory_metrics", derive(Serialize, Deserialize))]
70pub struct SnapshotComponentStats {
71    /// Current memory usage
72    pub current_usage: usize,
73
74    /// Peak memory usage
75    pub peak_usage: usize,
76
77    /// Number of allocations
78    pub allocation_count: usize,
79
80    /// Total bytes allocated
81    pub total_allocated: usize,
82
83    /// Average allocation size
84    pub avg_allocation_size: f64,
85}
86
87impl MemorySnapshot {
88    /// Create a new memory snapshot
89    pub fn new(id: impl Into<String>, description: impl Into<String>) -> Self {
90        // Get current report
91        let report = generate_memory_report();
92
93        // Get current time as unix timestamp
94        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    /// Create a new memory snapshot from a specific memory report
109    pub fn from_report(
110        id: impl Into<String>,
111        description: impl Into<String>,
112        report: MemoryReport,
113    ) -> Self {
114        // Get current time as unix timestamp
115        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    /// Add metadata to the snapshot
130    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    /// Save the snapshot to a file
136    #[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    /// Save the snapshot to a file - stub when memory_metrics is disabled
145    #[cfg(not(feature = "memory_metrics"))]
146    pub fn save_to_file(&self, path: impl AsRef<Path>) -> io::Result<()> {
147        Ok(())
148    }
149
150    /// Load a snapshot from a file
151    #[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    /// Load a snapshot from a file - stub when memory_metrics is disabled
161    #[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    /// Compare this snapshot with another snapshot
167    pub fn compare(&self, other: &MemorySnapshot) -> SnapshotDiff {
168        SnapshotDiff::new(self, other)
169    }
170
171    /// Export the snapshot as JSON
172    #[cfg(feature = "memory_metrics")]
173    pub fn to_json(&self) -> JsonValue {
174        serde_json::to_value(self).unwrap_or(JsonValue::Null)
175    }
176
177    /// Export the snapshot as JSON - stub when memory_metrics is disabled
178    #[cfg(not(feature = "memory_metrics"))]
179    pub fn to_json(&self) -> String {
180        "{}".to_string()
181    }
182}
183
184/// Conversion from MemoryReport to SnapshotReport
185impl 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/// The difference between two memory snapshots
214#[derive(Debug, Clone)]
215#[cfg_attr(feature = "memory_metrics", derive(Serialize, Deserialize))]
216pub struct SnapshotDiff {
217    /// First snapshot ID
218    pub first_id: String,
219
220    /// Second snapshot ID
221    pub second_id: String,
222
223    /// Time elapsed between snapshots in milliseconds
224    pub elapsed_ms: u64,
225
226    /// Change in total current usage
227    pub current_usage_delta: isize,
228
229    /// Change in total peak usage
230    pub peak_usage_delta: isize,
231
232    /// Change in total allocation count
233    pub allocation_count_delta: isize,
234
235    /// New components that weren't in the first snapshot
236    pub new_components: Vec<String>,
237
238    /// Components that were in the first snapshot but not in the second
239    pub removed_components: Vec<String>,
240
241    /// Changes in component statistics
242    pub component_changes: HashMap<String, ComponentStatsDiff>,
243}
244
245/// The difference in memory statistics for a component
246#[derive(Debug, Clone)]
247#[cfg_attr(feature = "memory_metrics", derive(Serialize, Deserialize))]
248pub struct ComponentStatsDiff {
249    /// Change in current usage
250    pub current_usage_delta: isize,
251
252    /// Change in peak usage
253    pub peak_usage_delta: isize,
254
255    /// Change in allocation count
256    pub allocation_count_delta: isize,
257
258    /// Change in total allocated bytes
259    pub total_allocated_delta: isize,
260
261    /// Potential memory leak (positive current usage delta)
262    pub potential_leak: bool,
263}
264
265impl SnapshotDiff {
266    /// Create a new snapshot diff by comparing two snapshots
267    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        // Find new and removed components
278        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        // Calculate component-specific changes
294        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                // Detect potential leaks (positive current usage regardless of allocations)
308                // In our test environment, we might have allocations but still detect leaks
309                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    /// Format the diff as a string
338    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        // Overall changes
353        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        // New components
385        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        // Removed components
394        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        // Component changes
403        if !self.component_changes.is_empty() {
404            output.push_str("Component Changes:\n");
405
406            // Sort components by current usage delta (descending)
407            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    /// Export the diff as JSON
468    #[cfg(feature = "memory_metrics")]
469    pub fn to_json(&self) -> JsonValue {
470        serde_json::to_value(self).unwrap_or(JsonValue::Null)
471    }
472
473    /// Export the diff as JSON - stub when memory_metrics is disabled
474    #[cfg(not(feature = "memory_metrics"))]
475    pub fn to_json(&self) -> String {
476        "{}".to_string()
477    }
478
479    /// Check if there are any potential memory leaks
480    pub fn has_potential_leaks(&self) -> bool {
481        self.component_changes
482            .values()
483            .any(|diff| diff.potential_leak)
484    }
485
486    /// Get a list of components with potential memory leaks
487    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    /// Generate a visualization of the memory usage changes
501    #[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        // Create basic text-based visualization
512        // Header
513        visualization.push_str(
514            "Component                |   Current Usage |     Peak Usage |   Allocations\n",
515        );
516        visualization.push_str(
517            "-------------------------|----------------|----------------|----------------\n",
518        );
519
520        // Sort components by current usage delta (descending)
521        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        // Table rows
525        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        // Summary
568        visualization.push_str("\nLegend:\n");
569        visualization.push_str("⚠️ = Potential memory leak\n");
570
571        visualization
572    }
573}
574
575/// Memory snapshots manager
576pub struct SnapshotManager {
577    /// List of snapshots
578    snapshots: Vec<MemorySnapshot>,
579}
580
581impl SnapshotManager {
582    /// Create a new snapshot manager
583    pub fn new() -> Self {
584        Self {
585            snapshots: Vec::new(),
586        }
587    }
588
589    /// Take a new snapshot
590    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    /// Get a snapshot by ID
601    pub fn get_snapshot(&self, id: &str) -> Option<&MemorySnapshot> {
602        self.snapshots.iter().find(|s| s.id == id)
603    }
604
605    /// Compare two snapshots
606    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    /// Save all snapshots to a directory
613    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    /// Load all snapshots from a directory
630    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    /// Get all snapshots
652    pub fn get_snapshots(&self) -> &[MemorySnapshot] {
653        &self.snapshots
654    }
655
656    /// Clear all snapshots
657    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
668/// Global snapshot manager instance
669static 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/// Get the global snapshot manager instance
673#[allow(dead_code)]
674pub fn global_snapshot_manager() -> &'static std::sync::Mutex<SnapshotManager> {
675    &GLOBAL_SNAPSHOT_MANAGER
676}
677
678/// Take a snapshot using the global snapshot manager
679#[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            // Still use the poisoned lock by recovering the guard
685            poisoned.into_inner()
686        }
687    };
688    let snapshot = manager.take_snapshot(id, description);
689    snapshot.clone()
690}
691
692/// Compare two snapshots using the global snapshot manager
693#[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            // Still use the poisoned lock by recovering the guard
699            poisoned.into_inner()
700        }
701    };
702    manager.compare_snapshots(first_id, secondid)
703}
704
705/// Save all snapshots to a directory using the global snapshot manager
706#[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            // Still use the poisoned lock by recovering the guard
712            poisoned.into_inner()
713        }
714    };
715    manager.save_all(dir)
716}
717
718/// Load all snapshots from a directory using the global snapshot manager
719#[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            // Still use the poisoned lock by recovering the guard
725            poisoned.into_inner()
726        }
727    };
728    manager.load_all(dir)
729}
730
731/// Clear all snapshots using the global snapshot manager
732#[allow(dead_code)]
733pub fn clear_snapshots() {
734    let mut manager = match global_snapshot_manager().lock() {
735        Ok(guard) => guard,
736        Err(poisoned) => {
737            // Still use the poisoned lock by recovering the guard
738            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    /// Synchronize tests using a mutex to prevent concurrent use of the global metrics collector
752    use crate::memory::metrics::test_utils::MEMORY_METRICS_TEST_MUTEX as TEST_MUTEX;
753
754    #[test]
755    fn test_snapshot_creation() {
756        // Use unwrap_or_else to make sure we can continue even with a poisoned mutex
757        let lock = TEST_MUTEX
758            .lock()
759            .unwrap_or_else(|poisoned| poisoned.into_inner());
760        println!("test_snapshot_creation started");
761
762        // First make sure all global state is clean
763        reset_memory_metrics();
764        clear_snapshots();
765
766        // Print initial state for debugging
767        let initial_report = generate_memory_report();
768        println!(
769            "Initial state: total_current_usage={}",
770            initial_report.total_current_usage
771        );
772
773        // Take a snapshot with no allocations
774        let snapshot1 = MemorySnapshot::new("start", "Initial state");
775
776        // Verify no allocations in first snapshot
777        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 to get clean metrics state
784        reset_memory_metrics();
785
786        // Allocate some memory in a known clean state
787        track_allocation("TestComponent", 1024, 0x1000);
788        track_allocation("TestComponent", 2048, 0x2000);
789
790        // Print state after allocations
791        let allocations_report = generate_memory_report();
792        println!(
793            "After allocations: total_current_usage={}",
794            allocations_report.total_current_usage
795        );
796
797        // First verify our memory tracking is correct
798        let expected_usage = 1024 + 2048; // From the two allocations above
799        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        // Take another snapshot
806        let snapshot2 = MemorySnapshot::new("allocated", "After allocations");
807
808        // Check that we have the expected memory usage (should be 3072 bytes)
809        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        // Create a direct comparison (not using the global snapshots)
816        // This ensures we're just comparing these two snapshots directly
817        let diff = snapshot1.compare(&snapshot2);
818
819        // Verify correct deltas
820        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 and test deallocation
827        reset_memory_metrics();
828
829        // Create a new baseline snapshot
830        let snapshotbase = MemorySnapshot::new("base", "Base for deallocation test");
831
832        // Verify that it starts with zero
833        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        // Allocate and then deallocate
840        track_allocation("TestComponent", 1024, 0x1000);
841        track_deallocation("TestComponent", 1024, 0x1000);
842
843        // Final snapshot should show no memory usage
844        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        // Clean up for the next test
850        reset_memory_metrics();
851        clear_snapshots();
852        println!("test_snapshot_creation completed");
853    }
854
855    #[test]
856    fn test_snapshot_manager() {
857        // Use unwrap_or_else for better error handling
858        let lock = TEST_MUTEX
859            .lock()
860            .unwrap_or_else(|poisoned| poisoned.into_inner());
861        println!("test_snapshot_manager started");
862
863        // Reset metrics and snapshots - do this BEFORE creating anything
864        reset_memory_metrics();
865        clear_snapshots();
866
867        // Verify the initial state is clean
868        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        // Create a snapshot manager
880        let mut manager = SnapshotManager::new();
881
882        // Take a snapshot
883        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        // Verify there are no allocations to start with
891        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 before allocating
898        reset_memory_metrics();
899
900        // Allocate some memory
901        track_allocation("TestComponent", 1024, 0x1000);
902
903        // Print state after allocation
904        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        // Verify the reported memory is as expected
911        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        // Take another snapshot
918        let snapshot2 = MemorySnapshot::new("s2", "Second snapshot");
919
920        // Add the snapshot to our manager for testing
921        manager.snapshots.push(snapshot2.clone());
922
923        println!(
924            "Second snapshot total_current_usage: {}",
925            snapshot2.report.total_current_usage
926        );
927
928        // Verify the allocation is recorded in the second snapshot
929        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        // Compare snapshots directly
936        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        // Get a snapshot by ID
944        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        // Clear snapshots
953        manager.clear();
954        assert!(
955            manager.get_snapshot("s1").is_none(),
956            "After clearing, snapshot 's1' should not exist"
957        );
958
959        // Clean up for the next test
960        reset_memory_metrics();
961        clear_snapshots();
962        println!("test_snapshot_manager completed");
963    }
964
965    #[test]
966    fn test_global_snapshot_manager() {
967        // Lock to prevent concurrent access to global state
968        let lock = match TEST_MUTEX.lock() {
969            Ok(guard) => guard,
970            Err(poisoned) => {
971                // Still use the poisoned lock by recovering the guard
972                poisoned.into_inner()
973            }
974        };
975        println!("test_global_snapshot_manager started");
976
977        // Ensure we have a clean state to start with
978        reset_memory_metrics();
979        clear_snapshots();
980
981        // Print initial state for debugging
982        let initial_report = generate_memory_report();
983        println!(
984            "Initial state: total_current_usage={}",
985            initial_report.total_current_usage
986        );
987
988        // Take a snapshot with no allocations
989        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        // Verify there are no allocations in the first snapshot
997        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 metrics to get a clean state
1004        reset_memory_metrics();
1005
1006        // Explicitly clear snapshots again to ensure no interference
1007        clear_snapshots();
1008
1009        // Take a snapshot with no allocations for the "before" state
1010        let clean_snapshot = MemorySnapshot::new("clean", "Baseline for comparison");
1011        assert_eq!(clean_snapshot.report.total_current_usage, 0);
1012
1013        // Allocate some memory (exactly 1024 bytes after reset)
1014        track_allocation("TestComponent", 1024, 0x1000);
1015
1016        // Print state after allocation
1017        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        // Verify exactly 1024 bytes are allocated
1024        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        // Take another snapshot with just our new allocation directly (not via global manager)
1031        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        // Verify the allocation is recorded in the second snapshot
1039        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        // Compare snapshots directly - we should have exactly 1024 bytes difference
1046        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        // Clean up by clearing snapshots for the next test
1054        clear_snapshots();
1055        // Also reset memory metrics again to leave a clean state
1056        reset_memory_metrics();
1057        println!("test_global_snapshot_manager completed");
1058    }
1059
1060    #[test]
1061    fn test_thread_safety() {
1062        // Test thread safety using separate collectors per thread to avoid global state interference
1063        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        // Thread 1: Use separate collector to avoid global state interference
1074        let thread1 = thread::spawn(move || {
1075            // Create a dedicated collector for this thread
1076            let collector = Arc::new(MemoryMetricsCollector::new(MemoryMetricsConfig::default()));
1077
1078            // Take initial snapshot from this collector
1079            let initial_report = collector.generate_report();
1080            let snapshot =
1081                MemorySnapshot::from_report("thread1", "Initial snapshot", initial_report);
1082
1083            // Wait for all threads to reach this point
1084            barrier1.wait();
1085
1086            // Record allocation in this thread's collector
1087            collector.record_event(MemoryEvent::new(
1088                MemoryEventType::Allocation,
1089                "Thread1Component",
1090                2048,
1091                0x1000,
1092            ));
1093
1094            // Take snapshot after allocation
1095            let after_report = collector.generate_report();
1096            let snapshot2 =
1097                MemorySnapshot::from_report("thread1_after", "After allocation", after_report);
1098
1099            // Verify the diff shows the expected allocation
1100            let diff = snapshot.compare(&snapshot2);
1101            assert_eq!(diff.current_usage_delta, 2048);
1102        });
1103
1104        // Thread 2: Use separate collector to avoid global state interference
1105        let thread2 = thread::spawn(move || {
1106            // Create a dedicated collector for this thread
1107            let collector = Arc::new(MemoryMetricsCollector::new(MemoryMetricsConfig::default()));
1108
1109            // Take initial snapshot from this collector
1110            let initial_report = collector.generate_report();
1111            let snapshot =
1112                MemorySnapshot::from_report("thread2", "Initial snapshot", initial_report);
1113
1114            // Wait for all threads to reach this point
1115            barrier2.wait();
1116
1117            // Record allocation in this thread's collector
1118            collector.record_event(MemoryEvent::new(
1119                MemoryEventType::Allocation,
1120                "Thread2Component",
1121                4096,
1122                0x2000,
1123            ));
1124
1125            // Take snapshot after allocation
1126            let after_report = collector.generate_report();
1127            let snapshot2 =
1128                MemorySnapshot::from_report("thread2_after", "After allocation", after_report);
1129
1130            // Verify the diff shows the expected allocation
1131            let diff = snapshot.compare(&snapshot2);
1132            assert_eq!(diff.current_usage_delta, 4096);
1133        });
1134
1135        // Main thread: Wait for other threads to complete
1136        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        // Test thread safety using separate collectors per thread to avoid interference
1145        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        // Thread 1: Use separate collector to avoid global state interference
1156        let thread1 = thread::spawn(move || {
1157            // Create a dedicated collector for this thread
1158            let collector = Arc::new(MemoryMetricsCollector::new(MemoryMetricsConfig::default()));
1159
1160            // Take initial snapshot from this collector
1161            let initial_report = collector.generate_report();
1162            let snapshot =
1163                MemorySnapshot::from_report("thread1", "Initial snapshot", initial_report);
1164
1165            // Wait for all threads to reach this point
1166            barrier1.wait();
1167
1168            // Record allocation in this thread's collector
1169            collector.record_event(MemoryEvent::new(
1170                MemoryEventType::Allocation,
1171                "Thread1Component",
1172                2048,
1173                0x1000,
1174            ));
1175
1176            // Take snapshot after allocation
1177            let after_report = collector.generate_report();
1178            let snapshot2 =
1179                MemorySnapshot::from_report("thread1_after", "After allocation", after_report);
1180
1181            // Verify the diff shows the expected allocation
1182            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        // Thread 2: Use separate collector to avoid global state interference
1196        let thread2 = thread::spawn(move || {
1197            // Create a dedicated collector for this thread
1198            let collector = Arc::new(MemoryMetricsCollector::new(MemoryMetricsConfig::default()));
1199
1200            // Take initial snapshot from this collector
1201            let initial_report = collector.generate_report();
1202            let snapshot =
1203                MemorySnapshot::from_report("thread2", "Initial snapshot", initial_report);
1204
1205            // Wait for all threads to reach this point
1206            barrier2.wait();
1207
1208            // Record allocation in this thread's collector
1209            collector.record_event(MemoryEvent::new(
1210                MemoryEventType::Allocation,
1211                "Thread2Component",
1212                4096,
1213                0x2000,
1214            ));
1215
1216            // Take snapshot after allocation
1217            let after_report = collector.generate_report();
1218            let snapshot2 =
1219                MemorySnapshot::from_report("thread2_after", "After allocation", after_report);
1220
1221            // Verify the diff shows the expected allocation
1222            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        // Main thread: Wait for other threads to complete
1236        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        // Use unwrap_or_else for better error handling
1247        let lock = TEST_MUTEX
1248            .lock()
1249            .unwrap_or_else(|poisoned| poisoned.into_inner());
1250        println!("test_leak_detection started");
1251
1252        // Ensure we have a clean state to start with
1253        reset_memory_metrics();
1254        clear_snapshots();
1255
1256        // Print initial state for debugging
1257        let initial_report = generate_memory_report();
1258        println!(
1259            "Initial state: total_current_usage={}",
1260            initial_report.total_current_usage
1261        );
1262
1263        // Take initial snapshot with no allocations
1264        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        // Verify there are no allocations to start with
1271        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 again to ensure clean state
1278        reset_memory_metrics();
1279
1280        // Allocate memory without tracking deallocation (simulating a leak)
1281        track_allocation("LeakyComponent", 4096, 0x3000);
1282
1283        // Print state after allocation
1284        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        // Verify exactly 4096 bytes are now allocated
1291        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        // Take snapshot after our leak
1298        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        // Verify the second snapshot has exactly the allocation we made
1305        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        // Compare the snapshots
1312        let diff = snapshot1.compare(&snapshot2);
1313
1314        // We should have a leaked memory of 4096 bytes
1315        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        // Our simple leak detection identifies any memory increase as a potential leak
1322        // This is a simplification for the test
1323        assert!(
1324            diff.current_usage_delta > 0,
1325            "Memory increase should be detected as potential leak"
1326        );
1327
1328        // Let's artificially introduce a component with a leak
1329        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        // This should detect potential leaks
1354        assert!(
1355            test_diff.has_potential_leaks(),
1356            "Leak detection should identify potential leaks"
1357        );
1358
1359        // And identify the leaky component
1360        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        // Clean up for the next test
1372        reset_memory_metrics();
1373        clear_snapshots();
1374        println!("test_leak_detection completed");
1375    }
1376}