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                Relation::ArcClone => clone_edges += 1,
132                Relation::RcClone => clone_edges += 1,
133                Relation::ImmutableBorrow => slice_edges += 1,
134                Relation::MutableBorrow => ownership_edges += 1,
135            }
136        }
137
138        RelationshipStats {
139            ownership_edges,
140            contains_edges,
141            shares_edges,
142            slice_edges,
143            clone_edges,
144        }
145    }
146
147    /// Get all relationship edges for visualization.
148    ///
149    /// Returns a list of relationship edges with full allocation info.
150    /// Limited to 500 edges to avoid performance issues.
151    pub fn relationships(&mut self) -> Vec<RelationshipEdge> {
152        // Clone allocations to avoid borrow conflict with relation_graph()
153        let allocations: Vec<crate::snapshot::types::ActiveAllocation> = self
154            .view
155            .allocations()
156            .iter()
157            .map(|a| (*a).clone())
158            .collect();
159
160        let relation_graph = self.relation_graph();
161        let edges = &relation_graph.edges;
162
163        debug!("Building relationships from {} edges", edges.len());
164
165        let mut relationships: Vec<RelationshipEdge> = edges
166            .iter()
167            .filter_map(|edge| {
168                let from_alloc = allocations.get(edge.from);
169                let to_alloc = allocations.get(edge.to);
170
171                let from_ptr = from_alloc.and_then(|a| a.ptr).unwrap_or(edge.from);
172                let to_ptr = to_alloc.and_then(|a| a.ptr).unwrap_or(edge.to);
173
174                // Skip if both are virtual/container pointers
175                if from_ptr == 0 && to_ptr == 0 {
176                    return None;
177                }
178
179                Some(RelationshipEdge {
180                    from_ptr,
181                    to_ptr,
182                    from_var_name: from_alloc.and_then(|a| a.var_name.clone()),
183                    to_var_name: to_alloc.and_then(|a| a.var_name.clone()),
184                    from_type_name: from_alloc.and_then(|a| a.type_name.clone()),
185                    to_type_name: to_alloc.and_then(|a| a.type_name.clone()),
186                    relation: edge.relation.clone(),
187                    is_container_source: from_ptr == 0,
188                    is_container_target: to_ptr == 0,
189                })
190            })
191            .collect();
192
193        // Limit relationships to avoid performance issues
194        if relationships.len() > 500 {
195            warn!(
196                "Relationship count ({}) exceeds limit, truncating to 500",
197                relationships.len()
198            );
199            relationships.truncate(500);
200        }
201
202        // Integrate borrow edges from BorrowAnalyzer
203        let borrow_analyzer = crate::analysis::borrow_analysis::get_global_borrow_analyzer();
204        let borrow_history = borrow_analyzer.get_borrow_history();
205
206        for event in &borrow_history {
207            let borrow_relation = match event.borrow_info.borrow_type {
208                crate::analysis::borrow_analysis::BorrowType::Immutable => {
209                    crate::analysis::relation_inference::Relation::ImmutableBorrow
210                }
211                crate::analysis::borrow_analysis::BorrowType::Mutable => {
212                    crate::analysis::relation_inference::Relation::MutableBorrow
213                }
214                crate::analysis::borrow_analysis::BorrowType::Shared => {
215                    crate::analysis::relation_inference::Relation::Shares
216                }
217                crate::analysis::borrow_analysis::BorrowType::Weak => {
218                    crate::analysis::relation_inference::Relation::Contains
219                }
220            };
221
222            relationships.push(RelationshipEdge {
223                from_ptr: event.borrow_info.ptr,
224                to_ptr: event.borrow_info.ptr,
225                from_var_name: Some(event.borrow_info.var_name.clone()),
226                to_var_name: Some(format!("borrow_{:?}", event.borrow_info.id)),
227                from_type_name: Some(format!("{:?}", event.borrow_info.borrow_type)),
228                to_type_name: Some(format!("{:?}", event.borrow_info.borrow_type)),
229                relation: borrow_relation,
230                is_container_source: false,
231                is_container_target: false,
232            });
233        }
234
235        debug!("Returning {} relationships", relationships.len());
236        relationships
237    }
238}
239
240/// Ownership statistics.
241#[derive(Debug, Clone)]
242pub struct OwnershipStats {
243    /// Total number of objects
244    pub total_objects: usize,
245    /// Total bytes owned
246    pub total_bytes: usize,
247    /// Number of unique types
248    pub unique_types: usize,
249}
250
251/// Relationship statistics computed from actual graph analysis.
252#[derive(Debug, Clone, Default)]
253pub struct RelationshipStats {
254    /// Number of ownership edges (A owns heap memory)
255    pub ownership_edges: usize,
256    /// Number of contains edges (Container → HeapOwner)
257    pub contains_edges: usize,
258    /// Number of shares edges (Arc/Rc)
259    pub shares_edges: usize,
260    /// Number of slice edges (view into sub-region)
261    pub slice_edges: usize,
262    /// Number of clone edges (copy of another allocation)
263    pub clone_edges: usize,
264}
265
266#[cfg(test)]
267mod tests {
268    use super::*;
269    use crate::event_store::MemoryEvent;
270
271    #[test]
272    fn test_graph_analysis() {
273        let events = vec![MemoryEvent::allocate(0x1000, 64, 1)];
274        let view = MemoryView::from_events(events);
275        let analysis = GraphAnalysis::from_view(&view);
276        let stats = analysis.ownership_stats();
277        assert_eq!(stats.total_objects, 1);
278    }
279
280    #[test]
281    fn test_cycle_detection_empty() {
282        let events: Vec<MemoryEvent> = vec![];
283        let view = MemoryView::from_events(events);
284        let mut analysis = GraphAnalysis::from_view(&view);
285        let cycles = analysis.cycles();
286        assert_eq!(cycles.cycle_count, 0);
287    }
288
289    #[test]
290    fn test_relationship_stats() {
291        use crate::core::types::TrackKind;
292        use crate::snapshot::types::ActiveAllocation;
293
294        let buf1 = vec![0u8; 64];
295        let buf2 = vec![0u8; 128];
296        let ptr1 = buf1.as_ptr() as usize;
297        let ptr2 = buf2.as_ptr() as usize;
298
299        let allocs = vec![
300            ActiveAllocation {
301                ptr: Some(ptr1),
302                size: 64,
303                kind: TrackKind::HeapOwner {
304                    ptr: ptr1,
305                    size: 64,
306                },
307                allocated_at: 1000,
308                var_name: None,
309                type_name: None,
310                thread_id: 1,
311                call_stack_hash: None,
312                module_path: None,
313                stack_ptr: None,
314            },
315            ActiveAllocation {
316                ptr: Some(ptr2),
317                size: 128,
318                kind: TrackKind::HeapOwner {
319                    ptr: ptr2,
320                    size: 128,
321                },
322                allocated_at: 1001,
323                var_name: None,
324                type_name: None,
325                thread_id: 1,
326                call_stack_hash: None,
327                module_path: None,
328                stack_ptr: None,
329            },
330        ];
331
332        let snapshot = crate::snapshot::MemorySnapshot::new();
333        let mut snapshot = snapshot;
334        for alloc in allocs {
335            snapshot
336                .active_allocations
337                .insert(alloc.ptr.unwrap(), alloc);
338        }
339
340        let view = MemoryView::new(snapshot, vec![]);
341        let mut analysis = GraphAnalysis::from_view(&view);
342        let stats = analysis.relationship_stats();
343        // Stats should be computed from actual graph
344        // ownership_edges is usize, so it's always >= 0
345        let _ = stats.ownership_edges;
346
347        // Keep buffers alive until test ends
348        drop(buf1);
349        drop(buf2);
350    }
351}