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