use serde::{Deserialize, Serialize};
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, Serialize, Deserialize)]
pub struct ObjectId(pub u64);
impl ObjectId {
pub fn from_ptr(ptr: usize) -> Self {
ObjectId(ptr as u64)
}
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum OwnershipOp {
Create,
Drop,
RcClone,
ArcClone,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct OwnershipEvent {
pub ts: u64,
pub op: OwnershipOp,
pub src: ObjectId,
pub dst: Option<ObjectId>,
}
impl OwnershipEvent {
pub fn new(ts: u64, op: OwnershipOp, src: ObjectId, dst: Option<ObjectId>) -> Self {
Self { ts, op, src, dst }
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Node {
pub id: ObjectId,
pub type_name: String,
pub size: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum EdgeKind {
Owns,
Borrows,
RcClone,
ArcClone,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Edge {
pub from: ObjectId,
pub to: ObjectId,
pub op: EdgeKind,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OwnershipGraph {
pub nodes: Vec<Node>,
pub edges: Vec<Edge>,
pub cycles: Vec<Vec<ObjectId>>,
pub arc_clone_count: usize,
}
impl OwnershipGraph {
pub fn build<T: AsRef<[OwnershipEvent]>>(passports: &[(ObjectId, String, usize, T)]) -> Self {
let mut nodes = Vec::new();
let mut edges = Vec::new();
let mut arc_clone_count = 0;
for (id, type_name, size, events) in passports {
nodes.push(Node {
id: *id,
type_name: type_name.clone(),
size: *size,
});
for event in events.as_ref() {
match event.op {
OwnershipOp::RcClone => {
if let Some(dst) = event.dst {
edges.push(Edge {
from: event.src,
to: dst,
op: EdgeKind::RcClone,
});
}
}
OwnershipOp::ArcClone => {
arc_clone_count += 1;
if let Some(dst) = event.dst {
edges.push(Edge {
from: event.src,
to: dst,
op: EdgeKind::ArcClone,
});
}
}
OwnershipOp::Create | OwnershipOp::Drop => {
}
}
}
}
Self::compress_clone_chains(&mut edges);
let cycles = Self::detect_cycles(&edges);
OwnershipGraph {
nodes,
edges,
cycles,
arc_clone_count,
}
}
fn compress_clone_chains(edges: &mut Vec<Edge>) {
let mut i = 0;
while i + 1 < edges.len() {
if edges[i].op == edges[i + 1].op && edges[i].to == edges[i + 1].from {
edges[i].to = edges[i + 1].to;
edges.remove(i + 1);
} else {
i += 1;
}
}
}
fn detect_cycles(edges: &[Edge]) -> Vec<Vec<ObjectId>> {
use crate::analysis::relationship_cycle_detector;
if edges.is_empty() {
return Vec::new();
}
let relationships: Vec<(String, String, String)> = edges
.iter()
.map(|e| {
(
format!("0x{:x}", e.from.0),
format!("0x{:x}", e.to.0),
format!("{:?}", e.op).to_lowercase(),
)
})
.collect();
let result = relationship_cycle_detector::detect_cycles_with_indices(&relationships);
let mut cycles = Vec::new();
let mut obj_id_map: std::collections::HashMap<String, ObjectId> =
std::collections::HashMap::new();
for edge in edges {
obj_id_map.insert(format!("0x{:x}", edge.from.0), edge.from);
obj_id_map.insert(format!("0x{:x}", edge.to.0), edge.to);
}
for (from_idx, to_idx) in result.cycle_edges {
if let (Some(from_label), Some(to_label)) = (
result.node_labels.get(from_idx),
result.node_labels.get(to_idx),
) {
if let (Some(from_id), Some(to_id)) =
(obj_id_map.get(from_label), obj_id_map.get(to_label))
{
cycles.push(vec![*from_id, *to_id]);
}
}
}
cycles
}
pub fn has_arc_clone_storm(&self, threshold: usize) -> bool {
self.arc_clone_count > threshold
}
pub fn rc_clones(&self) -> Vec<&Edge> {
self.edges
.iter()
.filter(|e| e.op == EdgeKind::RcClone)
.collect()
}
pub fn arc_clones(&self) -> Vec<&Edge> {
self.edges
.iter()
.filter(|e| e.op == EdgeKind::ArcClone)
.collect()
}
pub fn diagnostics(&self, arc_storm_threshold: usize) -> OwnershipDiagnostics {
let mut issues = Vec::new();
for cycle in &self.cycles {
let cycle_type = self.detect_cycle_type(cycle);
issues.push(DiagnosticIssue::RcCycle {
nodes: cycle.clone(),
cycle_type,
});
}
if self.has_arc_clone_storm(arc_storm_threshold) {
issues.push(DiagnosticIssue::ArcCloneStorm {
clone_count: self.arc_clone_count,
threshold: arc_storm_threshold,
});
}
OwnershipDiagnostics {
issues,
total_nodes: self.nodes.len(),
total_edges: self.edges.len(),
rc_clone_count: self.rc_clones().len(),
arc_clone_count: self.arc_clone_count,
}
}
fn detect_cycle_type(&self, cycle: &[ObjectId]) -> CycleType {
for edge in &self.edges {
if cycle.contains(&edge.from)
&& cycle.contains(&edge.to)
&& edge.op == EdgeKind::RcClone
{
return CycleType::Rc;
}
}
CycleType::Arc
}
pub fn find_root_cause(&self) -> Option<RootCauseChain> {
if self.arc_clone_count > 50 {
return Some(RootCauseChain {
root_cause: RootCause::ArcCloneStorm,
description: format!(
"Arc clone storm detected: {} clones causing memory proliferation",
self.arc_clone_count
),
impact: format!(
"Potential memory spike from {} Arc clone operations",
self.arc_clone_count
),
});
}
if !self.cycles.is_empty() {
return Some(RootCauseChain {
root_cause: RootCause::RcCycle,
description: format!(
"Rc retain cycle detected: {} cycles found",
self.cycles.len()
),
impact: "Memory leak due to reference count cycles".to_string(),
});
}
None
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum CycleType {
Rc,
Arc,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum DiagnosticIssue {
RcCycle {
nodes: Vec<ObjectId>,
cycle_type: CycleType,
},
ArcCloneStorm {
clone_count: usize,
threshold: usize,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OwnershipDiagnostics {
pub issues: Vec<DiagnosticIssue>,
pub total_nodes: usize,
pub total_edges: usize,
pub rc_clone_count: usize,
pub arc_clone_count: usize,
}
impl OwnershipDiagnostics {
pub fn has_issues(&self) -> bool {
!self.issues.is_empty()
}
pub fn summary(&self) -> String {
let mut summary = format!(
"Ownership Graph: {} nodes, {} edges\n",
self.total_nodes, self.total_edges
);
for issue in &self.issues {
match issue {
DiagnosticIssue::RcCycle { nodes, cycle_type } => {
summary.push_str(&format!(
"🔴 {:?} Cycle detected: {} nodes\n",
cycle_type,
nodes.len()
));
}
DiagnosticIssue::ArcCloneStorm {
clone_count,
threshold,
} => {
summary.push_str(&format!(
"⚠ Arc Clone Storm: {} clones (threshold: {})\n",
clone_count, threshold
));
}
}
}
summary
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum RootCause {
ArcCloneStorm,
RcCycle,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RootCauseChain {
pub root_cause: RootCause,
pub description: String,
pub impact: String,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_object_id_from_ptr() {
let id = ObjectId::from_ptr(0x1000);
assert_eq!(id.0, 0x1000);
}
#[test]
fn test_ownership_event_creation() {
let id = ObjectId(0x1000);
let event = OwnershipEvent::new(1000, OwnershipOp::Create, id, None);
assert_eq!(event.ts, 1000);
assert_eq!(event.op, OwnershipOp::Create);
}
#[test]
fn test_graph_build_empty() {
let passports: Vec<(ObjectId, String, usize, Vec<OwnershipEvent>)> = vec![];
let graph = OwnershipGraph::build(&passports);
assert!(graph.nodes.is_empty());
assert!(graph.edges.is_empty());
assert!(graph.cycles.is_empty());
}
#[test]
fn test_graph_build_rc_clone() {
let id1 = ObjectId(0x1000);
let id2 = ObjectId(0x2000);
let events = vec![OwnershipEvent::new(
1000,
OwnershipOp::RcClone,
id1,
Some(id2),
)];
let passports = vec![(id1, "Rc<i32>".to_string(), 8, events)];
let graph = OwnershipGraph::build(&passports);
assert_eq!(graph.nodes.len(), 1);
assert_eq!(graph.edges.len(), 1);
assert_eq!(graph.edges[0].op, EdgeKind::RcClone);
}
#[test]
fn test_graph_build_arc_clone_storm() {
let id1 = ObjectId(0x1000);
let mut events = Vec::new();
for i in 0..100 {
let dst = ObjectId(0x2000 + i);
events.push(OwnershipEvent::new(
i,
OwnershipOp::ArcClone,
id1,
Some(dst),
));
}
let passports = vec![(id1, "Arc<i32>".to_string(), 8, events)];
let graph = OwnershipGraph::build(&passports);
assert_eq!(graph.arc_clone_count, 100);
assert!(graph.has_arc_clone_storm(50));
}
#[test]
fn test_compress_clone_chains() {
let id1 = ObjectId(0x1000);
let id2 = ObjectId(0x2000);
let id3 = ObjectId(0x3000);
let id4 = ObjectId(0x4000);
let events = vec![
OwnershipEvent::new(1000, OwnershipOp::ArcClone, id1, Some(id2)),
OwnershipEvent::new(2000, OwnershipOp::ArcClone, id2, Some(id3)),
OwnershipEvent::new(3000, OwnershipOp::ArcClone, id3, Some(id4)),
];
let passports = vec![(id1, "Arc<i32>".to_string(), 8, events)];
let graph = OwnershipGraph::build(&passports);
assert_eq!(graph.edges.len(), 1);
assert_eq!(graph.edges[0].from, id1);
assert_eq!(graph.edges[0].to, id4);
}
#[test]
fn test_diagnostics() {
let id1 = ObjectId(0x1000);
let mut events = Vec::new();
for i in 0..100 {
let dst = ObjectId(0x2000 + i);
events.push(OwnershipEvent::new(
i,
OwnershipOp::ArcClone,
id1,
Some(dst),
));
}
let passports = vec![(id1, "Arc<i32>".to_string(), 8, events)];
let graph = OwnershipGraph::build(&passports);
let diagnostics = graph.diagnostics(50);
assert!(diagnostics.has_issues());
assert!(diagnostics.summary().contains("Arc Clone Storm"));
}
#[test]
fn test_root_cause_detection() {
let id1 = ObjectId(0x1000);
let mut events = Vec::new();
for i in 0..100 {
let dst = ObjectId(0x2000 + i);
events.push(OwnershipEvent::new(
i,
OwnershipOp::ArcClone,
id1,
Some(dst),
));
}
let passports = vec![(id1, "Arc<i32>".to_string(), 8, events)];
let graph = OwnershipGraph::build(&passports);
let root_cause = graph.find_root_cause();
assert!(root_cause.is_some());
let chain = root_cause.unwrap();
assert_eq!(chain.root_cause, RootCause::ArcCloneStorm);
}
}