memscope_rs/analysis/
circular_reference.rs

1//! Circular reference detection for smart pointers
2//!
3//! This module provides functionality to detect circular references in Rc/Arc
4//! smart pointers that can lead to memory leaks.
5
6use crate::core::types::{AllocationInfo, SmartPointerInfo, SmartPointerType};
7use serde::{Deserialize, Serialize};
8use std::collections::{HashMap, HashSet};
9
10/// Represents a detected circular reference
11#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
12pub struct CircularReference {
13    /// The cycle path showing the circular reference chain
14    pub cycle_path: Vec<CircularReferenceNode>,
15
16    /// Suggested positions where Weak references should be used to break the cycle
17    pub suggested_weak_positions: Vec<usize>,
18
19    /// Estimated memory that would be leaked due to this cycle
20    pub estimated_leaked_memory: usize,
21
22    /// Severity level of this circular reference
23    pub severity: CircularReferenceSeverity,
24
25    /// Type of circular reference detected
26    pub cycle_type: CircularReferenceType,
27}
28
29/// Node in a circular reference path
30#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
31pub struct CircularReferenceNode {
32    /// Pointer address of this node
33    pub ptr: usize,
34
35    /// Data pointer this node points to
36    pub data_ptr: usize,
37
38    /// Variable name if available
39    pub var_name: Option<String>,
40
41    /// Type name of the smart pointer
42    pub type_name: Option<String>,
43
44    /// Smart pointer type
45    pub pointer_type: SmartPointerType,
46
47    /// Current reference count
48    pub ref_count: usize,
49}
50
51/// Severity levels for circular references
52#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
53pub enum CircularReferenceSeverity {
54    /// Low severity - small memory impact
55    Low,
56    /// Medium severity - moderate memory impact
57    Medium,
58    /// High severity - significant memory impact
59    High,
60    /// Critical severity - large memory impact or complex cycle
61    Critical,
62}
63
64/// Types of circular references
65#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
66pub enum CircularReferenceType {
67    /// Simple two-node cycle (A -> B -> A)
68    Simple,
69    /// Complex multi-node cycle
70    Complex,
71    /// Self-referencing cycle (A -> A)
72    SelfReference,
73    /// Nested cycles (cycles within cycles)
74    Nested,
75}
76
77/// Analysis result for circular reference detection
78#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct CircularReferenceAnalysis {
80    /// All detected circular references
81    pub circular_references: Vec<CircularReference>,
82
83    /// Total number of smart pointers analyzed
84    pub total_smart_pointers: usize,
85
86    /// Number of smart pointers involved in cycles
87    pub pointers_in_cycles: usize,
88
89    /// Total estimated leaked memory
90    pub total_leaked_memory: usize,
91
92    /// Analysis statistics
93    pub statistics: CircularReferenceStatistics,
94}
95
96/// Statistics for circular reference analysis
97#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct CircularReferenceStatistics {
99    /// Count by severity
100    pub by_severity: HashMap<String, usize>,
101
102    /// Count by type
103    pub by_type: HashMap<String, usize>,
104
105    /// Count by smart pointer type
106    pub by_pointer_type: HashMap<String, usize>,
107
108    /// Average cycle length
109    pub average_cycle_length: f64,
110
111    /// Largest cycle size
112    pub largest_cycle_size: usize,
113}
114
115/// Graph representation for circular reference detection
116#[derive(Debug)]
117struct ReferenceGraph {
118    /// Adjacency list: ptr -> list of data_ptrs it references
119    adjacency: HashMap<usize, Vec<usize>>,
120
121    /// Reverse mapping: data_ptr -> list of ptrs that reference it
122    reverse_refs: HashMap<usize, Vec<usize>>,
123
124    /// Smart pointer information by ptr
125    smart_pointers: HashMap<usize, SmartPointerInfo>,
126
127    /// Allocation information by ptr
128    allocations: HashMap<usize, AllocationInfo>,
129}
130
131impl ReferenceGraph {
132    /// Create a new reference graph from allocations
133    fn new(allocations: &[AllocationInfo]) -> Self {
134        let mut graph = ReferenceGraph {
135            adjacency: HashMap::new(),
136            reverse_refs: HashMap::new(),
137            smart_pointers: HashMap::new(),
138            allocations: HashMap::new(),
139        };
140
141        // Build the graph from smart pointer allocations
142        for allocation in allocations {
143            if let Some(ref smart_info) = allocation.smart_pointer_info {
144                // Skip weak references for cycle detection (they don't create strong cycles)
145                if smart_info.is_weak_reference {
146                    continue;
147                }
148
149                graph
150                    .smart_pointers
151                    .insert(allocation.ptr, smart_info.clone());
152                graph.allocations.insert(allocation.ptr, allocation.clone());
153
154                // Add edge from this pointer to its data
155                graph
156                    .adjacency
157                    .entry(allocation.ptr)
158                    .or_insert_with(Vec::new);
159
160                // Add reverse reference from data to this pointer
161                graph
162                    .reverse_refs
163                    .entry(smart_info.data_ptr)
164                    .or_insert_with(Vec::new)
165                    .push(allocation.ptr);
166
167                // Add edges to cloned pointers (they share the same data)
168                for &clone_ptr in &smart_info.clones {
169                    graph
170                        .adjacency
171                        .entry(allocation.ptr)
172                        .or_insert_with(Vec::new)
173                        .push(clone_ptr);
174                }
175
176                // Add edge from cloned_from if it exists
177                if let Some(source_ptr) = smart_info.cloned_from {
178                    graph
179                        .adjacency
180                        .entry(source_ptr)
181                        .or_insert_with(Vec::new)
182                        .push(allocation.ptr);
183                }
184            }
185        }
186
187        graph
188    }
189
190    /// Detect all circular references in the graph
191    fn detect_cycles(&self) -> Vec<Vec<usize>> {
192        let mut cycles = Vec::new();
193        let mut visited = HashSet::new();
194        let mut rec_stack = HashSet::new();
195        let mut path = Vec::new();
196
197        for &ptr in self.smart_pointers.keys() {
198            if !visited.contains(&ptr) {
199                self.dfs_detect_cycles(ptr, &mut visited, &mut rec_stack, &mut path, &mut cycles);
200            }
201        }
202
203        cycles
204    }
205
206    /// Depth-first search to detect cycles
207    fn dfs_detect_cycles(
208        &self,
209        ptr: usize,
210        visited: &mut HashSet<usize>,
211        rec_stack: &mut HashSet<usize>,
212        path: &mut Vec<usize>,
213        cycles: &mut Vec<Vec<usize>>,
214    ) {
215        visited.insert(ptr);
216        rec_stack.insert(ptr);
217        path.push(ptr);
218
219        if let Some(neighbors) = self.adjacency.get(&ptr) {
220            for &neighbor in neighbors {
221                if !visited.contains(&neighbor) {
222                    self.dfs_detect_cycles(neighbor, visited, rec_stack, path, cycles);
223                } else if rec_stack.contains(&neighbor) {
224                    // Found a cycle - extract the cycle path
225                    if let Some(cycle_start) = path.iter().position(|&x| x == neighbor) {
226                        let cycle = path[cycle_start..].to_vec();
227                        cycles.push(cycle);
228                    }
229                }
230            }
231        }
232
233        path.pop();
234        rec_stack.remove(&ptr);
235    }
236}
237
238/// Detect circular references in smart pointer allocations
239pub fn detect_circular_references(allocations: &[AllocationInfo]) -> CircularReferenceAnalysis {
240    let graph = ReferenceGraph::new(allocations);
241    let raw_cycles = graph.detect_cycles();
242
243    let mut circular_references = Vec::new();
244    let mut total_leaked_memory = 0;
245    let mut pointers_in_cycles = HashSet::new();
246
247    // Process each detected cycle
248    for cycle_path in raw_cycles {
249        if cycle_path.len() < 2 {
250            continue; // Skip trivial cycles
251        }
252
253        let circular_ref = analyze_cycle(&cycle_path, &graph);
254        total_leaked_memory += circular_ref.estimated_leaked_memory;
255
256        for node in &circular_ref.cycle_path {
257            pointers_in_cycles.insert(node.ptr);
258        }
259
260        circular_references.push(circular_ref);
261    }
262
263    // Generate statistics
264    let statistics = generate_statistics(&circular_references);
265
266    CircularReferenceAnalysis {
267        circular_references,
268        total_smart_pointers: graph.smart_pointers.len(),
269        pointers_in_cycles: pointers_in_cycles.len(),
270        total_leaked_memory,
271        statistics,
272    }
273}
274
275/// Analyze a single cycle to create a CircularReference
276fn analyze_cycle(cycle_path: &[usize], graph: &ReferenceGraph) -> CircularReference {
277    let mut nodes = Vec::new();
278    let mut total_memory = 0;
279
280    // Build cycle nodes
281    for &ptr in cycle_path {
282        if let (Some(smart_info), Some(allocation)) =
283            (graph.smart_pointers.get(&ptr), graph.allocations.get(&ptr))
284        {
285            let node = CircularReferenceNode {
286                ptr,
287                data_ptr: smart_info.data_ptr,
288                var_name: allocation.var_name.clone(),
289                type_name: allocation.type_name.clone(),
290                pointer_type: smart_info.pointer_type.clone(),
291                ref_count: smart_info
292                    .latest_ref_counts()
293                    .map(|snapshot| snapshot.strong_count)
294                    .unwrap_or(1),
295            };
296
297            total_memory += allocation.size;
298            nodes.push(node);
299        }
300    }
301
302    // Determine cycle type
303    let cycle_type = if cycle_path.len() == 1 {
304        CircularReferenceType::SelfReference
305    } else if cycle_path.len() == 2 {
306        CircularReferenceType::Simple
307    } else {
308        CircularReferenceType::Complex
309    };
310
311    // Determine severity based on memory impact and cycle complexity
312    let severity = if total_memory > 1024 * 1024 {
313        // > 1MB
314        CircularReferenceSeverity::Critical
315    } else if total_memory > 64 * 1024 {
316        // > 64KB
317        CircularReferenceSeverity::High
318    } else if total_memory > 4 * 1024 {
319        // > 4KB
320        CircularReferenceSeverity::Medium
321    } else {
322        CircularReferenceSeverity::Low
323    };
324
325    // Suggest weak reference positions (break the cycle at the longest-lived reference)
326    let suggested_weak_positions = suggest_weak_positions(&nodes);
327
328    CircularReference {
329        cycle_path: nodes,
330        suggested_weak_positions,
331        estimated_leaked_memory: total_memory,
332        severity,
333        cycle_type,
334    }
335}
336
337/// Suggest positions where weak references should be used to break cycles
338fn suggest_weak_positions(nodes: &[CircularReferenceNode]) -> Vec<usize> {
339    // Simple heuristic: suggest breaking at the node with the highest reference count
340    // (likely to be the most shared and least critical to make weak)
341    if let Some((index, _)) = nodes
342        .iter()
343        .enumerate()
344        .max_by_key(|(_, node)| node.ref_count)
345    {
346        vec![index]
347    } else {
348        vec![0] // Fallback to first position
349    }
350}
351
352/// Generate statistics for the analysis
353fn generate_statistics(circular_references: &[CircularReference]) -> CircularReferenceStatistics {
354    let mut by_severity = HashMap::new();
355    let mut by_type = HashMap::new();
356    let mut by_pointer_type = HashMap::new();
357    let mut total_cycle_length = 0;
358    let mut largest_cycle_size = 0;
359
360    for circular_ref in circular_references {
361        // Count by severity
362        let severity_key = format!("{:?}", circular_ref.severity);
363        *by_severity.entry(severity_key).or_insert(0) += 1;
364
365        // Count by type
366        let type_key = format!("{:?}", circular_ref.cycle_type);
367        *by_type.entry(type_key).or_insert(0) += 1;
368
369        // Count by pointer type
370        for node in &circular_ref.cycle_path {
371            let pointer_type_key = format!("{:?}", node.pointer_type);
372            *by_pointer_type.entry(pointer_type_key).or_insert(0) += 1;
373        }
374
375        // Track cycle sizes
376        let cycle_length = circular_ref.cycle_path.len();
377        total_cycle_length += cycle_length;
378        largest_cycle_size = largest_cycle_size.max(cycle_length);
379    }
380
381    let average_cycle_length = if circular_references.is_empty() {
382        0.0
383    } else {
384        total_cycle_length as f64 / circular_references.len() as f64
385    };
386
387    CircularReferenceStatistics {
388        by_severity,
389        by_type,
390        by_pointer_type,
391        average_cycle_length,
392        largest_cycle_size,
393    }
394}