Skip to main content

memscope_rs/analysis/
ownership_graph.rs

1//! Ownership Graph Engine
2//!
3//! This module provides a post-analysis engine for revealing Rust ownership propagation.
4//! It consumes Memory Passport events to build ownership graphs and detect issues.
5//!
6//! Design principles:
7//! - Zero runtime cost (post-analysis only)
8//! - Minimal data model (only track critical operations)
9//! - Focus on practical problems (Rc cycles, Arc clone storms)
10//!
11//! # Architecture
12//!
13//! ```text
14//! Runtime Tracking
15//!         │
16//!         │ Allocation Tracker
17//!         │
18//!         │ Memory Passport Tracker
19//!         │
20//!         │ Passport.events
21//!         ▼
22//! Ownership Graph Engine
23//!         │
24//!         ▼
25//! Dashboard Visualization
26//! ```
27//!
28//! # Key Features
29//!
30//! - Rc/Arc Cycle Detection
31//! - Arc Clone Storm Detection
32//! - Ownership Chain Compression
33//! - Root Cause Diagnostics
34
35use serde::{Deserialize, Serialize};
36
37use super::node_id::NodeId;
38
39/// Object identifier - unified with NodeId.
40///
41/// This type alias provides backward compatibility.
42/// Previously, ObjectId was a separate struct that wrapped u64.
43/// Now it is unified with NodeId to avoid duplication.
44/// Use NodeId directly for new code.
45pub type ObjectId = NodeId;
46
47/// Ownership operation types - only track critical operations
48#[repr(u8)]
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
50pub enum OwnershipOp {
51    /// Object creation
52    Create,
53    /// Object deallocation
54    Drop,
55    /// Rc clone operation
56    RcClone,
57    /// Arc clone operation
58    ArcClone,
59}
60
61/// Ownership event recorded in passport
62#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
63pub struct OwnershipEvent {
64    /// Timestamp
65    pub ts: u64,
66    /// Operation type
67    pub op: OwnershipOp,
68    /// Source object
69    pub src: ObjectId,
70    /// Destination object (optional)
71    pub dst: Option<ObjectId>,
72}
73
74impl OwnershipEvent {
75    /// Create a new ownership event
76    pub fn new(ts: u64, op: OwnershipOp, src: ObjectId, dst: Option<ObjectId>) -> Self {
77        Self { ts, op, src, dst }
78    }
79}
80
81/// Node in ownership graph
82#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct Node {
84    /// Node identifier
85    pub id: ObjectId,
86    /// Type name
87    pub type_name: String,
88    /// Size in bytes
89    pub size: usize,
90}
91
92/// Edge kind
93#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
94pub enum EdgeKind {
95    /// Owner relationship (A contains pointer to B)
96    Owns,
97    /// Contains relationship (Container → HeapOwner, e.g., HashMap → Vec)
98    Contains,
99    /// Borrow relationship (A borrows from B)
100    Borrows,
101    /// Rc clone edge
102    RcClone,
103    /// Arc clone edge
104    ArcClone,
105}
106
107/// Edge in ownership graph
108#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct Edge {
110    /// From object
111    pub from: ObjectId,
112    /// To object
113    pub to: ObjectId,
114    /// Edge kind
115    pub op: EdgeKind,
116}
117
118/// Ownership graph representation
119#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct OwnershipGraph {
121    /// All nodes in the graph
122    pub nodes: Vec<Node>,
123    /// All edges in the graph
124    pub edges: Vec<Edge>,
125    /// Detected cycles
126    pub cycles: Vec<Vec<ObjectId>>,
127    /// Arc clone count for storm detection
128    pub arc_clone_count: usize,
129}
130
131impl OwnershipGraph {
132    /// Build ownership graph from MemoryView.
133    ///
134    /// Creates an ownership graph from the allocations in a MemoryView.
135    /// This is a convenience method for the unified analyzer API.
136    pub fn from_view(view: &crate::view::MemoryView) -> Self {
137        let allocations = view.allocations();
138        let passports: Vec<(ObjectId, String, usize, Vec<OwnershipEvent>)> = allocations
139            .iter()
140            .filter_map(|a| {
141                a.ptr.map(|ptr| {
142                    let id = ObjectId::from_ptr(ptr);
143                    let type_name = a.type_name.clone().unwrap_or_else(|| "unknown".to_string());
144                    // Create a basic create event for each allocation
145                    let event = OwnershipEvent::new(a.allocated_at, OwnershipOp::Create, id, None);
146                    (id, type_name, a.size, vec![event])
147                })
148            })
149            .collect();
150
151        Self::build(&passports)
152    }
153
154    /// Build ownership graph from passports with ownership events
155    pub fn build<T: AsRef<[OwnershipEvent]>>(passports: &[(ObjectId, String, usize, T)]) -> Self {
156        let mut nodes = Vec::new();
157        let mut edges = Vec::new();
158        let mut arc_clone_count = 0;
159
160        for (id, type_name, size, events) in passports {
161            // Add node
162            nodes.push(Node {
163                id: *id,
164                type_name: type_name.clone(),
165                size: *size,
166            });
167
168            // Process events
169            for event in events.as_ref() {
170                match event.op {
171                    OwnershipOp::RcClone => {
172                        if let Some(dst) = event.dst {
173                            edges.push(Edge {
174                                from: event.src,
175                                to: dst,
176                                op: EdgeKind::RcClone,
177                            });
178                        }
179                    }
180                    OwnershipOp::ArcClone => {
181                        arc_clone_count += 1;
182                        if let Some(dst) = event.dst {
183                            edges.push(Edge {
184                                from: event.src,
185                                to: dst,
186                                op: EdgeKind::ArcClone,
187                            });
188                        }
189                    }
190                    OwnershipOp::Create | OwnershipOp::Drop => {
191                        // These don't create edges
192                    }
193                }
194            }
195        }
196
197        // Compress clone chains
198        Self::compress_clone_chains(&mut edges);
199
200        // Detect cycles
201        let cycles = Self::detect_cycles(&edges);
202
203        OwnershipGraph {
204            nodes,
205            edges,
206            cycles,
207            arc_clone_count,
208        }
209    }
210
211    /// Compress consecutive clone chains to reduce UI complexity.
212    ///
213    /// Uses a new vector to collect merged edges, avoiding O(n^2) complexity from repeated remove() calls.
214    fn compress_clone_chains(edges: &mut Vec<Edge>) {
215        if edges.len() < 2 {
216            return;
217        }
218
219        let mut result: Vec<Edge> = Vec::with_capacity(edges.len());
220        let mut i = 0;
221
222        while i < edges.len() {
223            let mut current = edges[i].clone();
224
225            // Try to merge with subsequent edges
226            while i + 1 < edges.len()
227                && current.op == edges[i + 1].op
228                && current.to == edges[i + 1].from
229            {
230                // Merge: extend current edge to skip the next one
231                current.to = edges[i + 1].to;
232                i += 1;
233            }
234
235            result.push(current);
236            i += 1;
237        }
238
239        *edges = result;
240    }
241
242    /// Detect cycles using existing DFS implementation
243    fn detect_cycles(edges: &[Edge]) -> Vec<Vec<ObjectId>> {
244        use crate::analysis::relationship_cycle_detector;
245
246        if edges.is_empty() {
247            return Vec::new();
248        }
249
250        // Convert edges to the format expected by the cycle detector
251        let relationships: Vec<(String, String, String)> = edges
252            .iter()
253            .map(|e| {
254                (
255                    format!("0x{:x}", e.from.0),
256                    format!("0x{:x}", e.to.0),
257                    format!("{:?}", e.op).to_lowercase(),
258                )
259            })
260            .collect();
261
262        let result = relationship_cycle_detector::detect_cycles_with_indices(&relationships);
263
264        // Convert cycle indices back to ObjectIds
265        let mut cycles = Vec::new();
266        let mut obj_id_map: std::collections::HashMap<String, ObjectId> =
267            std::collections::HashMap::new();
268
269        for edge in edges {
270            obj_id_map.insert(format!("0x{:x}", edge.from.0), edge.from);
271            obj_id_map.insert(format!("0x{:x}", edge.to.0), edge.to);
272        }
273
274        for (from_idx, to_idx) in result.cycle_edges {
275            if let (Some(from_label), Some(to_label)) = (
276                result.node_labels.get(from_idx),
277                result.node_labels.get(to_idx),
278            ) {
279                if let (Some(from_id), Some(to_id)) =
280                    (obj_id_map.get(from_label), obj_id_map.get(to_label))
281                {
282                    // Simple cycle representation
283                    cycles.push(vec![*from_id, *to_id]);
284                }
285            }
286        }
287
288        cycles
289    }
290
291    /// Check if Arc clone storm is detected
292    pub fn has_arc_clone_storm(&self, threshold: usize) -> bool {
293        self.arc_clone_count > threshold
294    }
295
296    /// Get all Rc clone edges
297    pub fn rc_clones(&self) -> Vec<&Edge> {
298        self.edges
299            .iter()
300            .filter(|e| e.op == EdgeKind::RcClone)
301            .collect()
302    }
303
304    /// Get all Arc clone edges
305    pub fn arc_clones(&self) -> Vec<&Edge> {
306        self.edges
307            .iter()
308            .filter(|e| e.op == EdgeKind::ArcClone)
309            .collect()
310    }
311
312    /// Generate diagnostics report for detected issues
313    pub fn diagnostics(&self, arc_storm_threshold: usize) -> OwnershipDiagnostics {
314        let mut issues = Vec::new();
315
316        // Check for Rc cycles
317        for cycle in &self.cycles {
318            let cycle_type = self.detect_cycle_type(cycle);
319            issues.push(DiagnosticIssue::RcCycle {
320                nodes: cycle.clone(),
321                cycle_type,
322            });
323        }
324
325        // Check for Arc clone storm
326        if self.has_arc_clone_storm(arc_storm_threshold) {
327            issues.push(DiagnosticIssue::ArcCloneStorm {
328                clone_count: self.arc_clone_count,
329                threshold: arc_storm_threshold,
330            });
331        }
332
333        OwnershipDiagnostics {
334            issues,
335            total_nodes: self.nodes.len(),
336            total_edges: self.edges.len(),
337            rc_clone_count: self.rc_clones().len(),
338            arc_clone_count: self.arc_clone_count,
339        }
340    }
341
342    /// Detect the type of cycle (Rc or Arc)
343    fn detect_cycle_type(&self, cycle: &[ObjectId]) -> CycleType {
344        // Check if any edge in the cycle is an RcClone
345        for edge in &self.edges {
346            if cycle.contains(&edge.from)
347                && cycle.contains(&edge.to)
348                && edge.op == EdgeKind::RcClone
349            {
350                return CycleType::Rc;
351            }
352        }
353        CycleType::Arc
354    }
355
356    /// Find root cause chain for memory growth
357    pub fn find_root_cause(&self) -> Option<RootCauseChain> {
358        // Check for Arc clone storm as root cause
359        if self.arc_clone_count > 50 {
360            return Some(RootCauseChain {
361                root_cause: RootCause::ArcCloneStorm,
362                description: format!(
363                    "Arc clone storm detected: {} clones causing memory proliferation",
364                    self.arc_clone_count
365                ),
366                impact: format!(
367                    "Potential memory spike from {} Arc clone operations",
368                    self.arc_clone_count
369                ),
370            });
371        }
372
373        // Check for Rc cycles as root cause
374        if !self.cycles.is_empty() {
375            return Some(RootCauseChain {
376                root_cause: RootCause::RcCycle,
377                description: format!(
378                    "Rc retain cycle detected: {} cycles found",
379                    self.cycles.len()
380                ),
381                impact: "Memory leak due to reference count cycles".to_string(),
382            });
383        }
384
385        None
386    }
387}
388
389/// Type of ownership cycle
390#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
391pub enum CycleType {
392    /// Rc reference cycle (memory leak)
393    Rc,
394    /// Arc reference cycle (potential leak)
395    Arc,
396}
397
398/// Diagnostic issue types
399#[derive(Debug, Clone, Serialize, Deserialize)]
400pub enum DiagnosticIssue {
401    /// Rc retain cycle detected
402    RcCycle {
403        nodes: Vec<ObjectId>,
404        cycle_type: CycleType,
405    },
406    /// Arc clone storm detected
407    ArcCloneStorm {
408        clone_count: usize,
409        threshold: usize,
410    },
411}
412
413/// Diagnostics output for ownership graph
414#[derive(Debug, Clone, Serialize, Deserialize)]
415pub struct OwnershipDiagnostics {
416    /// Detected issues
417    pub issues: Vec<DiagnosticIssue>,
418    /// Total number of nodes
419    pub total_nodes: usize,
420    /// Total number of edges
421    pub total_edges: usize,
422    /// Number of Rc clone operations
423    pub rc_clone_count: usize,
424    /// Number of Arc clone operations
425    pub arc_clone_count: usize,
426}
427
428impl OwnershipDiagnostics {
429    /// Check if any issues were detected
430    pub fn has_issues(&self) -> bool {
431        !self.issues.is_empty()
432    }
433
434    /// Get summary string for display
435    pub fn summary(&self) -> String {
436        let mut summary = format!(
437            "Ownership Graph: {} nodes, {} edges\n",
438            self.total_nodes, self.total_edges
439        );
440        for issue in &self.issues {
441            match issue {
442                DiagnosticIssue::RcCycle { nodes, cycle_type } => {
443                    summary.push_str(&format!(
444                        "🔴 {:?} Cycle detected: {} nodes\n",
445                        cycle_type,
446                        nodes.len()
447                    ));
448                }
449                DiagnosticIssue::ArcCloneStorm {
450                    clone_count,
451                    threshold,
452                } => {
453                    summary.push_str(&format!(
454                        "⚠ Arc Clone Storm: {} clones (threshold: {})\n",
455                        clone_count, threshold
456                    ));
457                }
458            }
459        }
460        summary
461    }
462}
463
464/// Root cause of memory issues
465#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
466pub enum RootCause {
467    /// Arc clone storm causing memory proliferation
468    ArcCloneStorm,
469    /// Rc retain cycle causing memory leak
470    RcCycle,
471}
472
473/// Root cause chain for memory growth analysis
474#[derive(Debug, Clone, Serialize, Deserialize)]
475pub struct RootCauseChain {
476    /// Root cause type
477    pub root_cause: RootCause,
478    /// Description of the root cause
479    pub description: String,
480    /// Impact on memory
481    pub impact: String,
482}
483
484#[cfg(test)]
485mod tests {
486    use super::*;
487
488    #[test]
489    fn test_object_id_from_ptr() {
490        let id = ObjectId::from_ptr(0x1000);
491        assert_eq!(id.0, 0x1000);
492    }
493
494    #[test]
495    fn test_ownership_event_creation() {
496        let id = NodeId(0x1000);
497        let event = OwnershipEvent::new(1000, OwnershipOp::Create, id, None);
498        assert_eq!(event.ts, 1000);
499        assert_eq!(event.op, OwnershipOp::Create);
500    }
501
502    #[test]
503    fn test_graph_build_empty() {
504        let passports: Vec<(NodeId, String, usize, Vec<OwnershipEvent>)> = vec![];
505        let graph = OwnershipGraph::build(&passports);
506        assert!(graph.nodes.is_empty());
507        assert!(graph.edges.is_empty());
508        assert!(graph.cycles.is_empty());
509    }
510
511    #[test]
512    fn test_graph_build_rc_clone() {
513        let id1 = NodeId(0x1000);
514        let id2 = NodeId(0x2000);
515        let events = vec![OwnershipEvent::new(
516            1000,
517            OwnershipOp::RcClone,
518            id1,
519            Some(id2),
520        )];
521        let passports = vec![(id1, "Rc<i32>".to_string(), 8, events)];
522
523        let graph = OwnershipGraph::build(&passports);
524        assert_eq!(graph.nodes.len(), 1);
525        assert_eq!(graph.edges.len(), 1);
526        assert_eq!(graph.edges[0].op, EdgeKind::RcClone);
527    }
528
529    #[test]
530    fn test_graph_build_arc_clone_storm() {
531        let id1 = NodeId(0x1000);
532        let mut events = Vec::new();
533        for i in 0..100 {
534            let dst = NodeId(0x2000 + i);
535            events.push(OwnershipEvent::new(
536                i,
537                OwnershipOp::ArcClone,
538                id1,
539                Some(dst),
540            ));
541        }
542        let passports = vec![(id1, "Arc<i32>".to_string(), 8, events)];
543
544        let graph = OwnershipGraph::build(&passports);
545        assert_eq!(graph.arc_clone_count, 100);
546        assert!(graph.has_arc_clone_storm(50));
547    }
548
549    #[test]
550    fn test_compress_clone_chains() {
551        let id1 = NodeId(0x1000);
552        let id2 = NodeId(0x2000);
553        let id3 = NodeId(0x3000);
554        let id4 = NodeId(0x4000);
555
556        let events = vec![
557            OwnershipEvent::new(1000, OwnershipOp::ArcClone, id1, Some(id2)),
558            OwnershipEvent::new(2000, OwnershipOp::ArcClone, id2, Some(id3)),
559            OwnershipEvent::new(3000, OwnershipOp::ArcClone, id3, Some(id4)),
560        ];
561        let passports = vec![(id1, "Arc<i32>".to_string(), 8, events)];
562
563        let graph = OwnershipGraph::build(&passports);
564
565        // After compression, we should have only 1 edge: id1 -> id4
566        assert_eq!(graph.edges.len(), 1);
567        assert_eq!(graph.edges[0].from, id1);
568        assert_eq!(graph.edges[0].to, id4);
569    }
570
571    #[test]
572    fn test_diagnostics() {
573        let id1 = NodeId(0x1000);
574        let mut events = Vec::new();
575        for i in 0..100 {
576            let dst = NodeId(0x2000 + i);
577            events.push(OwnershipEvent::new(
578                i,
579                OwnershipOp::ArcClone,
580                id1,
581                Some(dst),
582            ));
583        }
584        let passports = vec![(id1, "Arc<i32>".to_string(), 8, events)];
585
586        let graph = OwnershipGraph::build(&passports);
587        let diagnostics = graph.diagnostics(50);
588
589        assert!(diagnostics.has_issues());
590        assert!(diagnostics.summary().contains("Arc Clone Storm"));
591    }
592
593    #[test]
594    fn test_root_cause_detection() {
595        let id1 = NodeId(0x1000);
596        let mut events = Vec::new();
597        for i in 0..100 {
598            let dst = NodeId(0x2000 + i);
599            events.push(OwnershipEvent::new(
600                i,
601                OwnershipOp::ArcClone,
602                id1,
603                Some(dst),
604            ));
605        }
606        let passports = vec![(id1, "Arc<i32>".to_string(), 8, events)];
607
608        let graph = OwnershipGraph::build(&passports);
609        let root_cause = graph.find_root_cause();
610
611        assert!(root_cause.is_some());
612        let chain = root_cause.unwrap();
613        assert_eq!(chain.root_cause, RootCause::ArcCloneStorm);
614    }
615}