Skip to main content

memscope_rs/analyzer/
graph.rs

1//! Graph analysis module.
2
3use crate::analysis::relation_inference::{Relation, RelationGraph};
4use crate::analyzer::report::{CycleInfo, CycleReport};
5use crate::view::MemoryView;
6use std::collections::HashSet;
7use tracing::{debug, info, warn};
8
9/// Relationship edge information for visualization.
10#[derive(Debug, Clone)]
11pub struct RelationshipEdge {
12    /// Source allocation pointer
13    pub from_ptr: usize,
14    /// Target allocation pointer
15    pub to_ptr: usize,
16    /// Source variable name
17    pub from_var_name: Option<String>,
18    /// Target variable name
19    pub to_var_name: Option<String>,
20    /// Source type name
21    pub from_type_name: Option<String>,
22    /// Target type name
23    pub to_type_name: Option<String>,
24    /// Relationship type
25    pub relation: Relation,
26    /// Source is Container type (no heap pointer)
27    pub is_container_source: bool,
28    /// Target is Container type (no heap pointer)
29    pub is_container_target: bool,
30}
31
32/// Graph analysis module.
33///
34/// Provides ownership and relationship graph analysis.
35pub struct GraphAnalysis {
36    view: MemoryView,
37    relation_graph: Option<RelationGraph>,
38}
39
40impl GraphAnalysis {
41    /// Create from view.
42    pub fn from_view(view: &MemoryView) -> Self {
43        debug!("Creating GraphAnalysis with {} allocations", view.len());
44        Self {
45            view: view.clone(),
46            relation_graph: None,
47        }
48    }
49
50    /// Get or build the relation graph (lazy).
51    fn relation_graph(&mut self) -> &RelationGraph {
52        if self.relation_graph.is_none() {
53            info!("Building relation graph from view");
54            self.relation_graph = Some(RelationGraph::from_view(&self.view));
55        }
56        self.relation_graph.as_ref().unwrap_or_else(|| {
57            unreachable!("RelationGraph should be initialized after lazy initialization")
58        })
59    }
60
61    /// Detect cycles in ownership graph.
62    ///
63    /// Uses the project's relation inference system to build actual
64    /// memory relationships, then applies the existing cycle detection
65    /// algorithm from RelationGraph::detect_cycles().
66    ///
67    /// Uses lazy initialization to cache the relation graph for efficiency.
68    pub fn cycles(&mut self) -> CycleReport {
69        let allocations = self.view.allocations();
70        if allocations.is_empty() {
71            debug!("Cycle detection: no allocations, returning empty report");
72            return CycleReport::empty();
73        }
74
75        let relation_graph = self.relation_graph();
76        let cycle_edges = relation_graph.detect_cycles();
77
78        if cycle_edges.is_empty() {
79            debug!("Cycle detection: no cycles found");
80            return CycleReport::empty();
81        }
82
83        info!("Cycle detection: found {} cycles", cycle_edges.len());
84
85        let cycles: Vec<CycleInfo> = cycle_edges
86            .iter()
87            .map(|(from, to, _)| CycleInfo {
88                nodes: vec![*from as u64, *to as u64],
89            })
90            .collect();
91
92        CycleReport {
93            cycle_count: cycles.len(),
94            cycles,
95        }
96    }
97
98    /// Get ownership statistics.
99    pub fn ownership_stats(&self) -> OwnershipStats {
100        let allocations = self.view.allocations();
101        OwnershipStats {
102            total_objects: allocations.len(),
103            total_bytes: allocations.iter().map(|a| a.size).sum(),
104            unique_types: allocations
105                .iter()
106                .filter_map(|a| a.type_name.as_ref())
107                .collect::<HashSet<_>>()
108                .len(),
109        }
110    }
111
112    /// Get relationship statistics from actual graph analysis.
113    pub fn relationship_stats(&mut self) -> RelationshipStats {
114        let relation_graph = self.relation_graph();
115        let edges = &relation_graph.edges;
116
117        let mut ownership_edges = 0usize;
118        let mut contains_edges = 0usize;
119        let mut shares_edges = 0usize;
120        let mut slice_edges = 0usize;
121        let mut clone_edges = 0usize;
122
123        for edge in edges {
124            match edge.relation {
125                Relation::Owns => ownership_edges += 1,
126                Relation::Contains => contains_edges += 1,
127                Relation::Shares => shares_edges += 1,
128                Relation::Slice => slice_edges += 1,
129                Relation::Clone => clone_edges += 1,
130                Relation::Evolution => {}
131            }
132        }
133
134        RelationshipStats {
135            ownership_edges,
136            contains_edges,
137            shares_edges,
138            slice_edges,
139            clone_edges,
140        }
141    }
142
143    /// Get all relationship edges for visualization.
144    ///
145    /// Returns a list of relationship edges with full allocation info.
146    /// Limited to 500 edges to avoid performance issues.
147    pub fn relationships(&mut self) -> Vec<RelationshipEdge> {
148        // Clone allocations to avoid borrow conflict with relation_graph()
149        let allocations: Vec<crate::snapshot::types::ActiveAllocation> = self
150            .view
151            .allocations()
152            .iter()
153            .map(|a| (*a).clone())
154            .collect();
155
156        let relation_graph = self.relation_graph();
157        let edges = &relation_graph.edges;
158
159        debug!("Building relationships from {} edges", edges.len());
160
161        let mut relationships: Vec<RelationshipEdge> = edges
162            .iter()
163            .filter_map(|edge| {
164                let from_alloc = allocations.get(edge.from);
165                let to_alloc = allocations.get(edge.to);
166
167                let from_ptr = from_alloc.and_then(|a| a.ptr).unwrap_or(edge.from);
168                let to_ptr = to_alloc.and_then(|a| a.ptr).unwrap_or(edge.to);
169
170                // Skip if both are virtual/container pointers
171                if from_ptr == 0 && to_ptr == 0 {
172                    return None;
173                }
174
175                Some(RelationshipEdge {
176                    from_ptr,
177                    to_ptr,
178                    from_var_name: from_alloc.and_then(|a| a.var_name.clone()),
179                    to_var_name: to_alloc.and_then(|a| a.var_name.clone()),
180                    from_type_name: from_alloc.and_then(|a| a.type_name.clone()),
181                    to_type_name: to_alloc.and_then(|a| a.type_name.clone()),
182                    relation: edge.relation.clone(),
183                    is_container_source: from_ptr == 0,
184                    is_container_target: to_ptr == 0,
185                })
186            })
187            .collect();
188
189        // Limit relationships to avoid performance issues
190        if relationships.len() > 500 {
191            warn!(
192                "Relationship count ({}) exceeds limit, truncating to 500",
193                relationships.len()
194            );
195            relationships.truncate(500);
196        }
197
198        debug!("Returning {} relationships", relationships.len());
199        relationships
200    }
201}
202
203/// Ownership statistics.
204#[derive(Debug, Clone)]
205pub struct OwnershipStats {
206    /// Total number of objects
207    pub total_objects: usize,
208    /// Total bytes owned
209    pub total_bytes: usize,
210    /// Number of unique types
211    pub unique_types: usize,
212}
213
214/// Relationship statistics computed from actual graph analysis.
215#[derive(Debug, Clone, Default)]
216pub struct RelationshipStats {
217    /// Number of ownership edges (A owns heap memory)
218    pub ownership_edges: usize,
219    /// Number of contains edges (Container → HeapOwner)
220    pub contains_edges: usize,
221    /// Number of shares edges (Arc/Rc)
222    pub shares_edges: usize,
223    /// Number of slice edges (view into sub-region)
224    pub slice_edges: usize,
225    /// Number of clone edges (copy of another allocation)
226    pub clone_edges: usize,
227}
228
229#[cfg(test)]
230mod tests {
231    use super::*;
232    use crate::event_store::MemoryEvent;
233
234    #[test]
235    fn test_graph_analysis() {
236        let events = vec![MemoryEvent::allocate(0x1000, 64, 1)];
237        let view = MemoryView::from_events(events);
238        let analysis = GraphAnalysis::from_view(&view);
239        let stats = analysis.ownership_stats();
240        assert_eq!(stats.total_objects, 1);
241    }
242
243    #[test]
244    fn test_cycle_detection_empty() {
245        let events: Vec<MemoryEvent> = vec![];
246        let view = MemoryView::from_events(events);
247        let mut analysis = GraphAnalysis::from_view(&view);
248        let cycles = analysis.cycles();
249        assert_eq!(cycles.cycle_count, 0);
250    }
251
252    #[test]
253    fn test_relationship_stats() {
254        use crate::core::types::TrackKind;
255        use crate::snapshot::types::ActiveAllocation;
256
257        let buf1 = vec![0u8; 64];
258        let buf2 = vec![0u8; 128];
259        let ptr1 = buf1.as_ptr() as usize;
260        let ptr2 = buf2.as_ptr() as usize;
261
262        let allocs = vec![
263            ActiveAllocation {
264                ptr: Some(ptr1),
265                size: 64,
266                kind: TrackKind::HeapOwner {
267                    ptr: ptr1,
268                    size: 64,
269                },
270                allocated_at: 1000,
271                var_name: None,
272                type_name: None,
273                thread_id: 1,
274                call_stack_hash: None,
275            },
276            ActiveAllocation {
277                ptr: Some(ptr2),
278                size: 128,
279                kind: TrackKind::HeapOwner {
280                    ptr: ptr2,
281                    size: 128,
282                },
283                allocated_at: 1001,
284                var_name: None,
285                type_name: None,
286                thread_id: 1,
287                call_stack_hash: None,
288            },
289        ];
290
291        let snapshot = crate::snapshot::MemorySnapshot::new();
292        let mut snapshot = snapshot;
293        for alloc in allocs {
294            snapshot
295                .active_allocations
296                .insert(alloc.ptr.unwrap(), alloc);
297        }
298
299        let view = MemoryView::new(snapshot, vec![]);
300        let mut analysis = GraphAnalysis::from_view(&view);
301        let stats = analysis.relationship_stats();
302        // Stats should be computed from actual graph
303        // ownership_edges is usize, so it's always >= 0
304        let _ = stats.ownership_edges;
305
306        // Keep buffers alive until test ends
307        drop(buf1);
308        drop(buf2);
309    }
310}