use std::collections::HashMap;
#[cfg(feature = "memory_metrics")]
use std::fs::File;
use std::io;
#[cfg(feature = "memory_metrics")]
use std::io::{Read, Write};
use std::path::Path;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
#[cfg(feature = "memory_metrics")]
#[cfg(feature = "serialization")]
use serde::{Deserialize, Serialize};
#[cfg(feature = "memory_metrics")]
use serde_json::Value as JsonValue;
use crate::memory::metrics::collector::MemoryReport;
use crate::memory::metrics::generate_memory_report;
#[derive(Debug, Clone)]
#[cfg_attr(feature = "memory_metrics", derive(Serialize, Deserialize))]
pub struct MemorySnapshot {
pub id: String,
pub timestamp: u64,
pub description: String,
pub report: SnapshotReport,
pub metadata: HashMap<String, String>,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "memory_metrics", derive(Serialize, Deserialize))]
pub struct SnapshotReport {
pub total_current_usage: usize,
pub total_peak_usage: usize,
pub total_allocation_count: usize,
pub total_allocated_bytes: usize,
pub component_stats: HashMap<String, SnapshotComponentStats>,
pub duration_ms: u64,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "memory_metrics", derive(Serialize, Deserialize))]
pub struct SnapshotComponentStats {
pub current_usage: usize,
pub peak_usage: usize,
pub allocation_count: usize,
pub total_allocated: usize,
pub avg_allocation_size: f64,
}
impl MemorySnapshot {
pub fn new(id: impl Into<String>, description: impl Into<String>) -> Self {
let report = generate_memory_report();
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_else(|_| Duration::from_secs(0))
.as_secs();
Self {
id: id.into(),
timestamp,
description: description.into(),
report: SnapshotReport::from(report),
metadata: HashMap::new(),
}
}
pub fn from_report(
id: impl Into<String>,
description: impl Into<String>,
report: MemoryReport,
) -> Self {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_else(|_| Duration::from_secs(0))
.as_secs();
Self {
id: id.into(),
timestamp,
description: description.into(),
report: SnapshotReport::from(report),
metadata: HashMap::new(),
}
}
pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.insert(key.into(), value.into());
self
}
#[cfg(feature = "memory_metrics")]
pub fn save_to_file(&self, path: impl AsRef<Path>) -> io::Result<()> {
let json = serde_json::to_string_pretty(self)?;
let mut file = File::create(path)?;
file.write_all(json.as_bytes())?;
Ok(())
}
#[cfg(not(feature = "memory_metrics"))]
pub fn save_to_file(&self, path: impl AsRef<Path>) -> io::Result<()> {
Ok(())
}
#[cfg(feature = "memory_metrics")]
pub fn load_from_file(path: impl AsRef<Path>) -> io::Result<Self> {
let mut file = File::open(path)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
let snapshot = serde_json::from_str(&contents)?;
Ok(snapshot)
}
#[cfg(not(feature = "memory_metrics"))]
pub fn load_from_file(path: impl AsRef<Path>) -> io::Result<Self> {
Ok(Self::new("stub_id", "stub_description"))
}
pub fn compare(&self, other: &MemorySnapshot) -> SnapshotDiff {
SnapshotDiff::new(self, other)
}
#[cfg(feature = "memory_metrics")]
pub fn to_json(&self) -> JsonValue {
serde_json::to_value(self).unwrap_or(JsonValue::Null)
}
#[cfg(not(feature = "memory_metrics"))]
pub fn to_json(&self) -> String {
"{}".to_string()
}
}
impl From<MemoryReport> for SnapshotReport {
fn from(report: MemoryReport) -> Self {
let mut component_stats = HashMap::new();
for (name, stats) in report.component_stats {
component_stats.insert(
name,
SnapshotComponentStats {
current_usage: stats.current_usage,
peak_usage: stats.peak_usage,
allocation_count: stats.allocation_count,
total_allocated: stats.total_allocated,
avg_allocation_size: stats.avg_allocation_size,
},
);
}
Self {
total_current_usage: report.total_current_usage,
total_peak_usage: report.total_peak_usage,
total_allocation_count: report.total_allocation_count,
total_allocated_bytes: report.total_allocated_bytes,
component_stats,
duration_ms: report.duration.as_millis() as u64,
}
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "memory_metrics", derive(Serialize, Deserialize))]
pub struct SnapshotDiff {
pub first_id: String,
pub second_id: String,
pub elapsed_ms: u64,
pub current_usage_delta: isize,
pub peak_usage_delta: isize,
pub allocation_count_delta: isize,
pub new_components: Vec<String>,
pub removed_components: Vec<String>,
pub component_changes: HashMap<String, ComponentStatsDiff>,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "memory_metrics", derive(Serialize, Deserialize))]
pub struct ComponentStatsDiff {
pub current_usage_delta: isize,
pub peak_usage_delta: isize,
pub allocation_count_delta: isize,
pub total_allocated_delta: isize,
pub potential_leak: bool,
}
impl SnapshotDiff {
pub fn new(first: &MemorySnapshot, second: &MemorySnapshot) -> Self {
let elapsed_ms = second.timestamp.saturating_sub(first.timestamp) * 1000;
let current_usage_delta =
second.report.total_current_usage as isize - first.report.total_current_usage as isize;
let peak_usage_delta =
second.report.total_peak_usage as isize - first.report.total_peak_usage as isize;
let allocation_count_delta = second.report.total_allocation_count as isize
- first.report.total_allocation_count as isize;
let mut new_components = Vec::new();
let mut removed_components = Vec::new();
for component in second.report.component_stats.keys() {
if !first.report.component_stats.contains_key(component) {
new_components.push(component.clone());
}
}
for component in first.report.component_stats.keys() {
if !second.report.component_stats.contains_key(component) {
removed_components.push(component.clone());
}
}
let mut component_changes = HashMap::new();
for (component, second_stats) in &second.report.component_stats {
if let Some(first_stats) = first.report.component_stats.get(component) {
let current_usage_delta =
second_stats.current_usage as isize - first_stats.current_usage as isize;
let peak_usage_delta =
second_stats.peak_usage as isize - first_stats.peak_usage as isize;
let allocation_count_delta =
second_stats.allocation_count as isize - first_stats.allocation_count as isize;
let total_allocated_delta =
second_stats.total_allocated as isize - first_stats.total_allocated as isize;
let potential_leak = current_usage_delta > 0;
component_changes.insert(
component.clone(),
ComponentStatsDiff {
current_usage_delta,
peak_usage_delta,
allocation_count_delta,
total_allocated_delta,
potential_leak,
},
);
}
}
Self {
first_id: first.id.clone(),
second_id: second.id.clone(),
elapsed_ms,
current_usage_delta,
peak_usage_delta,
allocation_count_delta,
new_components,
removed_components,
component_changes,
}
}
pub fn format(&self) -> String {
use crate::memory::metrics::format_bytes;
let mut output = String::new();
output.push_str(&format!(
"Memory Snapshot Diff: {} -> {}\n",
self.first_id, self.second_id
));
output.push_str(&format!(
"Time elapsed: {elapsed}ms\n\n",
elapsed = self.elapsed_ms
));
let current_delta_prefix = if self.current_usage_delta >= 0 {
"+"
} else {
""
};
let peak_delta_prefix = if self.peak_usage_delta >= 0 { "+" } else { "" };
let alloc_delta_prefix = if self.allocation_count_delta >= 0 {
"+"
} else {
""
};
output.push_str(&format!(
"Total Current Usage: {}{}B ({})\n",
current_delta_prefix,
self.current_usage_delta,
format_bytes(self.current_usage_delta.unsigned_abs())
));
output.push_str(&format!(
"Total Peak Usage: {}{}B ({})\n",
peak_delta_prefix,
self.peak_usage_delta,
format_bytes(self.peak_usage_delta.unsigned_abs())
));
output.push_str(&format!(
"Total Allocations: {}{}\n\n",
alloc_delta_prefix, self.allocation_count_delta
));
if !self.new_components.is_empty() {
output.push_str("New Components:\n");
for component in &self.new_components {
output.push_str(&format!(" + {component}\n"));
}
output.push('\n');
}
if !self.removed_components.is_empty() {
output.push_str("Removed Components:\n");
for component in &self.removed_components {
output.push_str(&format!(" - {component}\n"));
}
output.push('\n');
}
if !self.component_changes.is_empty() {
output.push_str("Component Changes:\n");
let mut components: Vec<_> = self.component_changes.iter().collect();
components.sort_by_key(|b| std::cmp::Reverse(b.1.current_usage_delta));
for (component, diff) in components {
let current_prefix = if diff.current_usage_delta >= 0 {
"+"
} else {
""
};
let peak_prefix = if diff.peak_usage_delta >= 0 { "+" } else { "" };
let alloc_prefix = if diff.allocation_count_delta >= 0 {
"+"
} else {
""
};
let total_prefix = if diff.total_allocated_delta >= 0 {
"+"
} else {
""
};
output.push_str(&format!(" {component}"));
if diff.potential_leak {
output.push_str(" [POTENTIAL LEAK]");
}
output.push('\n');
output.push_str(&format!(
" Current: {}{}B ({})\n",
current_prefix,
diff.current_usage_delta,
format_bytes(diff.current_usage_delta.unsigned_abs())
));
output.push_str(&format!(
" Peak: {}{}B ({})\n",
peak_prefix,
diff.peak_usage_delta,
format_bytes(diff.peak_usage_delta.unsigned_abs())
));
output.push_str(&format!(
" Allocations: {}{}\n",
alloc_prefix, diff.allocation_count_delta
));
output.push_str(&format!(
" Total Allocated: {}{}B ({})\n",
total_prefix,
diff.total_allocated_delta,
format_bytes(diff.total_allocated_delta.unsigned_abs())
));
}
}
output
}
#[cfg(feature = "memory_metrics")]
pub fn to_json(&self) -> JsonValue {
serde_json::to_value(self).unwrap_or(JsonValue::Null)
}
#[cfg(not(feature = "memory_metrics"))]
pub fn to_json(&self) -> String {
"{}".to_string()
}
pub fn has_potential_leaks(&self) -> bool {
self.component_changes
.values()
.any(|diff| diff.potential_leak)
}
pub fn get_potential_leak_components(&self) -> Vec<String> {
self.component_changes
.iter()
.filter_map(|(component, diff)| {
if diff.potential_leak {
Some(component.clone())
} else {
None
}
})
.collect()
}
#[cfg(feature = "memory_visualization")]
pub fn visualize(&self) -> String {
use crate::memory::metrics::format_bytes;
let mut visualization = String::new();
visualization.push_str(&format!(
"Memory Snapshot Visualization: {} → {}\n\n",
self.first_id, self.second_id
));
visualization.push_str(
"Component | Current Usage | Peak Usage | Allocations\n",
);
visualization.push_str(
"-------------------------|----------------|----------------|----------------\n",
);
let mut components: Vec<_> = self.component_changes.iter().collect();
components.sort_by_key(|item| std::cmp::Reverse(item.1.current_usage_delta));
for (component, diff) in components {
let component_name = if component.len() > 25 {
format!("{}...", &component[0..22])
} else {
component.clone()
};
let current_prefix = if diff.current_usage_delta > 0 {
"+"
} else {
""
};
let peak_prefix = if diff.peak_usage_delta > 0 { "+" } else { "" };
let alloc_prefix = if diff.allocation_count_delta > 0 {
"+"
} else {
""
};
let leak_flag = if diff.potential_leak { "⚠️ " } else { "" };
visualization.push_str(&format!(
"{}{:<23} | {:>14} | {:>14} | {:>14}\n",
leak_flag,
component_name,
format!(
"{}{}",
current_prefix,
format_bytes(diff.current_usage_delta.unsigned_abs())
),
format!(
"{}{}",
peak_prefix,
format_bytes(diff.peak_usage_delta.unsigned_abs())
),
format!(
"{}{}",
alloc_prefix,
diff.allocation_count_delta.unsigned_abs()
)
));
}
visualization.push_str("\nLegend:\n");
visualization.push_str("⚠️ = Potential memory leak\n");
visualization
}
}
pub struct SnapshotManager {
snapshots: Vec<MemorySnapshot>,
}
impl SnapshotManager {
pub fn new() -> Self {
Self {
snapshots: Vec::new(),
}
}
pub fn take_snapshot(
&mut self,
id: impl Into<String>,
description: impl Into<String>,
) -> MemorySnapshot {
let snapshot = MemorySnapshot::new(id, description);
self.snapshots.push(snapshot.clone());
snapshot
}
pub fn get_snapshot(&self, id: &str) -> Option<&MemorySnapshot> {
self.snapshots.iter().find(|s| s.id == id)
}
pub fn compare_snapshots(&self, first_id: &str, secondid: &str) -> Option<SnapshotDiff> {
let first = self.get_snapshot(first_id)?;
let second = self.get_snapshot(secondid)?;
Some(first.compare(second))
}
pub fn save_all(&self, dir: impl AsRef<Path>) -> io::Result<()> {
let dir = dir.as_ref();
if !dir.exists() {
std::fs::create_dir_all(dir)?;
}
for snapshot in &self.snapshots {
let filename = format!("snapshot_{id}.json", id = snapshot.id);
let path = dir.join(filename);
snapshot.save_to_file(path)?;
}
Ok(())
}
pub fn load_all(&mut self, dir: impl AsRef<Path>) -> io::Result<()> {
let dir = dir.as_ref();
if !dir.exists() {
return Ok(());
}
for entry in std::fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if path.extension().and_then(|s| s.to_str()) == Some("json") {
if let Ok(snapshot) = MemorySnapshot::load_from_file(&path) {
self.snapshots.push(snapshot);
}
}
}
Ok(())
}
pub fn get_snapshots(&self) -> &[MemorySnapshot] {
&self.snapshots
}
pub fn clear(&mut self) {
self.snapshots.clear();
}
}
impl Default for SnapshotManager {
fn default() -> Self {
Self::new()
}
}
static GLOBAL_SNAPSHOT_MANAGER: once_cell::sync::Lazy<std::sync::Mutex<SnapshotManager>> =
once_cell::sync::Lazy::new(|| std::sync::Mutex::new(SnapshotManager::new()));
#[allow(dead_code)]
pub fn global_snapshot_manager() -> &'static std::sync::Mutex<SnapshotManager> {
&GLOBAL_SNAPSHOT_MANAGER
}
#[allow(dead_code)]
pub fn take_snapshot(id: impl Into<String>, description: impl Into<String>) -> MemorySnapshot {
let mut manager = match global_snapshot_manager().lock() {
Ok(guard) => guard,
Err(poisoned) => {
poisoned.into_inner()
}
};
let snapshot = manager.take_snapshot(id, description);
snapshot.clone()
}
#[allow(dead_code)]
pub fn compare_snapshots(first_id: &str, secondid: &str) -> Option<SnapshotDiff> {
let manager = match global_snapshot_manager().lock() {
Ok(guard) => guard,
Err(poisoned) => {
poisoned.into_inner()
}
};
manager.compare_snapshots(first_id, secondid)
}
#[allow(dead_code)]
pub fn save_all_snapshots(dir: impl AsRef<Path>) -> io::Result<()> {
let manager = match global_snapshot_manager().lock() {
Ok(guard) => guard,
Err(poisoned) => {
poisoned.into_inner()
}
};
manager.save_all(dir)
}
#[allow(dead_code)]
pub fn load_all_snapshots(dir: impl AsRef<Path>) -> io::Result<()> {
let mut manager = match global_snapshot_manager().lock() {
Ok(guard) => guard,
Err(poisoned) => {
poisoned.into_inner()
}
};
manager.load_all(dir)
}
#[allow(dead_code)]
pub fn clear_snapshots() {
let mut manager = match global_snapshot_manager().lock() {
Ok(guard) => guard,
Err(poisoned) => {
poisoned.into_inner()
}
};
manager.clear();
}
#[cfg(test)]
mod tests {
use super::*;
use crate::memory::metrics::{
generate_memory_report, reset_memory_metrics, track_allocation, track_deallocation,
};
use crate::memory::metrics::test_utils::MEMORY_METRICS_TEST_MUTEX as TEST_MUTEX;
#[test]
fn test_snapshot_creation() {
let lock = TEST_MUTEX
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
println!("test_snapshot_creation started");
reset_memory_metrics();
clear_snapshots();
let initial_report = generate_memory_report();
println!(
"Initial state: total_current_usage={}",
initial_report.total_current_usage
);
let snapshot1 = MemorySnapshot::new("start", "Initial state");
assert_eq!(
snapshot1.report.total_current_usage, 0,
"First snapshot should have 0 memory usage but had {} bytes",
snapshot1.report.total_current_usage
);
reset_memory_metrics();
track_allocation("TestComponent", 1024, 0x1000);
track_allocation("TestComponent", 2048, 0x2000);
let allocations_report = generate_memory_report();
println!(
"After allocations: total_current_usage={}",
allocations_report.total_current_usage
);
let expected_usage = 1024 + 2048; assert_eq!(
allocations_report.total_current_usage, expected_usage,
"Memory tracking should show {} bytes but showed {} bytes",
expected_usage, allocations_report.total_current_usage
);
let snapshot2 = MemorySnapshot::new("allocated", "After allocations");
assert_eq!(
snapshot2.report.total_current_usage, expected_usage,
"Second snapshot should have {} bytes but had {} bytes",
expected_usage, snapshot2.report.total_current_usage
);
let diff = snapshot1.compare(&snapshot2);
assert_eq!(
diff.current_usage_delta, expected_usage as isize,
"Delta between snapshots should be {} bytes but was {} bytes",
expected_usage, diff.current_usage_delta
);
reset_memory_metrics();
let snapshotbase = MemorySnapshot::new("base", "Base for deallocation test");
assert_eq!(
snapshotbase.report.total_current_usage, 0,
"Baseline snapshot after reset should have 0 memory usage but had {} bytes",
snapshotbase.report.total_current_usage
);
track_allocation("TestComponent", 1024, 0x1000);
track_deallocation("TestComponent", 1024, 0x1000);
let snapshot_final = MemorySnapshot::new("final", "After deallocation");
assert_eq!(snapshot_final.report.total_current_usage, 0,
"Final snapshot after allocation and deallocation should have 0 memory usage but had {} bytes",
snapshot_final.report.total_current_usage);
reset_memory_metrics();
clear_snapshots();
println!("test_snapshot_creation completed");
}
#[test]
fn test_snapshot_manager() {
let lock = TEST_MUTEX
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
println!("test_snapshot_manager started");
reset_memory_metrics();
clear_snapshots();
let initial_report = generate_memory_report();
println!(
"Initial state: total_current_usage={}",
initial_report.total_current_usage
);
assert_eq!(
initial_report.total_current_usage, 0,
"Initial memory usage should be 0 but was {} bytes",
initial_report.total_current_usage
);
let mut manager = SnapshotManager::new();
let snapshot1 = manager.take_snapshot("s1", "First snapshot");
assert_eq!(snapshot1.id, "s1", "First snapshot should have ID 's1'");
println!(
"First snapshot total_current_usage: {}",
snapshot1.report.total_current_usage
);
assert_eq!(
snapshot1.report.total_current_usage, 0,
"First snapshot should have 0 memory usage but had {} bytes",
snapshot1.report.total_current_usage
);
reset_memory_metrics();
track_allocation("TestComponent", 1024, 0x1000);
let after_alloc_report = generate_memory_report();
println!(
"After allocation: total_current_usage={}",
after_alloc_report.total_current_usage
);
assert_eq!(
after_alloc_report.total_current_usage, 1024,
"Expected 1024 bytes allocated but found {} bytes",
after_alloc_report.total_current_usage
);
let snapshot2 = MemorySnapshot::new("s2", "Second snapshot");
manager.snapshots.push(snapshot2.clone());
println!(
"Second snapshot total_current_usage: {}",
snapshot2.report.total_current_usage
);
assert_eq!(
snapshot2.report.total_current_usage, 1024,
"Second snapshot should have 1024 bytes but had {} bytes",
snapshot2.report.total_current_usage
);
let diff = snapshot1.compare(&snapshot2);
assert_eq!(
diff.current_usage_delta, 1024,
"Delta between snapshots should be 1024 bytes but was {} bytes",
diff.current_usage_delta
);
let retrieved = manager.get_snapshot("s1");
assert!(retrieved.is_some(), "Snapshot 's1' should exist");
assert_eq!(
retrieved.expect("Operation failed").id,
"s1",
"Retrieved snapshot should have ID 's1'"
);
manager.clear();
assert!(
manager.get_snapshot("s1").is_none(),
"After clearing, snapshot 's1' should not exist"
);
reset_memory_metrics();
clear_snapshots();
println!("test_snapshot_manager completed");
}
#[test]
fn test_global_snapshot_manager() {
let lock = match TEST_MUTEX.lock() {
Ok(guard) => guard,
Err(poisoned) => {
poisoned.into_inner()
}
};
println!("test_global_snapshot_manager started");
reset_memory_metrics();
clear_snapshots();
let initial_report = generate_memory_report();
println!(
"Initial state: total_current_usage={}",
initial_report.total_current_usage
);
let snapshot1 = take_snapshot("g1", "Global snapshot 1");
assert_eq!(snapshot1.id, "g1", "First snapshot should have ID 'g1'");
println!(
"First snapshot total_current_usage: {}",
snapshot1.report.total_current_usage
);
assert_eq!(
snapshot1.report.total_current_usage, 0,
"First snapshot should have 0 memory usage but had {} bytes",
snapshot1.report.total_current_usage
);
reset_memory_metrics();
clear_snapshots();
let clean_snapshot = MemorySnapshot::new("clean", "Baseline for comparison");
assert_eq!(clean_snapshot.report.total_current_usage, 0);
track_allocation("TestComponent", 1024, 0x1000);
let after_alloc_report = generate_memory_report();
println!(
"After allocation: total_current_usage={}",
after_alloc_report.total_current_usage
);
assert_eq!(
after_alloc_report.total_current_usage, 1024,
"Expected 1024 bytes allocated but found {} bytes",
after_alloc_report.total_current_usage
);
let snapshot2 = MemorySnapshot::new("g2", "Global snapshot 2");
assert_eq!(snapshot2.id, "g2", "Second snapshot should have ID 'g2'");
println!(
"Second snapshot total_current_usage: {}",
snapshot2.report.total_current_usage
);
assert_eq!(
snapshot2.report.total_current_usage, 1024,
"Second snapshot should have 1024 bytes but had {} bytes",
snapshot2.report.total_current_usage
);
let diff = clean_snapshot.compare(&snapshot2);
assert_eq!(
diff.current_usage_delta, 1024,
"Delta between snapshots should be 1024 bytes but was {} bytes",
diff.current_usage_delta
);
clear_snapshots();
reset_memory_metrics();
println!("test_global_snapshot_manager completed");
}
#[test]
fn test_thread_safety() {
use crate::memory::metrics::{
MemoryEvent, MemoryEventType, MemoryMetricsCollector, MemoryMetricsConfig,
};
use std::sync::{Arc, Barrier};
use std::thread;
let barrier = Arc::new(Barrier::new(3));
let barrier1 = Arc::clone(&barrier);
let barrier2 = Arc::clone(&barrier);
let thread1 = thread::spawn(move || {
let collector = Arc::new(MemoryMetricsCollector::new(MemoryMetricsConfig::default()));
let initial_report = collector.generate_report();
let snapshot =
MemorySnapshot::from_report("thread1", "Initial snapshot", initial_report);
barrier1.wait();
collector.record_event(MemoryEvent::new(
MemoryEventType::Allocation,
"Thread1Component",
2048,
0x1000,
));
let after_report = collector.generate_report();
let snapshot2 =
MemorySnapshot::from_report("thread1_after", "After allocation", after_report);
let diff = snapshot.compare(&snapshot2);
assert_eq!(diff.current_usage_delta, 2048);
});
let thread2 = thread::spawn(move || {
let collector = Arc::new(MemoryMetricsCollector::new(MemoryMetricsConfig::default()));
let initial_report = collector.generate_report();
let snapshot =
MemorySnapshot::from_report("thread2", "Initial snapshot", initial_report);
barrier2.wait();
collector.record_event(MemoryEvent::new(
MemoryEventType::Allocation,
"Thread2Component",
4096,
0x2000,
));
let after_report = collector.generate_report();
let snapshot2 =
MemorySnapshot::from_report("thread2_after", "After allocation", after_report);
let diff = snapshot.compare(&snapshot2);
assert_eq!(diff.current_usage_delta, 4096);
});
barrier.wait();
thread1.join().expect("Operation failed");
thread2.join().expect("Operation failed");
}
#[test]
fn test_thread_safety_isolated() {
use crate::memory::metrics::{
MemoryEvent, MemoryEventType, MemoryMetricsCollector, MemoryMetricsConfig,
};
use std::sync::{Arc, Barrier};
use std::thread;
let barrier = Arc::new(Barrier::new(3));
let barrier1 = Arc::clone(&barrier);
let barrier2 = Arc::clone(&barrier);
let thread1 = thread::spawn(move || {
let collector = Arc::new(MemoryMetricsCollector::new(MemoryMetricsConfig::default()));
let initial_report = collector.generate_report();
let snapshot =
MemorySnapshot::from_report("thread1", "Initial snapshot", initial_report);
barrier1.wait();
collector.record_event(MemoryEvent::new(
MemoryEventType::Allocation,
"Thread1Component",
2048,
0x1000,
));
let after_report = collector.generate_report();
let snapshot2 =
MemorySnapshot::from_report("thread1_after", "After allocation", after_report);
let diff = snapshot.compare(&snapshot2);
assert_eq!(diff.current_usage_delta, 2048);
assert!(
diff.new_components
.contains(&"Thread1Component".to_string()),
"Thread1Component should be in new_components"
);
assert!(
diff.removed_components.is_empty(),
"No components should be removed"
);
});
let thread2 = thread::spawn(move || {
let collector = Arc::new(MemoryMetricsCollector::new(MemoryMetricsConfig::default()));
let initial_report = collector.generate_report();
let snapshot =
MemorySnapshot::from_report("thread2", "Initial snapshot", initial_report);
barrier2.wait();
collector.record_event(MemoryEvent::new(
MemoryEventType::Allocation,
"Thread2Component",
4096,
0x2000,
));
let after_report = collector.generate_report();
let snapshot2 =
MemorySnapshot::from_report("thread2_after", "After allocation", after_report);
let diff = snapshot.compare(&snapshot2);
assert_eq!(diff.current_usage_delta, 4096);
assert!(
diff.new_components
.contains(&"Thread2Component".to_string()),
"Thread2Component should be in new_components"
);
assert!(
diff.removed_components.is_empty(),
"No components should be removed"
);
});
barrier.wait();
thread1.join().expect("Operation failed");
thread2.join().expect("Operation failed");
println!("Thread safety test with isolated collectors completed successfully");
}
#[test]
fn test_leak_detection() {
let lock = TEST_MUTEX
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
println!("test_leak_detection started");
reset_memory_metrics();
clear_snapshots();
let initial_report = generate_memory_report();
println!(
"Initial state: total_current_usage={}",
initial_report.total_current_usage
);
let snapshot1 = MemorySnapshot::new("leak_test_1", "Before potential leak");
println!(
"First snapshot total_current_usage: {}",
snapshot1.report.total_current_usage
);
assert_eq!(
snapshot1.report.total_current_usage, 0,
"Initial snapshot should have 0 memory usage but had {} bytes",
snapshot1.report.total_current_usage
);
reset_memory_metrics();
track_allocation("LeakyComponent", 4096, 0x3000);
let after_alloc_report = generate_memory_report();
println!(
"After allocation: total_current_usage={}",
after_alloc_report.total_current_usage
);
assert_eq!(
after_alloc_report.total_current_usage, 4096,
"Expected 4096 bytes allocated but found {} bytes",
after_alloc_report.total_current_usage
);
let snapshot2 = MemorySnapshot::new("leak_test_2", "After operations");
println!(
"Second snapshot total_current_usage: {}",
snapshot2.report.total_current_usage
);
assert_eq!(
snapshot2.report.total_current_usage, 4096,
"Second snapshot should have 4096 bytes but had {} bytes",
snapshot2.report.total_current_usage
);
let diff = snapshot1.compare(&snapshot2);
assert_eq!(
diff.current_usage_delta, 4096,
"Delta between snapshots should be 4096 bytes but was {} bytes",
diff.current_usage_delta
);
assert!(
diff.current_usage_delta > 0,
"Memory increase should be detected as potential leak"
);
let mut component_diffs = HashMap::new();
component_diffs.insert(
"LeakyComponent".to_string(),
ComponentStatsDiff {
current_usage_delta: 4096,
peak_usage_delta: 4096,
allocation_count_delta: 1,
total_allocated_delta: 4096,
potential_leak: true,
},
);
let test_diff = SnapshotDiff {
first_id: "test1".to_string(),
second_id: "test2".to_string(),
elapsed_ms: 100,
current_usage_delta: 4096,
peak_usage_delta: 4096,
allocation_count_delta: 1,
new_components: vec!["LeakyComponent".to_string()],
removed_components: vec![],
component_changes: component_diffs,
};
assert!(
test_diff.has_potential_leaks(),
"Leak detection should identify potential leaks"
);
let leak_components = test_diff.get_potential_leak_components();
assert_eq!(
leak_components.len(),
1,
"Should find exactly one leaky component"
);
assert_eq!(
leak_components[0], "LeakyComponent",
"Leaky component should be 'LeakyComponent'"
);
reset_memory_metrics();
clear_snapshots();
println!("test_leak_detection completed");
}
}