ferric_ai/parser/
tree.rs

1// Tree structures for flamegraph analysis
2
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use uuid::Uuid;
6
7use super::nodes::{ParserNode, RgbColor};
8
9/// A hierarchical tree structure containing TreeNodes with UUID-based relationships
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct Tree {
12    /// All nodes in the tree indexed by their UUID
13    pub nodes: HashMap<Uuid, TreeNode>,
14    /// Root node UUIDs (nodes with no parent)
15    pub roots: Vec<Uuid>,
16    /// Total samples from the flamegraph SVG for percentage calculations
17    pub total_samples: u64,
18}
19
20/// A node in the call tree with UUID-based parent/child relationships
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct TreeNode {
23    /// Unique identifier for this node
24    pub id: Uuid,
25    /// Parent node UUID (None for root nodes)
26    pub parent: Option<Uuid>,
27    /// Child node UUIDs (direct children only)
28    pub children: Vec<Uuid>,
29    /// Descendant node UUIDs (all levels below)
30    pub descendants: Vec<Uuid>,
31
32    // === Core Function Identity ===
33    /// Function name (cleaned and demangled)
34    pub function_name: String,
35    /// Raw function signature from flamegraph title
36    pub raw_signature: String,
37
38    // === Performance Metrics ===
39    /// CPU percentage this function consumes (including children)
40    pub cpu_percent: f64,
41    /// Total sample count for this function (including children)
42    pub sample_count: u64,
43    /// Total sample count for this function (excluding children)
44    pub self_sample_count: u64,
45
46    // === Visual/Color Information ===
47    /// RGB color from flamegraph (indicates heat level)
48    pub color: Option<RgbColor>,
49
50    // === Call Function Context ===
51    pub source_library: Option<String>,
52    pub is_alloc: bool,
53}
54
55impl Tree {
56    /// Create a new Tree from a vector of ParserNodes
57    pub fn from_parser_nodes(parser_nodes: Vec<ParserNode>, _total_samples: u64) -> Self {
58        let mut tree = Tree {
59            nodes: HashMap::new(),
60            roots: Vec::new(),
61            total_samples: 0, // Will be calculated from root nodes
62        };
63
64        // First pass: Create all TreeNodes with UUIDs
65        let mut node_map: HashMap<Uuid,String> = HashMap::new();
66
67        for parser_node in &parser_nodes {
68            let id = Uuid::new_v4();
69            let tree_node = TreeNode {
70                id,
71                parent: None, // Will be set in second pass
72                children: Vec::new(), // Will be populated in second pass
73                descendants: Vec::new(), // Will be populated in third pass
74                function_name: parser_node.function_name.clone(),
75                raw_signature: parser_node.raw_signature.clone(),
76                cpu_percent: parser_node.cpu_percent,
77                sample_count: parser_node.sample_count,
78                self_sample_count: 0, // PLACEHOLDER
79                color: parser_node.color.clone(),
80                source_library: None, // PLACEHOLDER
81                is_alloc: false, // PLACEHOLDER
82            };
83
84            node_map.insert(id, parser_node.unique_key.clone());
85            tree.nodes.insert(id, tree_node);
86        }
87
88        // Second pass: Build parent-child relationships using positioning
89        tree.build_relationships(parser_nodes, &node_map);
90
91        // Third pass: Calculate self_sample_count for all nodes
92        tree.calculate_self_sample_counts();
93
94        // Fourth pass: Calculate total_samples from root nodes
95        tree.calculate_total_samples();
96
97        tree
98    }
99
100    /// Build parent-child relationships based on flamegraph positioning
101    fn build_relationships(&mut self, parser_nodes: Vec<ParserNode>, node_map: &HashMap<Uuid, String>) {
102        // Create reverse lookup map from unique_key to UUID
103        let key_to_uuid: HashMap<&String, Uuid> = node_map.iter().map(|(uuid, key)| (key, *uuid)).collect();
104
105        // Group nodes by Y level for efficient processing
106        let mut nodes_by_y: HashMap<u32, Vec<&ParserNode>> = HashMap::new();
107        for node in &parser_nodes {
108            let y_level = (node.y_position as u32 / 16) * 16; // Round to nearest 16px level
109            nodes_by_y.entry(y_level).or_default().push(node);
110        }
111
112        let mut y_levels: Vec<u32> = nodes_by_y.keys().copied().collect();
113        y_levels.sort();
114
115        log::debug!("Found {} Y levels: {:?}", y_levels.len(), y_levels);
116
117        // Track which nodes have been assigned parents
118        let mut assigned_as_child = std::collections::HashSet::new();
119
120        // Process each level, finding immediate parent relationships
121        for (level_idx, &current_y) in y_levels.iter().enumerate() {
122            let empty_vec = vec![];
123            let current_nodes = nodes_by_y.get(&current_y).unwrap_or(&empty_vec);
124
125            // Look for immediate parent level (next Y level down)
126            if let Some(&parent_y) = y_levels.get(level_idx + 1) {
127                let empty_parent_vec = vec![];
128                let parent_nodes = nodes_by_y.get(&parent_y).unwrap_or(&empty_parent_vec);
129
130                // For each node at current level, find its immediate parent
131                for child_node in current_nodes {
132                    let child_id = match key_to_uuid.get(&child_node.unique_key) {
133                        Some(&id) => id,
134                        None => continue,
135                    };
136
137                    // Skip if already has a parent
138                    if assigned_as_child.contains(&child_id) {
139                        continue;
140                    }
141
142                    // Find parent that contains this child's X range
143                    for parent_node in parent_nodes {
144                        if self.x_ranges_overlap(parent_node, child_node) {
145                            let parent_id = match key_to_uuid.get(&parent_node.unique_key) {
146                                Some(&id) => id,
147                                None => continue,
148                            };
149
150                            // Set parent-child relationship
151                            if let Some(child_tree_node) = self.nodes.get_mut(&child_id) {
152                                child_tree_node.parent = Some(parent_id);
153                                assigned_as_child.insert(child_id);
154                            }
155
156                            // Add child to parent's children list
157                            if let Some(parent_tree_node) = self.nodes.get_mut(&parent_id) {
158                                parent_tree_node.children.push(child_id);
159                            }
160
161                            break; // Found immediate parent, move to next child
162                        }
163                    }
164                }
165            }
166        }
167
168        // Identify root nodes (those without parents)
169        self.roots.clear();
170        for (&node_id, node) in &self.nodes {
171            if !assigned_as_child.contains(&node_id) {
172                self.roots.push(node_id);
173                log::debug!("Root: {}", node.function_name);
174            }
175        }
176
177        // Build descendants lists (all children recursively)
178        self.build_descendants();
179
180        log::debug!("Relationships complete: {} roots, {} nodes with children",
181            self.roots.len(),
182            self.nodes.values().filter(|n| !n.children.is_empty()).count());
183    }
184
185    /// Recursively build descendants list for all nodes
186    fn build_descendants(&mut self) {
187        // Create a temporary map to avoid borrow checker issues
188        let mut descendants_map: HashMap<Uuid, Vec<Uuid>> = HashMap::new();
189
190        for &node_id in self.nodes.keys() {
191            let descendants = self.collect_all_descendants(node_id);
192            descendants_map.insert(node_id, descendants);
193        }
194
195        // Update the nodes with their descendants
196        for (node_id, descendants) in descendants_map {
197            if let Some(node) = self.nodes.get_mut(&node_id) {
198                node.descendants = descendants;
199            }
200        }
201    }
202
203    /// Recursively collect all descendants of a node
204    fn collect_all_descendants(&self, node_id: Uuid) -> Vec<Uuid> {
205        let mut descendants = Vec::new();
206
207        if let Some(node) = self.nodes.get(&node_id) {
208            for &child_id in &node.children {
209                descendants.push(child_id);
210                // Recursively add grandchildren, great-grandchildren, etc.
211                descendants.extend(self.collect_all_descendants(child_id));
212            }
213        }
214
215        descendants
216    }
217
218    /// Calculate self_sample_count for all nodes in the tree
219    /// self_sample_count = inclusive sample_count - sum of children's inclusive sample_counts
220    pub fn calculate_self_sample_counts(&mut self) {
221        // Process all root nodes recursively
222        for &root_id in self.roots.clone().iter() {
223            self.calculate_self_sample_counts_recursive(root_id);
224        }
225    }
226
227    /// Calculate total_samples from the sum of all root nodes' sample_count
228    /// This reflects the actual total samples in the flamegraph more accurately
229    /// than relying on the SVG total_samples attribute
230    pub fn calculate_total_samples(&mut self) {
231        self.total_samples = self.roots
232            .iter()
233            .filter_map(|&root_id| self.nodes.get(&root_id))
234            .map(|node| node.sample_count)
235            .sum();
236
237        log::debug!("Calculated total_samples from {} root nodes: {}",
238                   self.roots.len(), self.total_samples);
239    }
240
241    /// Recursively calculate self_sample_count starting from a node
242    fn calculate_self_sample_counts_recursive(&mut self, node_id: Uuid) {
243        // First, recursively calculate for all children (bottom-up approach)
244        let child_ids: Vec<Uuid> = if let Some(node) = self.nodes.get(&node_id) {
245            node.children.clone()
246        } else {
247            return;
248        };
249
250        for child_id in child_ids {
251            self.calculate_self_sample_counts_recursive(child_id);
252        }
253
254        // Now calculate self_sample_count for this node
255        if let Some(node) = self.nodes.get(&node_id) {
256            let inclusive_count = node.sample_count;
257
258            // Sum up all children's inclusive sample counts
259            let children_inclusive_sum: u64 = node.children
260                .iter()
261                .filter_map(|&child_id| self.nodes.get(&child_id))
262                .map(|child| child.sample_count)
263                .sum();
264
265            let self_count = inclusive_count.saturating_sub(children_inclusive_sum);
266
267            // Update the node's self_sample_count
268            if let Some(node_mut) = self.nodes.get_mut(&node_id) {
269                node_mut.self_sample_count = self_count;
270            }
271        }
272    }
273
274    /// Check if two nodes have overlapping X coordinate ranges
275    fn x_ranges_overlap(&self, parent: &ParserNode, child: &ParserNode) -> bool {
276        let parent_start = parent.x_position;
277        let parent_end = parent.x_position + parent.width;
278        let child_start = child.x_position;
279        let child_end = child.x_position + child.width;
280
281        // Check for overlap: child must be within parent's X range
282        child_start >= parent_start && child_end <= parent_end
283    }
284
285
286    /// Get a node by UUID
287    pub fn get_node(&self, id: Uuid) -> Option<&TreeNode> {
288        self.nodes.get(&id)
289    }
290
291    /// Get a mutable node by UUID
292    pub fn get_node_mut(&mut self, id: Uuid) -> Option<&mut TreeNode> {
293        self.nodes.get_mut(&id)
294    }
295
296    /// Get all root nodes
297    pub fn get_roots(&self) -> Vec<&TreeNode> {
298        self.roots
299            .iter()
300            .filter_map(|&id| self.nodes.get(&id))
301            .collect()
302    }
303
304    /// Get children of a node
305    pub fn get_children(&self, node_id: Uuid) -> Vec<&TreeNode> {
306        if let Some(node) = self.nodes.get(&node_id) {
307            node.children
308                .iter()
309                .filter_map(|&child_id| self.nodes.get(&child_id))
310                .collect()
311        } else {
312            Vec::new()
313        }
314    }
315
316    /// Get parent of a node
317    pub fn get_parent(&self, node_id: Uuid) -> Option<&TreeNode> {
318        if let Some(node) = self.nodes.get(&node_id) {
319            node.parent.and_then(|parent_id| self.nodes.get(&parent_id))
320        } else {
321            None
322        }
323    }
324
325    /// Get total number of nodes in the tree
326    pub fn node_count(&self) -> usize {
327        self.nodes.len()
328    }
329}
330
331impl TreeNode {
332    /// Check if this node is a root (has no parent)
333    pub fn is_root(&self) -> bool {
334        self.parent.is_none()
335    }
336
337    /// Check if this node is a leaf (has no children)
338    pub fn is_leaf(&self) -> bool {
339        self.children.is_empty()
340    }
341}
342
343#[cfg(test)]
344mod tests {
345    use super::*;
346    use crate::parser::nodes::FlamegraphParser;
347
348    // #[test]
349    // fn test_tree_from_parser_nodes() {
350    //     // Create some test ParserNodes
351    //     let parser_nodes = vec![
352    //         ParserNode {
353    //             todo!()
354    //         },
355    //         ParserNode {
356    //             todo!()
357    //         },
358    //     ];
359
360    //     let tree = Tree::from_parser_nodes(parser_nodes);
361
362    //     // Verify we have 2 nodes
363    //     assert_eq!(tree.node_count(), 2);
364
365    //     // Find the main and child nodes
366    //     let main_node = tree
367    //         .nodes
368    //         .values()
369    //         .find(|node| node.function_name == "main")
370    //         .expect("Should find main node");
371
372    //     let child_node = tree
373    //         .nodes
374    //         .values()
375    //         .find(|node| node.function_name == "child")
376    //         .expect("Should find child node");
377
378    //     // Verify main is root and child has main as parent
379    //     assert!(main_node.is_root());
380    //     assert_eq!(child_node.parent, Some(main_node.id));
381    //     assert!(main_node.children.contains(&child_node.id));
382
383    //     println!("Tree created successfully with {} nodes", tree.node_count());
384    //     println!("Main node ID: {}", main_node.id);
385    //     println!("Child node ID: {}", child_node.id);
386    //     println!("Child's parent: {:?}", child_node.parent);
387    // }
388
389    #[test]
390    fn test_tree_with_real_flamegraph() {
391        crate::init_test_logging();
392        let svg_content = include_str!("test_flamegraph.svg");
393        let mut parser = FlamegraphParser::default();
394        let parser_nodes = parser.parse_svg(svg_content).expect("Should parse SVG");
395
396        let tree = Tree::from_parser_nodes(parser_nodes, 0);
397
398        log::debug!("Created tree with {} nodes from real flamegraph", tree.node_count());
399        log::debug!("Number of root nodes: {}", tree.roots.len());
400
401        log::debug!("Root node IDs: {:?}", tree.roots);
402
403        // Test the Display implementation
404        log::debug!("{}", tree);
405
406        // Verify we have nodes
407        assert!(tree.node_count() > 0);
408        // assert!(!tree.roots.is_empty());
409
410        // Check that some nodes have proper relationships
411        let nodes_with_children = tree
412            .nodes
413            .values()
414            .filter(|node| !node.children.is_empty())
415            .count();
416
417        log::debug!("Nodes with children: {}", nodes_with_children);
418
419        // Should have some parent-child relationships
420        assert!(nodes_with_children > 0);
421    }
422}