Skip to main content

memscope_rs/analysis/relation_inference/
graph_builder.rs

1//! Graph Builder — main pipeline for relation inference.
2//!
3//! Orchestrates HeapScanner, UTI Engine, and all relation detectors
4//! to produce a complete `RelationGraph` from active allocations.
5
6use tracing::debug;
7
8use crate::analysis::{
9    heap_scanner::{HeapScanner, ScanResult},
10    relation_inference::{
11        clone_detector::{detect_clones, CloneConfig},
12        container_detector::{detect_containers, ContainerConfig},
13        pointer_scan::{detect_owner, InferenceRecord},
14        shared_detector::detect_shared,
15        slice_detector::detect_slice,
16        RangeMap, RelationEdge, RelationGraph,
17    },
18    unsafe_inference::{MemoryView, OwnedMemoryView, TypeKind, UnsafeInferenceEngine},
19};
20
21use crate::snapshot::types::ActiveAllocation;
22
23/// Configuration for the relation graph builder.
24///
25/// This struct controls the behavior of the relation inference pipeline,
26/// allowing fine-tuning of detection thresholds and parameters.
27///
28/// # Example
29///
30/// ```ignore
31/// use memscope_rs::analysis::relation_inference::{
32///     GraphBuilderConfig, CloneConfig,
33/// };
34///
35/// // Default configuration
36/// let config = GraphBuilderConfig::default();
37///
38/// // Custom configuration for stricter clone detection
39/// let config = GraphBuilderConfig {
40///     clone_config: CloneConfig {
41///         min_similarity: 0.95,  // Require 95% content match
42///         max_time_diff_ns: 5_000_000,  // 5ms window
43///         ..Default::default()
44///     },
45/// };
46///
47/// let graph = RelationGraphBuilder::build(&allocations, Some(config));
48/// ```
49///
50/// # Fields
51///
52/// * `clone_config` - Controls clone detection behavior. See [`CloneConfig`]
53///   for details on similarity thresholds and time windows.
54#[derive(Debug, Clone, Default)]
55pub struct GraphBuilderConfig {
56    /// Configuration for clone detection.
57    ///
58    /// Controls how the system identifies cloned allocations based on:
59    /// - Content similarity ratio (`min_similarity`)
60    /// - Maximum time difference between allocations (`max_time_diff_ns`)
61    /// - Number of bytes to compare (`compare_bytes`)
62    ///
63    /// Default values are tuned for typical Rust workloads:
64    /// - 80% similarity threshold
65    /// - 10ms time window
66    /// - 64 bytes comparison
67    pub clone_config: CloneConfig,
68
69    /// Configuration for container relation detection.
70    ///
71    /// Controls how the system infers `Contains` relationships between
72    /// Container types (HashMap, BTreeMap) and HeapOwner types (Vec, Box).
73    ///
74    /// Default values use temporal locality with 1ms time window:
75    /// - 1ms time window
76    /// - 10x size ratio limit
77    /// - 5 candidate lookahead
78    pub container_config: ContainerConfig,
79}
80
81/// Builds a relation graph from active allocations.
82///
83/// # Pipeline
84///
85/// ```text
86/// ActiveAllocation[]
87///     │
88///     ▼
89/// HeapScanner → ScanResult[]
90///     │
91///     ▼
92/// UTI Engine → InferenceRecord[]
93///     │
94///     ▼
95/// RangeMap + Owner/Slice/Clone detectors
96///     │
97///     ▼
98/// RelationGraph
99/// ```
100pub struct RelationGraphBuilder;
101
102impl RelationGraphBuilder {
103    /// Build a relation graph from a list of active allocations.
104    ///
105    /// # Arguments
106    ///
107    /// * `allocations` - List of active allocations from a snapshot.
108    /// * `config` - Builder configuration (optional, uses defaults if None).
109    ///
110    /// # Returns
111    ///
112    /// A `RelationGraph` containing all inferred relationships.
113    pub fn build(
114        allocations: &[ActiveAllocation],
115        config: Option<GraphBuilderConfig>,
116    ) -> RelationGraph {
117        let config = config.unwrap_or_default();
118
119        if allocations.is_empty() {
120            return RelationGraph::new();
121        }
122
123        // Step 1: Scan heap memory for all allocations.
124        debug!(step = 1, "HeapScanner::scan");
125        let scan_results = HeapScanner::scan(allocations);
126        debug!(
127            step = 1,
128            scan_results = scan_results.len(),
129            "HeapScanner completed"
130        );
131
132        Self::build_with_scans(allocations, config, scan_results)
133    }
134
135    /// Reconstruct relationships from metadata without dereferencing recorded addresses.
136    /// A snapshot owns observations, not the allocations they once described. Live
137    /// bytes may have changed or been freed since capture, even in this process.
138    pub(crate) fn from_observations(allocations: &[ActiveAllocation]) -> RelationGraph {
139        Self::build_with_scans(allocations, GraphBuilderConfig::default(), Vec::new())
140    }
141
142    fn build_with_scans(
143        allocations: &[ActiveAllocation],
144        config: GraphBuilderConfig,
145        scan_results: Vec<ScanResult>,
146    ) -> RelationGraph {
147        // Create a mapping from (ptr, size) to scan result
148        debug!(step = 2, "Building scan_map");
149        let scan_map: std::collections::HashMap<(usize, usize), &ScanResult> = scan_results
150            .iter()
151            .map(|scan| ((scan.ptr, scan.size), scan))
152            .collect();
153        debug!(step = 2, "scan_map completed");
154
155        // Step 2: Run UTI Engine on each allocation.
156        // We ensure records has the same length as allocations to maintain index consistency.
157        debug!(step = 3, "Running UTI Engine");
158        let records: Vec<InferenceRecord> = allocations
159            .iter()
160            .enumerate()
161            .map(|(id, alloc)| {
162                let scan = scan_map.get(&(alloc.ptr.unwrap_or(0), alloc.size));
163
164                let (type_kind, confidence) =
165                    if let Some(memory) = scan.and_then(|s| s.memory.as_deref()) {
166                        let view = MemoryView::new(memory);
167                        let guess = UnsafeInferenceEngine::infer_single(&view, alloc.size);
168                        (guess.kind, guess.confidence)
169                    } else {
170                        (TypeKind::Unknown, 0)
171                    };
172
173                InferenceRecord {
174                    id,
175                    ptr: alloc.ptr.unwrap_or(0),
176                    size: alloc.size,
177                    memory: scan
178                        .and_then(|s| s.memory.as_deref())
179                        .map(OwnedMemoryView::copy_scan),
180                    type_kind,
181                    confidence,
182                    call_stack_hash: alloc.call_stack_hash,
183                    alloc_time: alloc.allocated_at,
184                    stack_ptr: alloc.stack_ptr,
185                }
186            })
187            .collect();
188        debug!(step = 3, records = records.len(), "UTI Engine completed");
189
190        // Step 3: Build RangeMap for address → allocation lookup.
191        debug!(step = 4, "Building RangeMap");
192        let range_map = RangeMap::new(allocations);
193        debug!(step = 4, "RangeMap completed");
194
195        // Step 4: Run relation detectors.
196        let mut graph = RelationGraph::new();
197
198        // Owner detection: scan each allocation's memory for pointers.
199        debug!(step = 5, "detect_owner");
200        for record in &records {
201            let edges = detect_owner(record, &range_map);
202            graph.add_edges(edges);
203        }
204        debug!(
205            step = 5,
206            edges = graph.edge_count(),
207            "detect_owner completed"
208        );
209
210        // Slice detection: check if each allocation points into another's interior.
211        debug!(step = 6, "detect_slice");
212        let slice_edges = detect_slice(&records, allocations, &range_map);
213        graph.add_edges(slice_edges);
214        debug!(
215            step = 6,
216            edges = graph.edge_count(),
217            "detect_slice completed"
218        );
219
220        // Clone detection: batch comparison by (type, size, stack_hash).
221        debug!(step = 7, "detect_clones");
222        let clone_edges = detect_clones(&records, &config.clone_config);
223        graph.add_edges(clone_edges);
224        debug!(
225            step = 7,
226            edges = graph.edge_count(),
227            "detect_clones completed"
228        );
229
230        // Container detection: infer Contains relationships using temporal locality.
231        debug!(step = 8, "detect_containers");
232        let container_edges = detect_containers(allocations, Some(config.container_config));
233        graph.add_edges(container_edges);
234        debug!(
235            step = 8,
236            edges = graph.edge_count(),
237            "detect_containers completed"
238        );
239
240        // Variable name evolution detection: infer relationships for allocations with same variable name
241        debug!(step = 9, "detect_variable_evolution");
242        let var_evolution_edges = detect_variable_evolution(allocations);
243        graph.add_edges(var_evolution_edges);
244        debug!(
245            step = 9,
246            edges = graph.edge_count(),
247            "detect_variable_evolution completed"
248        );
249
250        // Shared detection: find Arc/Rc shared ownership via Owner graph analysis.
251        debug!(step = 10, "detect_shared");
252        let shared_edges = detect_shared(&records, &graph.edges);
253        graph.add_edges(shared_edges);
254        debug!(
255            step = 10,
256            edges = graph.edge_count(),
257            "detect_shared completed"
258        );
259
260        debug!("RelationGraphBuilder all steps completed");
261        graph
262    }
263}
264
265/// Detect variable evolution relationships based on variable names.
266///
267/// For allocations with the same variable name, this function infers
268/// evolution relationships indicating that the same variable was
269/// tracked multiple times (e.g., growing HashMap, reallocated Vec).
270fn detect_variable_evolution(allocations: &[ActiveAllocation]) -> Vec<RelationEdge> {
271    use crate::analysis::relation_inference::Relation;
272    use std::collections::HashMap;
273
274    // Group allocations by variable name
275    let mut var_groups: HashMap<String, Vec<usize>> = HashMap::new();
276    for (i, alloc) in allocations.iter().enumerate() {
277        if let Some(ref var_name) = alloc.var_name {
278            if !var_name.is_empty() && var_name != "unknown" {
279                var_groups.entry(var_name.clone()).or_default().push(i);
280            }
281        }
282    }
283
284    let mut edges = Vec::new();
285
286    // For each variable with multiple allocations, create evolution edges
287    for indices in var_groups.values() {
288        if indices.len() < 2 {
289            continue;
290        }
291
292        // Sort by allocation time
293        let mut sorted_indices: Vec<(usize, u64)> = indices
294            .iter()
295            .map(|&i| (i, allocations[i].allocated_at))
296            .collect();
297        sorted_indices.sort_by_key(|&(_, time)| time);
298
299        // Create edges from earlier to later allocations
300        for window in sorted_indices.windows(2) {
301            let (from_idx, _) = window[0];
302            let (to_idx, _) = window[1];
303
304            edges.push(RelationEdge {
305                from: from_idx,
306                to: to_idx,
307                relation: Relation::Evolution,
308            });
309        }
310    }
311
312    edges
313}
314
315#[cfg(test)]
316mod tests {
317    use super::*;
318    use crate::analysis::relation_inference::Relation;
319    use crate::core::types::TrackKind;
320
321    fn make_alloc(ptr: usize, size: usize) -> ActiveAllocation {
322        ActiveAllocation {
323            ptr: Some(ptr),
324            size,
325            kind: TrackKind::HeapOwner { ptr, size },
326            allocated_at: 1000,
327            var_name: None,
328            type_name: None,
329            thread_id: 0,
330            call_stack_hash: None,
331            module_path: None,
332            stack_ptr: None,
333        }
334    }
335
336    #[test]
337    fn test_build_empty() {
338        let graph = RelationGraphBuilder::build(&[], None);
339        assert_eq!(graph.edge_count(), 0);
340    }
341
342    #[test]
343    fn test_build_basic_owner_relationship() {
344        // Use real heap allocations via Vec to ensure addresses are valid.
345        let buf1 = [0u8; 24];
346        let buf2 = vec![0u8; 1024];
347        let ptr1 = buf1.as_ptr() as usize;
348        let ptr2 = buf2.as_ptr() as usize;
349
350        let allocs = vec![make_alloc(ptr1, 24), make_alloc(ptr2, 1024)];
351        let graph = RelationGraphBuilder::build(&allocs, None);
352
353        // Graph should be built without panicking.
354        // The content is all zeros so no Owner relationships will be found.
355        assert_eq!(graph.edge_count(), 0);
356    }
357
358    #[test]
359    fn test_build_with_real_vec_metadata() {
360        // Create a real Vec-like metadata pattern: [ptr, len, cap]
361        let inner = vec![42u8; 256];
362        let ptr = inner.as_ptr() as usize;
363        let len = inner.len();
364        let cap = inner.capacity();
365
366        let mut metadata = [0u8; 24];
367        metadata[0..8].copy_from_slice(&ptr.to_le_bytes());
368        metadata[8..16].copy_from_slice(&len.to_le_bytes());
369        metadata[16..24].copy_from_slice(&cap.to_le_bytes());
370
371        let meta_ptr = metadata.as_ptr() as usize;
372        let inner_ptr = inner.as_ptr() as usize;
373
374        let allocs = vec![make_alloc(meta_ptr, 24), make_alloc(inner_ptr, 256)];
375        let graph = RelationGraphBuilder::build(&allocs, None);
376
377        // Should not crash. Actual edge depends on whether metadata is readable
378        // at meta_ptr (it's on the heap, so should be).
379        assert!(graph.edge_count() <= 2);
380    }
381
382    #[test]
383    fn test_build_single_allocation() {
384        let allocs = vec![make_alloc(0x1000, 64)];
385        let graph = RelationGraphBuilder::build(&allocs, None);
386
387        // Single allocation should not produce any relationships.
388        assert_eq!(graph.edge_count(), 0);
389    }
390
391    #[test]
392    fn test_builder_with_default_config() {
393        let allocs = vec![make_alloc(0x1000, 64)];
394        let graph = RelationGraphBuilder::build(&allocs, Some(GraphBuilderConfig::default()));
395        assert_eq!(graph.edge_count(), 0);
396    }
397
398    #[test]
399    fn test_graph_node_count() {
400        let mut graph = RelationGraph::new();
401        graph.add_edge(0, 1, Relation::Owns);
402        graph.add_edge(1, 2, Relation::Slice);
403
404        let nodes = graph.all_nodes();
405        assert_eq!(nodes, vec![0, 1, 2]);
406    }
407}