grafos_profile/
span_tree.rs

1//! Hierarchical span tree reconstruction from flat span lists.
2//!
3//! Builds a tree from parent-child relationships using [`ResourceSpan::parent_span_id`].
4//! Orphan spans (missing parents) become additional roots.
5
6use alloc::vec::Vec;
7
8use grafos_observe::span::ResourceSpan;
9
10/// A node in the hierarchical span tree.
11#[derive(Debug, Clone)]
12pub struct SpanTreeNode {
13    /// The span data at this node.
14    pub span: ResourceSpan,
15    /// Indices of child nodes in the parent [`SpanTree::nodes`] vector.
16    pub children: Vec<usize>,
17}
18
19/// Hierarchical reconstruction of spans from parent-child relationships.
20#[derive(Debug, Clone)]
21pub struct SpanTree {
22    /// All nodes in the tree (flat storage, children reference by index).
23    pub nodes: Vec<SpanTreeNode>,
24    /// Indices of root nodes (spans with no parent or orphaned parents).
25    pub roots: Vec<usize>,
26}
27
28impl SpanTree {
29    /// Build a span tree from a flat list of spans.
30    ///
31    /// Uses `parent_span_id` to establish parent-child relationships.
32    /// Spans whose parent is not found in the list become additional roots.
33    pub fn build(spans: &[ResourceSpan]) -> Self {
34        let mut nodes: Vec<SpanTreeNode> = spans
35            .iter()
36            .map(|s| SpanTreeNode {
37                span: s.clone(),
38                children: Vec::new(),
39            })
40            .collect();
41
42        // Build a map from span_id -> index
43        let mut roots = Vec::new();
44
45        // For each span, find its parent and add it as a child
46        for i in 0..nodes.len() {
47            let parent_id = nodes[i].span.parent_span_id;
48            if let Some(pid) = parent_id {
49                if pid.is_valid() {
50                    // Find parent by span_id
51                    let parent_idx = nodes
52                        .iter()
53                        .position(|n| n.span.trace_context.span_id == pid);
54                    if parent_idx.is_some() {
55                        // Has a parent in the list — will be wired up in second pass
56                        continue;
57                    }
58                }
59            }
60            // No valid parent or parent not found -> root
61            roots.push(i);
62        }
63
64        // Second pass: actually wire up children
65        // Collect (parent_idx, child_idx) pairs first
66        let mut child_links: Vec<(usize, usize)> = Vec::new();
67        for i in 0..nodes.len() {
68            if roots.contains(&i) {
69                continue;
70            }
71            if let Some(pid) = nodes[i].span.parent_span_id {
72                if pid.is_valid() {
73                    if let Some(pi) = nodes
74                        .iter()
75                        .position(|n| n.span.trace_context.span_id == pid)
76                    {
77                        child_links.push((pi, i));
78                    } else {
79                        roots.push(i);
80                    }
81                }
82            }
83        }
84
85        for (parent_idx, child_idx) in child_links {
86            nodes[parent_idx].children.push(child_idx);
87        }
88
89        SpanTree { nodes, roots }
90    }
91
92    /// Total lease cost across all root subtrees.
93    pub fn total_cost(&self) -> u64 {
94        self.nodes.iter().map(|n| n.span.lease_cost_byte_secs).sum()
95    }
96
97    /// Depth-first iteration yielding (depth, node_index).
98    pub fn dfs(&self) -> Vec<(usize, usize)> {
99        let mut result = Vec::new();
100        for &root_idx in &self.roots {
101            self.dfs_visit(root_idx, 0, &mut result);
102        }
103        result
104    }
105
106    fn dfs_visit(&self, idx: usize, depth: usize, result: &mut Vec<(usize, usize)>) {
107        result.push((depth, idx));
108        for &child_idx in &self.nodes[idx].children {
109            self.dfs_visit(child_idx, depth + 1, result);
110        }
111    }
112
113    /// Compute the aggregated cost of a subtree rooted at the given node index.
114    pub fn subtree_cost(&self, idx: usize) -> u64 {
115        let mut cost = self.nodes[idx].span.lease_cost_byte_secs;
116        for &child_idx in &self.nodes[idx].children {
117            cost += self.subtree_cost(child_idx);
118        }
119        cost
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126    use grafos_observe::trace::{SpanId, TraceContext};
127
128    fn test_ctx() -> TraceContext {
129        let mut bytes = [0u8; 24];
130        for (i, b) in bytes.iter_mut().enumerate() {
131            *b = (i as u8).wrapping_add(0x42);
132        }
133        TraceContext::new_root(&bytes)
134    }
135
136    #[test]
137    fn single_root() {
138        let ctx = test_ctx();
139        let span = ResourceSpan::new("root", ctx);
140        let tree = SpanTree::build(&[span]);
141
142        assert_eq!(tree.nodes.len(), 1);
143        assert_eq!(tree.roots.len(), 1);
144        assert_eq!(tree.roots[0], 0);
145        assert!(tree.nodes[0].children.is_empty());
146    }
147
148    #[test]
149    fn parent_child_hierarchy() {
150        let root_ctx = test_ctx();
151        let child_ctx = root_ctx.child(&[0xAA; 8]);
152        let grandchild_ctx = child_ctx.child(&[0xBB; 8]);
153
154        let mut root_span = ResourceSpan::new("root", root_ctx);
155        root_span.lease_cost_byte_secs = 100;
156
157        let mut child_span = ResourceSpan::new("child", child_ctx);
158        child_span.parent_span_id = Some(root_ctx.span_id);
159        child_span.lease_cost_byte_secs = 50;
160
161        let mut grandchild_span = ResourceSpan::new("grandchild", grandchild_ctx);
162        grandchild_span.parent_span_id = Some(child_ctx.span_id);
163        grandchild_span.lease_cost_byte_secs = 25;
164
165        let tree = SpanTree::build(&[root_span, child_span, grandchild_span]);
166
167        assert_eq!(tree.roots.len(), 1);
168        assert_eq!(tree.roots[0], 0);
169        assert_eq!(tree.nodes[0].children, vec![1]);
170        assert_eq!(tree.nodes[1].children, vec![2]);
171        assert!(tree.nodes[2].children.is_empty());
172
173        assert_eq!(tree.total_cost(), 175);
174        assert_eq!(tree.subtree_cost(0), 175);
175        assert_eq!(tree.subtree_cost(1), 75);
176        assert_eq!(tree.subtree_cost(2), 25);
177    }
178
179    #[test]
180    fn orphan_becomes_root() {
181        let ctx1 = test_ctx();
182        let ctx2 = ctx1.child(&[0xCC; 8]);
183
184        let root_span = ResourceSpan::new("root", ctx1);
185
186        // Orphan: has a parent_span_id that doesn't match any span in the list
187        let mut orphan = ResourceSpan::new("orphan", ctx2);
188        orphan.parent_span_id = Some(SpanId(0xDEAD));
189
190        let tree = SpanTree::build(&[root_span, orphan]);
191
192        assert_eq!(tree.roots.len(), 2);
193        assert!(tree.roots.contains(&0));
194        assert!(tree.roots.contains(&1));
195    }
196
197    #[test]
198    fn dfs_order() {
199        let root_ctx = test_ctx();
200        let child_a_ctx = root_ctx.child(&[0xAA; 8]);
201        let child_b_ctx = root_ctx.child(&[0xBB; 8]);
202
203        let root = ResourceSpan::new("root", root_ctx);
204
205        let mut child_a = ResourceSpan::new("child_a", child_a_ctx);
206        child_a.parent_span_id = Some(root_ctx.span_id);
207
208        let mut child_b = ResourceSpan::new("child_b", child_b_ctx);
209        child_b.parent_span_id = Some(root_ctx.span_id);
210
211        let tree = SpanTree::build(&[root, child_a, child_b]);
212        let dfs = tree.dfs();
213
214        assert_eq!(dfs.len(), 3);
215        assert_eq!(dfs[0], (0, 0)); // root at depth 0
216        assert_eq!(dfs[1], (1, 1)); // child_a at depth 1
217        assert_eq!(dfs[2], (1, 2)); // child_b at depth 1
218    }
219}