use crate::analysis::is_virtual_pointer;
use crate::analysis::node_id::NodeId;
use crate::analysis::ownership_graph::{EdgeKind, OwnershipGraph, OwnershipOp};
use crate::core::{MemScopeError, MemScopeResult};
use crate::snapshot::ActiveAllocation;
use serde_json::json;
use std::path::Path;
use tracing::debug;
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 events = event_store.snapshot();
let borrow_history =
crate::analysis::borrow_analysis::get_global_borrow_analyzer().get_borrow_history();
export_ownership_graph_snapshot(
base_path,
allocations,
&events,
&borrow_history,
super::capture_cut_ns(),
)
}
pub fn export_ownership_graph_snapshot<P: AsRef<Path>>(
base_path: P,
allocations: &[crate::capture::types::AllocationInfo],
events: &[crate::event_store::MemoryEvent],
borrow_history: &[crate::analysis::borrow_analysis::BorrowEvent],
captured_at: u64,
) -> MemScopeResult<()> {
let base_path = base_path.as_ref();
let graph = build_ownership_graph_from_allocations(allocations, events);
let diagnostics = graph.diagnostics(50);
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": captured_at / 1_000_000_000,
"snapshot_timestamp_ns": captured_at,
"events_source": "event_snapshot", "borrow_source": "borrow_history_snapshot"
},
"summary": {
"total_nodes": graph.nodes.len(), "total_edges": edges_json.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],
events: &[crate::event_store::MemoryEvent],
) -> 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<_> = events
.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
}