use tracing::{debug, warn};
use crate::analysis::is_virtual_pointer;
use crate::analysis::memory_passport_tracker::MemoryPassportTracker;
use crate::analysis::node_id::NodeId;
use crate::analysis::ownership_graph::{EdgeKind, OwnershipGraph, OwnershipOp};
use crate::capture::platform::memory_info::PlatformMemoryInfo;
use crate::core::{MemScopeError, MemScopeResult};
use crate::render_engine::dashboard::{rebuild_allocations_from_events, DashboardRenderer};
use crate::snapshot::{ActiveAllocation, MemorySnapshot, ThreadMemoryStats};
use crate::tracker::Tracker;
use rayon::prelude::*;
use serde_json::json;
use std::{
collections::HashMap,
fs::File,
io::{BufWriter, Write},
path::Path,
sync::Arc,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum OptimizationLevel {
Low,
#[default]
Medium,
High,
Maximum,
}
#[derive(Debug, Clone, Default)]
pub struct SchemaValidator {
strict_mode: bool,
}
impl SchemaValidator {
pub fn new() -> Self {
Self { strict_mode: false }
}
pub fn with_strict_mode(mut self, strict: bool) -> Self {
self.strict_mode = strict;
self
}
pub fn validate(&self, data: &serde_json::Value) -> Result<(), String> {
if !data.is_object() {
return Err("Export data must be a JSON object".to_string());
}
let obj = data.as_object().ok_or("Invalid JSON object")?;
if self.strict_mode {
let required_fields = ["timestamp", "allocations", "stats"];
for field in &required_fields {
if !obj.contains_key(*field) {
return Err(format!("Missing required field: {}", field));
}
}
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct ExportJsonOptions {
pub parallel_processing: bool,
pub buffer_size: usize,
pub use_compact_format: Option<bool>,
pub enable_type_cache: bool,
pub batch_size: usize,
pub streaming_writer: bool,
pub schema_validation: bool,
pub adaptive_optimization: bool,
pub max_cache_size: usize,
pub security_analysis: bool,
pub include_low_severity: bool,
pub integrity_hashes: bool,
pub fast_export_mode: bool,
pub auto_fast_export_threshold: Option<usize>,
pub thread_count: Option<usize>,
}
impl Default for ExportJsonOptions {
fn default() -> Self {
Self {
parallel_processing: true,
buffer_size: 256 * 1024,
use_compact_format: None,
enable_type_cache: true,
batch_size: 1000,
streaming_writer: true,
schema_validation: false,
adaptive_optimization: true,
max_cache_size: 10_000,
security_analysis: false,
include_low_severity: false,
integrity_hashes: false,
fast_export_mode: false,
auto_fast_export_threshold: Some(10_000),
thread_count: None,
}
}
}
impl ExportJsonOptions {
pub fn fast_export_mode(mut self, enabled: bool) -> Self {
self.fast_export_mode = enabled;
self
}
pub fn security_analysis(mut self, enabled: bool) -> Self {
self.security_analysis = enabled;
self
}
pub fn streaming_writer(mut self, enabled: bool) -> Self {
self.streaming_writer = enabled;
self
}
pub fn schema_validation(mut self, enabled: bool) -> Self {
self.schema_validation = enabled;
self
}
pub fn integrity_hashes(mut self, enabled: bool) -> Self {
self.integrity_hashes = enabled;
self
}
pub fn batch_size(mut self, size: usize) -> Self {
self.batch_size = size;
self
}
pub fn adaptive_optimization(mut self, enabled: bool) -> Self {
self.adaptive_optimization = enabled;
self
}
pub fn max_cache_size(mut self, size: usize) -> Self {
self.max_cache_size = size;
self
}
pub fn include_low_severity(mut self, include: bool) -> Self {
self.include_low_severity = include;
self
}
pub fn thread_count(mut self, count: Option<usize>) -> Self {
self.thread_count = count;
self
}
}
pub fn export_snapshot_to_json(
snapshot: &MemorySnapshot,
output_path: &Path,
options: &ExportJsonOptions,
) -> Result<(), Box<dyn std::error::Error>> {
if let Some(parent) = output_path.parent() {
if !parent.as_os_str().is_empty() {
std::fs::create_dir_all(parent)?;
}
}
let allocations: Vec<&ActiveAllocation> = snapshot.active_allocations.values().collect();
let processed = process_allocations(&allocations, options)?;
let output_dir = if output_path.extension().is_some() {
output_path.parent().unwrap_or(Path::new("."))
} else {
output_path
};
generate_memory_analysis_json(output_dir, &processed, options)?;
generate_lifetime_json(output_dir, &processed, options)?;
generate_thread_analysis_json(output_dir, &snapshot.thread_stats, options)?;
Ok(())
}
fn process_allocations(
allocations: &[&ActiveAllocation],
options: &ExportJsonOptions,
) -> Result<Vec<serde_json::Value>, Box<dyn std::error::Error>> {
if options.parallel_processing && allocations.len() > options.batch_size {
let chunk_size = (allocations.len() / num_cpus::get()).max(1);
Ok(allocations
.par_chunks(chunk_size)
.flat_map(process_allocation_batch)
.collect())
} else {
Ok(process_allocation_batch(allocations))
}
}
fn process_allocation_batch(allocations: &[&ActiveAllocation]) -> Vec<serde_json::Value> {
let current_time = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos() as u64)
.unwrap_or(0);
allocations
.iter()
.map(|alloc| {
let type_info = get_or_compute_type_info(
alloc.type_name.as_deref().unwrap_or("unknown"),
alloc.size,
);
let lifetime_ms = if alloc.allocated_at > 0 {
(current_time.saturating_sub(alloc.allocated_at)) / 1_000_000
} else {
0
};
let address = match alloc.ptr {
Some(ptr) => format!("0x{:x}", ptr),
None => "N/A".to_string(),
};
let mut entry = json!({
"address": address,
"size": alloc.size,
"type": type_info,
"timestamp": alloc.allocated_at,
"thread_id": alloc.thread_id,
"lifetime_ms": lifetime_ms,
});
if let Some(ref var_name) = alloc.var_name {
entry["var_name"] = serde_json::json!(var_name);
}
if let Some(ref type_name) = alloc.type_name {
entry["type_name"] = serde_json::json!(type_name);
}
if let Some(ref module_path) = alloc.module_path {
if !is_library_module_path(module_path) {
entry["module_path"] = serde_json::json!(module_path);
}
}
entry
})
.collect()
}
fn is_library_module_path(module_path: &str) -> bool {
if module_path.starts_with("std::")
|| module_path.starts_with("core::")
|| module_path.starts_with("alloc::")
{
return true;
}
if module_path.starts_with("memscope_rs::") {
return true;
}
let library_prefixes = [
"tokio::",
"serde::",
"async_trait::",
"futures::",
"log::",
"tracing::",
"chrono::",
"indexmap::",
"rustc_hash::",
"parking_lot::",
"crossbeam::",
"rayon::",
"dashmap::",
"ahash::",
"hashbrown::",
];
for prefix in library_prefixes.iter() {
if module_path.starts_with(prefix) {
return true;
}
}
false
}
fn get_or_compute_type_info(type_name: &str, size: usize) -> String {
if (type_name.contains("Vec<") || type_name.contains("vec::Vec<"))
&& !type_name.contains("VecDeque")
{
"dynamic_array".to_string()
} else if type_name == "str"
|| type_name == "String"
|| type_name.contains("&str")
|| type_name.contains("alloc::string::String")
{
"string".to_string()
} else if type_name.contains("Box") || type_name.contains("Rc") || type_name.contains("Arc") {
"smart_pointer".to_string()
} else if type_name.contains("[") && type_name.contains("u8") {
"byte_array".to_string()
} else if size > 1024 * 1024 {
"large_buffer".to_string()
} else {
"custom".to_string()
}
}
fn generate_memory_analysis_json<P: AsRef<Path>>(
output_path: P,
allocations: &[serde_json::Value],
options: &ExportJsonOptions,
) -> Result<(), Box<dyn std::error::Error>> {
let total_size: usize = allocations
.iter()
.filter_map(|a| a.get("size").and_then(|s| s.as_u64()))
.map(|s| s as usize)
.sum();
let type_distribution: HashMap<String, usize> = {
let mut dist = HashMap::new();
for alloc in allocations {
if let Some(t) = alloc.get("type").and_then(|t| t.as_str()) {
*dist.entry(t.to_string()).or_insert(0) += 1;
}
}
dist
};
let data = json!({
"metadata": {
"export_version": "2.0",
"export_timestamp": chrono::Utc::now().to_rfc3339(),
"specification": "memscope-rs memory analysis",
"total_allocations": allocations.len(),
"total_size_bytes": total_size
},
"allocations": allocations,
"statistics": {
"total_allocations": allocations.len(),
"total_size_bytes": total_size,
"average_size_bytes": if allocations.is_empty() { 0 } else { total_size / allocations.len() }
},
"type_distribution": type_distribution
});
let path = output_path.as_ref().join("memory_analysis.json");
write_json_optimized(path, &data, options)?;
Ok(())
}
fn generate_lifetime_json<P: AsRef<Path>>(
output_path: P,
allocations: &[serde_json::Value],
options: &ExportJsonOptions,
) -> Result<(), Box<dyn std::error::Error>> {
let ownership_histories: Vec<serde_json::Value> = allocations
.iter()
.map(|alloc| {
json!({
"address": alloc.get("address"),
"var_name": alloc.get("var_name"),
"type_name": alloc.get("type_name"),
"size": alloc.get("size"),
"timestamp_alloc": alloc.get("timestamp"),
"timestamp_dealloc": null,
"lifetime_ms": alloc.get("lifetime_ms"),
"events": [
{
"event_type": "Created",
"timestamp": alloc.get("timestamp"),
"context": "initial_allocation"
}
]
})
})
.collect();
let lifetime_data = json!({
"metadata": {
"export_version": "2.0",
"export_timestamp": chrono::Utc::now().to_rfc3339(),
"specification": "memscope-rs lifetime tracking",
"total_tracked_allocations": ownership_histories.len()
},
"ownership_histories": ownership_histories
});
let lifetime_path = output_path.as_ref().join("lifetime.json");
write_json_optimized(lifetime_path, &lifetime_data, options)?;
Ok(())
}
fn generate_thread_analysis_json<P: AsRef<Path>>(
output_path: P,
thread_stats: &HashMap<u64, ThreadMemoryStats>,
options: &ExportJsonOptions,
) -> Result<(), Box<dyn std::error::Error>> {
let thread_analysis: Vec<serde_json::Value> = thread_stats
.values()
.map(|stats| {
json!({
"thread_id": stats.thread_id,
"allocation_count": stats.allocation_count,
"total_allocated": stats.total_allocated,
"current_memory": stats.current_memory,
"peak_memory": stats.peak_memory,
})
})
.collect();
let data = json!({
"metadata": {
"export_version": "2.0",
"export_timestamp": chrono::Utc::now().to_rfc3339(),
"specification": "thread analysis",
"total_threads": thread_analysis.len()
},
"thread_analysis": thread_analysis
});
let path = output_path.as_ref().join("thread_analysis.json");
write_json_optimized(path, &data, options)?;
Ok(())
}
fn write_json_optimized<P: AsRef<Path>>(
path: P,
data: &serde_json::Value,
options: &ExportJsonOptions,
) -> Result<(), Box<dyn std::error::Error>> {
let path = path.as_ref();
let estimated_size = estimate_json_size(data);
let use_compact = options
.use_compact_format
.unwrap_or(estimated_size > 1_000_000);
if options.streaming_writer && estimated_size > 500_000 {
let file = File::create(path)?;
let mut writer = BufWriter::with_capacity(options.buffer_size, file);
if use_compact {
serde_json::to_writer(&mut writer, data)?;
} else {
serde_json::to_writer_pretty(&mut writer, data)?;
}
writer.flush()?;
} else {
let json_string = if use_compact {
serde_json::to_string(data)?
} else {
serde_json::to_string_pretty(data)?
};
std::fs::write(path, json_string)?;
}
Ok(())
}
fn estimate_json_size(data: &serde_json::Value) -> usize {
match data {
serde_json::Value::Object(map) => {
map.values().map(estimate_json_size).sum::<usize>() + map.len() * 20
}
serde_json::Value::Array(arr) => {
arr.iter().map(estimate_json_size).sum::<usize>() + arr.len() * 10
}
serde_json::Value::String(s) => s.len(),
serde_json::Value::Number(n) => n.to_string().len(),
_ => 10,
}
}
#[derive(Debug, thiserror::Error)]
pub enum ExportError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("JSON error: {0}")]
Json(#[from] serde_json::Error),
#[error("Export failed: {0}")]
ExportFailed(String),
}
pub fn export_all_json<P: AsRef<Path>>(
path: P,
tracker: &Tracker,
passport_tracker: &Arc<MemoryPassportTracker>,
async_tracker: &Arc<crate::capture::backends::async_tracker::AsyncTracker>,
) -> MemScopeResult<()> {
let path_ref = path.as_ref();
let events = tracker.event_store().snapshot();
let allocations = rebuild_allocations_from_events(&events);
let snapshot = MemorySnapshot::from_allocation_infos(allocations.clone());
let options = ExportJsonOptions::default();
std::fs::create_dir_all(path_ref)
.map_err(|e| MemScopeError::error("export", "export_all_json", e.to_string()))?;
debug!("Starting export_snapshot_to_json");
export_snapshot_to_json(&snapshot, path_ref, &options)
.map_err(|e| MemScopeError::error("export", "export_all_json", e.to_string()))?;
debug!("Completed export_snapshot_to_json");
debug!("Starting export_memory_passports_json");
export_memory_passports_json(path_ref, passport_tracker)
.map_err(|e| MemScopeError::error("export", "export_all_json", e.to_string()))?;
debug!("Completed export_memory_passports_json");
debug!("Starting export_leak_detection_json");
export_leak_detection_json(path_ref, passport_tracker)
.map_err(|e| MemScopeError::error("export", "export_all_json", e.to_string()))?;
debug!("Completed export_leak_detection_json");
debug!("Starting export_unsafe_ffi_json");
export_unsafe_ffi_json(path_ref, passport_tracker)
.map_err(|e| MemScopeError::error("export", "export_all_json", e.to_string()))?;
debug!("Completed export_unsafe_ffi_json");
debug!("Starting export_system_resources_json");
export_system_resources_json(path_ref)
.map_err(|e| MemScopeError::error("export", "export_all_json", e.to_string()))?;
debug!("Completed export_system_resources_json");
debug!("Starting export_async_analysis_json");
export_async_analysis_json(path_ref, async_tracker)
.map_err(|e| MemScopeError::error("export", "export_all_json", e.to_string()))?;
debug!("Completed export_async_analysis_json");
debug!("Starting export_ownership_graph_json");
let typed_allocations: Vec<crate::capture::types::AllocationInfo> =
allocations.clone().into_iter().collect();
debug!(
allocations = typed_allocations.len(),
"Converted allocations to typed format"
);
export_ownership_graph_json(path_ref, &typed_allocations, tracker.event_store())
.map_err(|e| MemScopeError::error("export", "export_all_json", e.to_string()))?;
debug!("Completed export_ownership_graph_json");
export_task_graph_json(path_ref)
.map_err(|e| MemScopeError::error("export", "export_all_json", e.to_string()))?;
debug!("Completed export_task_graph_json");
debug!("All exports completed successfully");
Ok(())
}
pub fn export_task_graph_json<P: AsRef<Path>>(base_path: P) -> MemScopeResult<()> {
use crate::task_registry::global_registry;
let base_path = base_path.as_ref();
let registry = global_registry();
let graph = registry.export_graph();
let json_string = serde_json::to_string_pretty(&graph)
.map_err(|e| MemScopeError::error("export", "export_task_graph_json", e.to_string()))?;
let file_path = base_path.join("task_graph.json");
std::fs::write(&file_path, json_string)
.map_err(|e| MemScopeError::error("export", "export_task_graph_json", e.to_string()))?;
tracing::info!("✅ Task graph JSON exported to: {:?}", file_path);
Ok(())
}
pub fn export_async_analysis_json<P: AsRef<Path>>(
path: P,
async_tracker: &Arc<crate::capture::backends::async_tracker::AsyncTracker>,
) -> MemScopeResult<()> {
let path_ref = path.as_ref();
let stats = async_tracker.get_stats();
let profiles = async_tracker.get_all_profiles();
let snapshot = async_tracker.snapshot();
let async_data = json!({
"summary": {
"total_tasks": stats.total_tasks,
"active_tasks": stats.active_tasks,
"total_allocations": stats.total_allocations,
"total_memory_bytes": stats.total_memory,
"active_memory_bytes": stats.active_memory,
"peak_memory_bytes": stats.peak_memory,
},
"task_profiles": profiles.iter().map(|p| json!({
"task_id": p.task_id,
"task_name": p.task_name,
"task_type": format!("{:?}", p.task_type),
"created_at_ms": p.created_at_ms,
"completed_at_ms": p.completed_at_ms,
"total_bytes": p.total_bytes,
"current_memory": p.current_memory,
"peak_memory": p.peak_memory,
"total_allocations": p.total_allocations,
"total_deallocations": p.total_deallocations,
"duration_ns": p.duration_ns,
"allocation_rate": p.allocation_rate,
"efficiency_score": p.efficiency_score,
"average_allocation_size": p.average_allocation_size,
"is_completed": p.is_completed(),
"has_potential_leak": p.has_potential_leak(),
})).collect::<Vec<_>>(),
"allocations": snapshot.allocations.iter().map(|a| json!({
"ptr": format!("0x{:x}", a.ptr),
"size": a.size,
"timestamp": a.timestamp,
"task_id": a.task_id,
"var_name": a.var_name,
"type_name": a.type_name,
})).collect::<Vec<_>>(),
});
let async_path = path_ref.join("async_analysis.json");
let file = File::create(async_path)
.map_err(|e| MemScopeError::error("export", "export_async_analysis_json", e.to_string()))?;
let mut writer = BufWriter::new(file);
serde_json::to_writer_pretty(&mut writer, &async_data)
.map_err(|e| MemScopeError::error("export", "export_async_analysis_json", e.to_string()))?;
writer
.flush()
.map_err(|e| MemScopeError::error("export", "export_async_analysis_json", e.to_string()))?;
Ok(())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum DashboardTemplate {
#[default]
Unified,
Final,
}
impl std::fmt::Display for DashboardTemplate {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DashboardTemplate::Unified => write!(f, "dashboard_unified"),
DashboardTemplate::Final => write!(f, "dashboard_final"),
}
}
}
pub fn export_dashboard_html<P: AsRef<Path>>(
path: P,
tracker: &Tracker,
passport_tracker: &Arc<MemoryPassportTracker>,
) -> MemScopeResult<()> {
export_dashboard_html_with_template(
path,
tracker,
passport_tracker,
DashboardTemplate::default(),
None,
)
}
pub fn export_dashboard_html_with_async<P: AsRef<Path>>(
path: P,
tracker: &Tracker,
passport_tracker: &Arc<MemoryPassportTracker>,
async_tracker: &Arc<crate::capture::backends::async_tracker::AsyncTracker>,
) -> MemScopeResult<()> {
export_dashboard_html_with_template(
path,
tracker,
passport_tracker,
DashboardTemplate::default(),
Some(async_tracker),
)
}
pub fn export_dashboard_html_with_template<P: AsRef<Path>>(
path: P,
tracker: &Tracker,
passport_tracker: &Arc<MemoryPassportTracker>,
template: DashboardTemplate,
async_tracker: Option<&Arc<crate::capture::backends::async_tracker::AsyncTracker>>,
) -> MemScopeResult<()> {
let path_ref = path.as_ref();
std::fs::create_dir_all(path_ref).map_err(|e| {
MemScopeError::error(
"export",
"export_dashboard_html_with_template",
format!("Failed to create output directory: {}", e),
)
})?;
let renderer = DashboardRenderer::new().map_err(|e| {
MemScopeError::error(
"export",
"export_dashboard_html_with_template",
format!("Failed to create dashboard renderer: {}", e),
)
})?;
let context = renderer
.build_context_from_tracker_with_async(tracker, passport_tracker, async_tracker)
.map_err(|e| {
MemScopeError::error(
"export",
"export_dashboard_html_with_template",
format!("Failed to build context: {}", e),
)
})?;
let html_content = match template {
DashboardTemplate::Final => renderer.render_final_dashboard(&context).map_err(|e| {
MemScopeError::error(
"export",
"export_dashboard_html_with_template",
format!("Failed to render final dashboard: {}", e),
)
})?,
DashboardTemplate::Unified => renderer.render_unified_dashboard(&context).map_err(|e| {
MemScopeError::error(
"export",
"export_dashboard_html_with_template",
format!("Failed to render dashboard: {}", e),
)
})?,
};
let output_file = path_ref.join(format!("{}_dashboard.html", template));
std::fs::write(&output_file, html_content).map_err(|e| {
MemScopeError::error(
"export",
"export_dashboard_html_with_template",
format!("Failed to write HTML file: {}", e),
)
})?;
tracing::info!("✅ Dashboard HTML exported to: {:?}", output_file);
Ok(())
}
pub fn export_memory_passports_json<P: AsRef<Path>>(
base_path: P,
passport_tracker: &Arc<MemoryPassportTracker>,
) -> MemScopeResult<()> {
let base_path = base_path.as_ref();
let passports = passport_tracker.get_all_passports();
let passport_data: Vec<_> = passports
.values()
.map(|p| {
serde_json::json!({
"passport_id": p.passport_id,
"allocation_ptr": format!("0x{:x}", p.allocation_ptr),
"size_bytes": p.size_bytes,
"created_at": p.created_at,
"lifecycle_events": p.lifecycle_events.len(),
"status": format!("{:?}", p.status_at_shutdown),
})
})
.collect();
let json_data = serde_json::json!({
"metadata": {
"export_version": "2.0",
"specification": "memory passport tracking",
"total_passports": passports.len()
},
"memory_passports": passport_data,
});
let file_path = base_path.join("memory_passports.json");
let json_string = serde_json::to_string_pretty(&json_data).map_err(|e| {
MemScopeError::error("export", "export_memory_passports_json", e.to_string())
})?;
std::fs::write(&file_path, json_string).map_err(|e| {
MemScopeError::error("export", "export_memory_passports_json", e.to_string())
})?;
Ok(())
}
pub fn export_leak_detection_json<P: AsRef<Path>>(
base_path: P,
passport_tracker: &Arc<MemoryPassportTracker>,
) -> MemScopeResult<()> {
let base_path = base_path.as_ref();
let leak_result = passport_tracker.detect_leaks_at_shutdown();
let leak_details: Vec<_> = leak_result
.leak_details
.iter()
.map(|detail| {
serde_json::json!({
"passport_id": detail.passport_id,
"memory_address": format!("0x{:x}", detail.memory_address),
"size_bytes": detail.size_bytes,
"lifecycle_summary": detail.lifecycle_summary,
})
})
.collect();
let json_data = serde_json::json!({
"metadata": {
"export_version": "2.0",
"specification": "leak detection",
"leaks_detected": leak_result.total_leaks
},
"leak_detection": {
"total_leaks": leak_result.total_leaks,
"leak_details": leak_details
}
});
let file_path = base_path.join("leak_detection.json");
let json_string = serde_json::to_string_pretty(&json_data)
.map_err(|e| MemScopeError::error("export", "export_leak_detection_json", e.to_string()))?;
std::fs::write(&file_path, json_string)
.map_err(|e| MemScopeError::error("export", "export_leak_detection_json", e.to_string()))?;
Ok(())
}
pub fn export_unsafe_ffi_json<P: AsRef<Path>>(
base_path: P,
passport_tracker: &Arc<MemoryPassportTracker>,
) -> MemScopeResult<()> {
use crate::analysis::memory_passport_tracker::PassportStatus;
let base_path = base_path.as_ref();
let passports = passport_tracker.get_all_passports();
let ffi_reports: Vec<_> = passports
.values()
.filter(|p| {
matches!(
p.status_at_shutdown,
PassportStatus::HandoverToFfi
| PassportStatus::InForeignCustody
| PassportStatus::FreedByForeign
)
})
.map(|p| {
serde_json::json!({
"passport_id": p.passport_id,
"allocation_ptr": format!("0x{:x}", p.allocation_ptr),
"size_bytes": p.size_bytes,
"status": format!("{:?}", p.status_at_shutdown),
"created_at": p.created_at,
"boundary_events": p.lifecycle_events.iter().map(|e| {
serde_json::json!({
"timestamp": e.timestamp,
"event_type": format!("{:?}", e.event_type),
"context": e.context,
})
}).collect::<Vec<_>>(),
})
})
.collect();
let json_data = serde_json::json!({
"metadata": {
"export_version": "2.0",
"specification": "unsafe FFI tracking",
"total_ffi_reports": ffi_reports.len(),
"total_memory_passports": passports.len()
},
"unsafe_reports": ffi_reports,
"memory_passports": passports.len()
});
let file_path = base_path.join("unsafe_ffi.json");
let json_string = serde_json::to_string_pretty(&json_data)
.map_err(|e| MemScopeError::error("export", "export_unsafe_ffi_json", e.to_string()))?;
std::fs::write(&file_path, json_string)
.map_err(|e| MemScopeError::error("export", "export_unsafe_ffi_json", e.to_string()))?;
Ok(())
}
pub fn export_system_resources_json<P: AsRef<Path>>(base_path: P) -> MemScopeResult<()> {
let base_path = base_path.as_ref();
let mut memory_info = PlatformMemoryInfo::new();
let _ = memory_info.initialize();
let memory_stats = match memory_info.collect_stats() {
Ok(stats) => stats,
Err(e) => {
warn!(error = %e, "Failed to collect memory stats");
return Err(MemScopeError::error(
"export",
"export_system_resources_json",
e.to_string(),
));
}
};
let system_info = match memory_info.get_system_info() {
Ok(info) => info,
Err(e) => {
warn!(error = %e, "Failed to collect system info");
return Err(MemScopeError::error(
"export",
"export_system_resources_json",
e.to_string(),
));
}
};
let json_data = serde_json::json!({
"metadata": {
"export_version": "2.0",
"specification": "system resource monitoring",
"timestamp": std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
},
"system_info": {
"os_name": system_info.os_name,
"os_version": system_info.os_version,
"architecture": system_info.architecture,
"cpu_cores": system_info.cpu_cores,
"page_size": system_info.page_size,
"large_page_size": system_info.large_page_size,
"cpu_cache": {
"l1_cache_size": system_info.cpu_cache.l1_cache_size,
"l2_cache_size": system_info.cpu_cache.l2_cache_size,
"l3_cache_size": system_info.cpu_cache.l3_cache_size,
"cache_line_size": system_info.cpu_cache.cache_line_size
},
"mmu_info": {
"virtual_address_bits": system_info.mmu_info.virtual_address_bits,
"physical_address_bits": system_info.mmu_info.physical_address_bits,
"aslr_enabled": system_info.mmu_info.aslr_enabled,
"nx_bit_supported": system_info.mmu_info.nx_bit_supported
}
},
"memory_stats": {
"virtual_memory": {
"total_virtual": memory_stats.virtual_memory.total_virtual,
"available_virtual": memory_stats.virtual_memory.available_virtual,
"used_virtual": memory_stats.virtual_memory.used_virtual,
"reserved": memory_stats.virtual_memory.reserved,
"committed": memory_stats.virtual_memory.committed
},
"physical_memory": {
"total_physical": memory_stats.physical_memory.total_physical,
"available_physical": memory_stats.physical_memory.available_physical,
"used_physical": memory_stats.physical_memory.used_physical,
"cached": memory_stats.physical_memory.cached,
"buffers": memory_stats.physical_memory.buffers,
"swap": {
"total_swap": memory_stats.physical_memory.swap.total_swap,
"used_swap": memory_stats.physical_memory.swap.used_swap,
"available_swap": memory_stats.physical_memory.swap.available_swap,
"swap_in_rate": memory_stats.physical_memory.swap.swap_in_rate,
"swap_out_rate": memory_stats.physical_memory.swap.swap_out_rate
}
},
"process_memory": {
"virtual_size": memory_stats.process_memory.virtual_size,
"resident_size": memory_stats.process_memory.resident_size,
"shared_size": memory_stats.process_memory.shared_size,
"private_size": memory_stats.process_memory.private_size,
"heap_size": memory_stats.process_memory.heap_size,
"stack_size": memory_stats.process_memory.stack_size,
"mapped_files": memory_stats.process_memory.mapped_files,
"peak_usage": memory_stats.process_memory.peak_usage
},
"system_memory": {
"allocation_count": memory_stats.system_memory.allocation_count,
"deallocation_count": memory_stats.system_memory.deallocation_count,
"active_allocations": memory_stats.system_memory.active_allocations,
"total_allocated": memory_stats.system_memory.total_allocated,
"total_deallocated": memory_stats.system_memory.total_deallocated,
"fragmentation_level": memory_stats.system_memory.fragmentation_level,
"large_pages": {
"supported": memory_stats.system_memory.large_pages.supported,
"total_large_pages": memory_stats.system_memory.large_pages.total_large_pages,
"used_large_pages": memory_stats.system_memory.large_pages.used_large_pages,
"page_size": memory_stats.system_memory.large_pages.page_size
}
},
"pressure_indicators": {
"pressure_level": format!("{:?}", memory_stats.pressure_indicators.pressure_level),
"low_memory": memory_stats.pressure_indicators.low_memory,
"swapping_active": memory_stats.pressure_indicators.swapping_active,
"allocation_failure_rate": memory_stats.pressure_indicators.allocation_failure_rate,
"gc_pressure": memory_stats.pressure_indicators.gc_pressure
}
}
});
let file_path = base_path.join("system_resources.json");
let json_string = serde_json::to_string_pretty(&json_data).map_err(|e| {
MemScopeError::error("export", "export_system_resources_json", e.to_string())
})?;
std::fs::write(&file_path, json_string).map_err(|e| {
MemScopeError::error("export", "export_system_resources_json", e.to_string())
})?;
Ok(())
}
pub fn export_ownership_graph_json<P: AsRef<Path>>(
base_path: P,
allocations: &[crate::capture::types::AllocationInfo],
event_store: &crate::event_store::EventStore,
) -> MemScopeResult<()> {
let base_path = base_path.as_ref();
let graph = build_ownership_graph_from_allocations(allocations, event_store);
let diagnostics = graph.diagnostics(50);
let borrow_analyzer = crate::analysis::borrow_analysis::get_global_borrow_analyzer();
let borrow_history = borrow_analyzer.get_borrow_history();
let nodes_json: Vec<_> = graph
.nodes
.iter()
.map(|node| {
json!({
"id": format!("0x{:x}", node.id.0),
"type_name": node.type_name,
"size": node.size,
"stack_ptr": node.stack_ptr.map(|p| format!("0x{:x}", p)),
})
})
.collect();
let mut edges_json: Vec<_> = graph
.edges
.iter()
.map(|edge| {
json!({
"from": format!("0x{:x}", edge.from.0),
"to": format!("0x{:x}", edge.to.0),
"kind": match edge.op {
EdgeKind::Owns => "Owns",
EdgeKind::Contains => "Contains",
EdgeKind::Borrows => "Borrows",
EdgeKind::RcClone => "RcClone",
EdgeKind::ArcClone => "ArcClone",
EdgeKind::Move => "Move",
EdgeKind::SharedBorrow => "SharedBorrow",
EdgeKind::MutBorrow => "MutBorrow",
},
})
})
.collect();
for event in &borrow_history {
let edge_kind = match event.borrow_info.borrow_type {
crate::analysis::borrow_analysis::BorrowType::Immutable => "SharedBorrow",
crate::analysis::borrow_analysis::BorrowType::Mutable => "MutBorrow",
crate::analysis::borrow_analysis::BorrowType::Shared => "Borrows",
crate::analysis::borrow_analysis::BorrowType::Weak => "Borrows",
};
edges_json.push(json!({
"from": format!("0x{:x}", event.borrow_info.ptr),
"to": format!("0x{:x}", event.borrow_info.ptr),
"kind": edge_kind,
"var_name": event.borrow_info.var_name,
"thread_id": event.borrow_info.thread_id,
"borrow_id": format!("{:?}", event.borrow_info.id),
}));
}
let cycles_json: Vec<_> = graph
.cycles
.iter()
.map(|cycle| {
let nodes: Vec<_> = cycle.iter().map(|id| format!("0x{:x}", id.0)).collect();
json!({
"nodes": nodes,
})
})
.collect();
let issues_json: Vec<_> = diagnostics
.issues
.iter()
.map(|issue| match issue {
crate::analysis::ownership_graph::DiagnosticIssue::RcCycle { nodes, cycle_type } => {
json!({
"type": "RcCycle",
"cycle_type": format!("{:?}", cycle_type),
"nodes": nodes.iter().map(|id| format!("0x{:x}", id.0)).collect::<Vec<_>>(),
"severity": "error",
})
}
crate::analysis::ownership_graph::DiagnosticIssue::ArcCloneStorm {
clone_count,
threshold,
} => {
json!({
"type": "ArcCloneStorm",
"clone_count": clone_count,
"threshold": threshold,
"severity": "warning",
})
}
})
.collect();
let root_cause_json = graph.find_root_cause().map(|rc| {
json!({
"cause": match rc.root_cause {
crate::analysis::ownership_graph::RootCause::ArcCloneStorm => "ArcCloneStorm",
crate::analysis::ownership_graph::RootCause::RcCycle => "RcCycle",
},
"description": rc.description,
"impact": rc.impact,
})
});
let json_data = json!({
"metadata": {
"export_version": "2.0",
"specification": "ownership graph analysis",
"timestamp": std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
},
"summary": {
"total_nodes": graph.nodes.len(),
"total_edges": graph.edges.len(),
"total_cycles": graph.cycles.len(),
"rc_clone_count": diagnostics.rc_clone_count,
"arc_clone_count": diagnostics.arc_clone_count,
"has_issues": diagnostics.has_issues(),
},
"nodes": nodes_json,
"edges": edges_json,
"cycles": cycles_json,
"diagnostics": {
"issues": issues_json,
"root_cause": root_cause_json,
},
});
let file_path = base_path.join("ownership_graph.json");
let json_string = serde_json::to_string_pretty(&json_data).map_err(|e| {
MemScopeError::error("export", "export_ownership_graph_json", e.to_string())
})?;
std::fs::write(&file_path, json_string).map_err(|e| {
MemScopeError::error("export", "export_ownership_graph_json", e.to_string())
})?;
Ok(())
}
fn build_ownership_graph_from_allocations(
allocations: &[crate::capture::types::AllocationInfo],
event_store: &crate::event_store::EventStore,
) -> OwnershipGraph {
debug!(
allocations = allocations.len(),
"Starting build_ownership_graph"
);
use crate::analysis::relation_inference::{detect_containers, Relation, RelationGraphBuilder};
use crate::event_store::MemoryEventType;
debug!("Converting allocations to passports");
let passports: Vec<(
NodeId,
String,
usize,
Vec<crate::analysis::ownership_graph::OwnershipEvent>,
)> = allocations
.iter()
.enumerate()
.map(|(idx, alloc)| {
let unique_ptr = if alloc.ptr == 0 {
crate::analysis::VIRTUAL_PTR_BASE + idx
} else {
alloc.ptr
};
let id = NodeId::from_ptr(unique_ptr);
let type_name = alloc
.type_name
.clone()
.unwrap_or_else(|| "unknown".to_string());
let size = alloc.size;
let events = vec![crate::analysis::ownership_graph::OwnershipEvent::new(
alloc.timestamp_alloc,
OwnershipOp::Create,
id,
None,
)];
if type_name.contains("Arc<") || type_name.contains("Rc<") {
}
(id, type_name, size, events)
})
.collect();
debug!(passports = passports.len(), "Created passports");
debug!("Building initial ownership graph");
let mut graph = OwnershipGraph::build(&passports);
debug!(
nodes = graph.nodes.len(),
edges = graph.edges.len(),
"Initial graph built"
);
let _heap_owner_original_indices: Vec<usize> = allocations
.iter()
.enumerate()
.filter(|(_, a)| a.timestamp_dealloc.is_none())
.map(|(i, _)| i)
.collect();
let heap_owner_allocations: Vec<ActiveAllocation> = allocations
.iter()
.enumerate()
.filter(|(_, a)| a.timestamp_dealloc.is_none())
.filter_map(|(_idx, a)| {
if a.ptr == 0 || is_virtual_pointer(a.ptr) {
return None;
}
Some(ActiveAllocation {
ptr: Some(a.ptr),
kind: crate::core::types::TrackKind::HeapOwner {
ptr: a.ptr,
size: a.size,
},
size: a.size,
allocated_at: a.timestamp_alloc,
var_name: a.var_name.clone(),
type_name: a.type_name.clone(),
thread_id: a.thread_id_u64,
call_stack_hash: None,
module_path: a.module_path.clone(),
stack_ptr: a.stack_ptr,
})
})
.collect();
for (i, alloc) in heap_owner_allocations.iter().enumerate() {
if i < graph.nodes.len() {
graph.nodes[i].stack_ptr = alloc.stack_ptr;
}
}
let valid_thread_ids: std::collections::HashSet<u64> =
heap_owner_allocations.iter().map(|a| a.thread_id).collect();
let container_events: Vec<_> = event_store
.snapshot()
.into_iter()
.filter(|e| e.event_type == MemoryEventType::Metadata)
.filter(|e| valid_thread_ids.contains(&e.thread_id))
.filter_map(|e| {
let type_name = e.type_name.clone().unwrap_or_default();
let var_name = e.var_name.clone().unwrap_or_default();
let is_container = type_name.contains("HashMap")
|| type_name.contains("BTreeMap")
|| type_name.contains("VecDeque")
|| type_name.contains("RefCell")
|| type_name.contains("RwLock");
if is_container {
return Some((e, type_name, var_name));
}
None
})
.collect();
let container_allocations: Vec<ActiveAllocation> = container_events
.iter()
.enumerate()
.map(|(idx, (e, type_name, var_name))| {
let virtual_ptr = 0x300000000u64 as usize + idx;
ActiveAllocation {
ptr: Some(virtual_ptr),
kind: crate::core::types::TrackKind::Container,
size: e.size.max(1),
allocated_at: e.timestamp,
var_name: Some(var_name.clone()),
type_name: Some(type_name.clone()),
thread_id: e.thread_id,
call_stack_hash: e.call_stack_hash,
module_path: e.module_path.clone(),
stack_ptr: None,
}
})
.collect();
let mut all_for_relation: Vec<ActiveAllocation> = Vec::new();
all_for_relation.extend(heap_owner_allocations.clone());
all_for_relation.extend(container_allocations.clone());
debug!("Running container detection");
let container_config = crate::analysis::relation_inference::ContainerConfig {
time_window_ns: 10_000_000, size_ratio: 10000, lookahead: 10, };
let container_edges = detect_containers(&all_for_relation, Some(container_config));
debug!(
container_edges = container_edges.len(),
"Container detection completed"
);
debug!("Running RelationGraphBuilder");
let relation_graph = RelationGraphBuilder::build(&heap_owner_allocations, None);
debug!(
edges = relation_graph.edges.len(),
"RelationGraphBuilder completed"
);
const CONTAINER_PTR_BASE: usize = 0x300000000;
let heap_owner_count = graph.nodes.len();
for (idx, (e, type_name, _var_name)) in container_events.iter().enumerate() {
let node_id = NodeId::from_ptr(CONTAINER_PTR_BASE + idx);
graph.nodes.push(crate::analysis::ownership_graph::Node {
id: node_id,
type_name: type_name.clone(),
size: e.size,
stack_ptr: None,
});
}
for edge in &relation_graph.edges {
let from_id = graph.nodes[edge.from].id;
let to_id = graph.nodes[edge.to].id;
let edge_kind = match edge.relation {
Relation::Owns => EdgeKind::Owns,
Relation::Contains => EdgeKind::Contains,
Relation::Slice => EdgeKind::Borrows,
Relation::Clone => EdgeKind::RcClone,
Relation::Shares => EdgeKind::ArcClone,
Relation::Evolution => EdgeKind::Contains,
Relation::ArcClone => EdgeKind::ArcClone,
Relation::RcClone => EdgeKind::RcClone,
Relation::ImmutableBorrow => EdgeKind::SharedBorrow,
Relation::MutableBorrow => EdgeKind::MutBorrow,
};
graph.edges.push(crate::analysis::ownership_graph::Edge {
from: from_id,
to: to_id,
op: edge_kind,
});
}
for edge in &container_edges {
let from_all_idx = edge.from;
let to_all_idx = edge.to;
if from_all_idx < heap_owner_count || to_all_idx >= heap_owner_count {
continue; }
let container_graph_idx = from_all_idx - heap_owner_count;
if container_graph_idx >= container_events.len() {
continue; }
let from_id = graph.nodes[heap_owner_count + container_graph_idx].id;
let to_id = graph.nodes[to_all_idx].id;
graph.edges.push(crate::analysis::ownership_graph::Edge {
from: from_id,
to: to_id,
op: EdgeKind::Contains,
});
}
debug!(
nodes = graph.nodes.len(),
edges = graph.edges.len(),
"Final ownership graph built"
);
graph
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_optimization_level_variants() {
let _low = OptimizationLevel::Low;
let _medium = OptimizationLevel::Medium;
let _high = OptimizationLevel::High;
let _maximum = OptimizationLevel::Maximum;
}
#[test]
fn test_optimization_level_default() {
let level = OptimizationLevel::default();
assert!(
matches!(level, OptimizationLevel::Medium),
"Default should be Medium"
);
}
#[test]
fn test_schema_validator_new() {
let validator = SchemaValidator::new();
let data = serde_json::json!({"test": "value"});
let result = validator.validate(&data);
assert!(result.is_ok(), "Validation should pass for any object");
}
#[test]
fn test_schema_validator_strict_mode() {
let validator = SchemaValidator::new().with_strict_mode(true);
let missing_fields = serde_json::json!({"other": "data"});
let result = validator.validate(&missing_fields);
assert!(result.is_err(), "Should fail with missing required fields");
let valid_data = serde_json::json!({
"timestamp": 123,
"allocations": [],
"stats": {}
});
let result = validator.validate(&valid_data);
assert!(result.is_ok(), "Should pass with all required fields");
}
#[test]
fn test_schema_validator_non_object() {
let validator = SchemaValidator::new();
let data = serde_json::json!("not an object");
let result = validator.validate(&data);
assert!(result.is_err(), "Should reject non-object data");
}
#[test]
fn test_export_json_options_default() {
let options = ExportJsonOptions::default();
assert!(
options.parallel_processing,
"parallel_processing should be true by default"
);
assert!(
options.streaming_writer,
"streaming_writer should be true by default"
);
assert!(
options.enable_type_cache,
"enable_type_cache should be true by default"
);
assert!(
options.adaptive_optimization,
"adaptive_optimization should be true by default"
);
assert!(
!options.schema_validation,
"schema_validation should be false by default"
);
assert!(
!options.security_analysis,
"security_analysis should be false by default"
);
}
#[test]
fn test_export_json_options_builders() {
let options = ExportJsonOptions::default()
.fast_export_mode(true)
.security_analysis(true)
.streaming_writer(false)
.schema_validation(true)
.integrity_hashes(true)
.batch_size(500)
.adaptive_optimization(false)
.max_cache_size(5000)
.include_low_severity(true)
.thread_count(Some(4));
assert!(options.fast_export_mode, "fast_export_mode should be true");
assert!(
options.security_analysis,
"security_analysis should be true"
);
assert!(
!options.streaming_writer,
"streaming_writer should be false"
);
assert!(
options.schema_validation,
"schema_validation should be true"
);
assert!(options.integrity_hashes, "integrity_hashes should be true");
assert_eq!(options.batch_size, 500, "batch_size should be 500");
assert!(
!options.adaptive_optimization,
"adaptive_optimization should be false"
);
assert_eq!(
options.max_cache_size, 5000,
"max_cache_size should be 5000"
);
assert!(
options.include_low_severity,
"include_low_severity should be true"
);
assert_eq!(
options.thread_count,
Some(4),
"thread_count should be Some(4)"
);
}
#[test]
fn test_export_error_variants() {
let io_err = ExportError::Io(std::io::Error::new(std::io::ErrorKind::NotFound, "test"));
let json_err = ExportError::Json(serde_json::from_str::<i32>("invalid").unwrap_err());
let export_err = ExportError::ExportFailed("test error".to_string());
assert!(
format!("{}", io_err).contains("IO error"),
"Should contain IO error"
);
assert!(
format!("{}", json_err).contains("JSON error"),
"Should contain JSON error"
);
assert!(
format!("{}", export_err).contains("Export failed"),
"Should contain Export failed"
);
}
#[test]
fn test_estimate_json_size() {
let string_val = serde_json::json!("hello world");
let num_val = serde_json::json!(42);
let array_val = serde_json::json!([1, 2, 3]);
let object_val = serde_json::json!({"key": "value"});
let string_size = estimate_json_size(&string_val);
let num_size = estimate_json_size(&num_val);
let array_size = estimate_json_size(&array_val);
let object_size = estimate_json_size(&object_val);
assert!(string_size > 0, "String size should be positive");
assert!(num_size > 0, "Number size should be positive");
assert!(array_size > 0, "Array size should be positive");
assert!(object_size > 0, "Object size should be positive");
}
#[test]
fn test_get_or_compute_type_info() {
assert_eq!(
get_or_compute_type_info("Vec<i32>", 100),
"dynamic_array",
"Vec should be dynamic_array"
);
assert_eq!(
get_or_compute_type_info("String", 24),
"string",
"String should be string"
);
assert_eq!(
get_or_compute_type_info("Box<i32>", 8),
"smart_pointer",
"Box should be smart_pointer"
);
assert_eq!(
get_or_compute_type_info("Rc<String>", 8),
"smart_pointer",
"Rc should be smart_pointer"
);
assert_eq!(
get_or_compute_type_info("Arc<i32>", 16),
"smart_pointer",
"Arc should be smart_pointer"
);
assert_eq!(
get_or_compute_type_info("[u8; 100]", 100),
"byte_array",
"u8 array should be byte_array"
);
assert_eq!(
get_or_compute_type_info("CustomType", 1024 * 1024 * 2),
"large_buffer",
"Large allocation should be large_buffer"
);
assert_eq!(
get_or_compute_type_info("MyType", 100),
"custom",
"Unknown type should be custom"
);
}
}