Skip to main content

memscope_rs/analysis/relation_inference/
mod.rs

1//! Relation Inference - Memory relationship graph construction.
2//!
3//! Infers relationships between heap allocations by analyzing pointer patterns,
4//! content similarity, and allocation metadata.
5//!
6//! # Supported Relations
7//!
8//! | Relation | Meaning | Signal |
9//! |----------|---------|--------|
10//! | **Owner** | A holds a pointer into B | Pointer scan + RangeMap |
11//! | **Slice** | A is a sub-region of B | Pointer falls inside B (not at start) |
12//! | **Clone** | A is a copy of B | Same type/size/stack + content match |
13//!
14//! # Architecture
15//!
16//! ```text
17//! ActiveAllocation + MemoryView
18//!        │
19//!        ▼
20//!   UTI Engine (TypeGuess)
21//!        │
22//!        ▼
23//!   RangeMap (address → alloc index)
24//!        │
25//!        ▼
26//!   Relation Engine (Owner / Slice / Clone)
27//!        │
28//!        ▼
29//!   RelationGraph
30//! ```
31
32mod clone_detector;
33mod container_detector;
34mod graph_builder;
35mod pointer_scan;
36mod range_map;
37mod shared_detector;
38mod slice_detector;
39
40pub use clone_detector::{detect_clones, CloneConfig};
41pub use container_detector::{detect_containers, ContainerConfig};
42pub use graph_builder::{GraphBuilderConfig, RelationGraphBuilder};
43pub use pointer_scan::{detect_owner, InferenceRecord};
44pub use range_map::RangeMap;
45pub use shared_detector::detect_shared;
46pub use slice_detector::detect_slice;
47
48/// A relationship between two allocations in the graph.
49#[derive(Debug, Clone, PartialEq, Eq, Hash)]
50pub enum Relation {
51    /// A owns heap memory (HeapOwner → heap).
52    Owns,
53    /// A contains B (Container → HeapOwner).
54    Contains,
55    /// A and B share ownership (Arc/Rc).
56    Shares,
57    /// A is a view into a sub-region of B (e.g., &[T] into Vec).
58    Slice,
59    /// A is a copy of B (same type, size, stack, content).
60    Clone,
61    /// A is a later evolution of B (same variable, later allocation, e.g., Vec reallocation).
62    Evolution,
63}
64
65impl std::fmt::Display for Relation {
66    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67        match self {
68            Relation::Owns => write!(f, "Owns"),
69            Relation::Contains => write!(f, "Contains"),
70            Relation::Shares => write!(f, "Shares"),
71            Relation::Slice => write!(f, "Slice"),
72            Relation::Clone => write!(f, "Clone"),
73            Relation::Evolution => write!(f, "Evolution"),
74        }
75    }
76}
77
78/// An edge in the relation graph.
79#[derive(Debug, Clone)]
80pub struct RelationEdge {
81    /// Source allocation ID (index into the allocations list).
82    pub from: usize,
83    /// Target allocation ID.
84    pub to: usize,
85    /// The type of relationship.
86    pub relation: Relation,
87}
88
89/// A relation graph connecting allocations.
90#[derive(Debug, Default, Clone)]
91pub struct RelationGraph {
92    /// All edges in the graph.
93    pub edges: Vec<RelationEdge>,
94}
95
96impl RelationGraph {
97    /// Create a new empty relation graph.
98    pub fn new() -> Self {
99        Self::default()
100    }
101
102    /// Build relation graph from MemoryView.
103    ///
104    /// Creates a relation graph from the allocations in a MemoryView.
105    /// This is a convenience method for the unified analyzer API.
106    pub fn from_view(view: &crate::view::MemoryView) -> Self {
107        use crate::snapshot::types::ActiveAllocation;
108
109        let allocations: Vec<ActiveAllocation> =
110            view.allocations().iter().map(|a| (*a).clone()).collect();
111
112        RelationGraphBuilder::build(&allocations, None)
113    }
114
115    /// Add a single edge to the graph.
116    ///
117    /// Does not add duplicate edges (same from/to/relation).
118    pub fn add_edge(&mut self, from: usize, to: usize, relation: Relation) {
119        // Skip self-edges.
120        if from == to {
121            return;
122        }
123        // Skip duplicates.
124        let exists = self
125            .edges
126            .iter()
127            .any(|e| e.from == from && e.to == to && e.relation == relation);
128        if !exists {
129            self.edges.push(RelationEdge { from, to, relation });
130        }
131    }
132
133    /// Add multiple edges at once.
134    pub fn add_edges(&mut self, edges: Vec<RelationEdge>) {
135        for edge in edges {
136            self.add_edge(edge.from, edge.to, edge.relation);
137        }
138    }
139
140    /// Get all inbound edges to a given node.
141    pub fn get_inbound_edges(&self, node: usize) -> Vec<&RelationEdge> {
142        self.edges.iter().filter(|e| e.to == node).collect()
143    }
144
145    /// Get all outbound edges from a given node.
146    pub fn get_outbound_edges(&self, node: usize) -> Vec<&RelationEdge> {
147        self.edges.iter().filter(|e| e.from == node).collect()
148    }
149
150    /// Get the number of edges in the graph.
151    pub fn edge_count(&self) -> usize {
152        self.edges.len()
153    }
154
155    /// Get all unique node IDs referenced in the graph.
156    pub fn all_nodes(&self) -> Vec<usize> {
157        let mut nodes: Vec<usize> = self.edges.iter().flat_map(|e| [e.from, e.to]).collect();
158        nodes.sort();
159        nodes.dedup();
160        nodes
161    }
162
163    /// Detect cycles in the relation graph using DFS.
164    ///
165    /// Returns a list of cycle edges as `(from, to, relation)` tuples.
166    /// Empty result means no cycles detected.
167    pub fn detect_cycles(&self) -> Vec<(usize, usize, Relation)> {
168        if self.edges.is_empty() {
169            return Vec::new();
170        }
171
172        use crate::analysis::relationship_cycle_detector::detect_cycles_direct;
173
174        let relationships: Vec<(usize, usize)> =
175            self.edges.iter().map(|e| (e.from, e.to)).collect();
176
177        let cycle_indices = detect_cycles_direct(&relationships);
178
179        cycle_indices
180            .into_iter()
181            .filter_map(|(from_idx, to_idx)| {
182                self.edges.iter().find_map(|e| {
183                    if e.from == from_idx && e.to == to_idx {
184                        Some((e.from, e.to, e.relation.clone()))
185                    } else {
186                        None
187                    }
188                })
189            })
190            .collect()
191    }
192}
193
194mod tests {
195    #[allow(unused_imports)]
196    use crate::{Relation, RelationEdge, RelationGraph};
197
198    #[test]
199    fn test_graph_add_edge() {
200        let mut graph = RelationGraph::new();
201
202        graph.add_edge(0, 1, Relation::Owns);
203
204        assert_eq!(graph.edge_count(), 1);
205
206        assert_eq!(graph.edges[0].from, 0);
207
208        assert_eq!(graph.edges[0].to, 1);
209
210        assert_eq!(graph.edges[0].relation, Relation::Owns);
211    }
212
213    #[test]
214
215    fn test_graph_no_self_edges() {
216        let mut graph = RelationGraph::new();
217
218        graph.add_edge(0, 0, Relation::Owns);
219
220        assert_eq!(graph.edge_count(), 0);
221    }
222
223    #[test]
224
225    fn test_graph_no_duplicate_edges() {
226        let mut graph = RelationGraph::new();
227
228        graph.add_edge(0, 1, Relation::Owns);
229
230        graph.add_edge(0, 1, Relation::Owns);
231
232        assert_eq!(graph.edge_count(), 1);
233    }
234
235    #[test]
236
237    fn test_graph_different_relations_allowed() {
238        let mut graph = RelationGraph::new();
239
240        graph.add_edge(0, 1, Relation::Owns);
241
242        graph.add_edge(0, 1, Relation::Clone);
243
244        assert_eq!(graph.edge_count(), 2);
245    }
246
247    #[test]
248
249    fn test_graph_inbound_outbound_edges() {
250        let mut graph = RelationGraph::new();
251
252        graph.add_edge(0, 2, Relation::Owns);
253
254        graph.add_edge(1, 2, Relation::Owns);
255
256        graph.add_edge(2, 3, Relation::Slice);
257
258        let inbound_to_2 = graph.get_inbound_edges(2);
259
260        assert_eq!(inbound_to_2.len(), 2);
261
262        let outbound_from_2 = graph.get_outbound_edges(2);
263
264        assert_eq!(outbound_from_2.len(), 1);
265    }
266
267    #[test]
268
269    fn test_graph_all_nodes() {
270        let mut graph = RelationGraph::new();
271
272        graph.add_edge(3, 1, Relation::Owns);
273
274        graph.add_edge(1, 2, Relation::Slice);
275
276        let nodes = graph.all_nodes();
277
278        assert_eq!(nodes, vec![1, 2, 3]);
279    }
280
281    #[test]
282
283    fn test_graph_add_edges_batch() {
284        let mut graph = RelationGraph::new();
285
286        graph.add_edges(vec![
287            RelationEdge {
288                from: 0,
289
290                to: 1,
291
292                relation: Relation::Owns,
293            },
294            RelationEdge {
295                from: 1,
296
297                to: 2,
298
299                relation: Relation::Slice,
300            },
301        ]);
302
303        assert_eq!(graph.edge_count(), 2);
304    }
305
306    #[test]
307
308    fn test_graph_detect_cycles_none() {
309        // Linear chain: 0 → 1 → 2 → no cycles.
310
311        let mut graph = RelationGraph::new();
312
313        graph.add_edge(0, 1, Relation::Owns);
314
315        graph.add_edge(1, 2, Relation::Slice);
316
317        let cycles = graph.detect_cycles();
318
319        assert!(cycles.is_empty());
320    }
321
322    #[test]
323
324    fn test_graph_detect_cycles_simple() {
325        // Cycle: 0 → 1 → 0
326
327        let mut graph = RelationGraph::new();
328
329        graph.add_edge(0, 1, Relation::Owns);
330
331        graph.add_edge(1, 0, Relation::Owns);
332
333        let cycles = graph.detect_cycles();
334
335        // Should detect at least one cycle edge.
336
337        assert!(!cycles.is_empty());
338
339        // Both edges should be part of the cycle.
340
341        let edge_pairs: Vec<_> = cycles.iter().map(|(f, t, _)| (*f, *t)).collect();
342
343        assert!(edge_pairs.contains(&(0, 1)) || edge_pairs.contains(&(1, 0)));
344    }
345
346    #[test]
347
348    fn test_graph_detect_cycles_longer() {
349        // Cycle: 0 → 1 → 2 → 0
350
351        let mut graph = RelationGraph::new();
352
353        graph.add_edge(0, 1, Relation::Owns);
354
355        graph.add_edge(1, 2, Relation::Slice);
356
357        graph.add_edge(2, 0, Relation::Clone);
358
359        let cycles = graph.detect_cycles();
360
361        assert!(!cycles.is_empty());
362
363        // Verify the cycle includes the back-edge (2 → 0).
364
365        let has_back_edge = cycles.iter().any(|(f, t, _)| *f == 2 && *t == 0);
366
367        assert!(has_back_edge, "cycle should contain back-edge (2, 0)");
368    }
369
370    #[test]
371
372    fn test_graph_detect_cycles_empty() {
373        let graph = RelationGraph::new();
374
375        let cycles = graph.detect_cycles();
376
377        assert!(cycles.is_empty());
378    }
379
380    #[test]
381
382    fn test_graph_detect_cycles_self_loop_blocked() {
383        // Self-edges are blocked by add_edge, so no cycle possible.
384
385        let mut graph = RelationGraph::new();
386
387        graph.add_edge(0, 0, Relation::Owns);
388
389        assert_eq!(graph.edge_count(), 0);
390
391        let cycles = graph.detect_cycles();
392
393        assert!(cycles.is_empty());
394    }
395
396    #[test]
397
398    fn test_graph_detect_cycles_diamond() {
399        // Diamond: 0 → 1, 0 → 2, 1 → 3, 2 → 3, 3 → 0
400
401        // Cycles: 0→1→3→0 and 0→2→3→0
402
403        let mut graph = RelationGraph::new();
404
405        graph.add_edge(0, 1, Relation::Owns);
406
407        graph.add_edge(0, 2, Relation::Owns);
408
409        graph.add_edge(1, 3, Relation::Slice);
410
411        graph.add_edge(2, 3, Relation::Slice);
412
413        graph.add_edge(3, 0, Relation::Clone);
414
415        let cycles = graph.detect_cycles();
416
417        assert!(!cycles.is_empty());
418    }
419}