Skip to main content

trueno/brick/exec_graph/traversal/
export.rs

1//! ExecutionGraph export and visualization: DOT, CSR, TUI tree, ASCII tree.
2
3use std::collections::HashMap;
4
5use super::core::ExecutionGraph;
6use crate::brick::exec_graph::node::{EdgeType, ExecutionNode, TransferDirection};
7
8/// Build parent-child relationship map and identify root nodes.
9fn build_children_map(
10    edges: &[crate::brick::exec_graph::node::ExecutionEdge],
11    node_count: usize,
12) -> (HashMap<u32, Vec<u32>>, Vec<u32>) {
13    let mut children_map: HashMap<u32, Vec<u32>> = HashMap::new();
14    let mut has_parent: std::collections::HashSet<u32> = std::collections::HashSet::new();
15
16    for edge in edges {
17        if edge.edge_type == EdgeType::Contains || edge.edge_type == EdgeType::Launches {
18            children_map.entry(edge.src.0).or_default().push(edge.dst.0);
19            has_parent.insert(edge.dst.0);
20        }
21    }
22
23    let root_ids: Vec<u32> = (0..node_count as u32).filter(|id| !has_parent.contains(id)).collect();
24
25    (children_map, root_ids)
26}
27
28/// Format an execution node as (label, info) for ASCII tree rendering.
29fn format_ascii_node(node: &ExecutionNode) -> (String, String) {
30    match node {
31        ExecutionNode::Layer { index } => (format!("Layer {}", index), String::new()),
32        ExecutionNode::Brick { id: brick_id, timing_ns, elements } => (
33            brick_id.name().to_string(),
34            format!("  {:.1}µs ({} elem)", *timing_ns as f64 / 1000.0, elements),
35        ),
36        ExecutionNode::Kernel { name, grid, block, shared_mem, .. } => (
37            name.clone(),
38            format!("  <<<{},{},{}>>> smem={}B", grid.0, block.0, block.1, shared_mem),
39        ),
40        ExecutionNode::Function { name, file, line } => {
41            let loc = match (file, line) {
42                (Some(f), Some(l)) => format!(" ({}:{})", f, l),
43                (None, _) | (_, None) => String::new(),
44            };
45            (format!("{}{}", name, loc), String::new())
46        }
47        ExecutionNode::Transfer { src, dst, bytes, direction, timing_ns } => {
48            let timing_str =
49                timing_ns.map(|ns| format!(" {:.1}µs", ns as f64 / 1000.0)).unwrap_or_default();
50            (format!("{:?}: {} → {}", direction, src, dst), format!("  {}B{}", bytes, timing_str))
51        }
52        ExecutionNode::AsyncTask { name, poll_count, yield_count, total_poll_ns } => {
53            let efficiency = if *poll_count > 0 { 100.0 / *poll_count as f64 } else { 0.0 };
54            (
55                name.clone(),
56                format!(
57                    "  polls:{} yields:{} {:.1}µs ({:.0}% eff)",
58                    poll_count,
59                    yield_count,
60                    *total_poll_ns as f64 / 1000.0,
61                    efficiency
62                ),
63            )
64        }
65    }
66}
67
68/// Recursively build ASCII tree string for a node and its children.
69fn build_ascii_tree(
70    graph: &ExecutionGraph,
71    id: u32,
72    children_map: &HashMap<u32, Vec<u32>>,
73    prefix: &str,
74    connector: &str,
75    output: &mut String,
76) {
77    let (label, info) = format_ascii_node(&graph.nodes[id as usize]);
78    output.push_str(&format!("{}{}{}{}\n", prefix, connector, label, info));
79
80    if let Some(child_ids) = children_map.get(&id) {
81        let child_count = child_ids.len();
82        for (i, &child_id) in child_ids.iter().enumerate() {
83            let is_last = i == child_count - 1;
84            let new_connector = if is_last { "└── " } else { "├── " };
85            let new_prefix = if connector.is_empty() {
86                prefix.to_string()
87            } else if connector == "└── " {
88                format!("{}    ", prefix)
89            } else {
90                format!("{}│   ", prefix)
91            };
92            build_ascii_tree(graph, child_id, children_map, &new_prefix, new_connector, output);
93        }
94    }
95}
96
97/// Map an `ExecutionNode` to its DOT label text and style attribute string.
98fn node_to_dot_label(node: &ExecutionNode) -> (String, &'static str) {
99    match node {
100        ExecutionNode::Layer { index } => {
101            (format!("Layer {}", index), "style=filled,fillcolor=lightblue")
102        }
103        ExecutionNode::Brick { id, timing_ns, .. } => (
104            format!("{}\\n{:.1}µs", id.name(), *timing_ns as f64 / 1000.0),
105            "style=filled,fillcolor=lightgreen",
106        ),
107        ExecutionNode::Kernel { name, grid, block, .. } => (
108            format!("{}\\n<<<{},{},{}>>>", name, grid.0, block.0, block.1),
109            "style=filled,fillcolor=lightyellow",
110        ),
111        ExecutionNode::Function { name, file, line } => {
112            let loc = match (file, line) {
113                (Some(f), Some(l)) => format!("\\n{}:{}", f, l),
114                (None, _) | (_, None) => String::new(),
115            };
116            (format!("{}{}", name, loc), "style=filled,fillcolor=lightgray")
117        }
118        ExecutionNode::Transfer { src, dst, bytes, direction, .. } => {
119            let dir = match direction {
120                TransferDirection::H2D => "H2D",
121                TransferDirection::D2H => "D2H",
122                TransferDirection::D2D => "D2D",
123            };
124            (
125                format!("{}\\n{}->{}\\n{:.1}MB", dir, src, dst, *bytes as f64 / 1e6),
126                "style=filled,fillcolor=lightsalmon",
127            )
128        }
129        ExecutionNode::AsyncTask { name, poll_count, yield_count, total_poll_ns } => {
130            let efficiency = if *poll_count > 0 { 100.0 / *poll_count as f64 } else { 0.0 };
131            (
132                format!(
133                    "{}\\npolls:{} yields:{}\\n{:.1}µs ({:.0}%)",
134                    name,
135                    poll_count,
136                    yield_count,
137                    *total_poll_ns as f64 / 1000.0,
138                    efficiency
139                ),
140                "style=filled,fillcolor=lightcyan",
141            )
142        }
143    }
144}
145
146/// Map an `EdgeType` to its DOT style attribute string.
147fn edge_to_dot_style(edge_type: &EdgeType) -> &'static str {
148    match edge_type {
149        EdgeType::Calls => "style=solid",
150        EdgeType::Contains => "style=dashed",
151        EdgeType::Launches => "style=bold,color=red",
152        EdgeType::Sequence => "style=dotted",
153        EdgeType::DependsOn => "style=solid,color=blue",
154        EdgeType::Transfer { .. } => "style=bold,color=orange",
155    }
156}
157
158impl ExecutionGraph {
159    /// Export to DOT format for Graphviz visualization.
160    pub fn to_dot(&self) -> String {
161        debug_assert!(
162            self.nodes.len() <= u32::MAX as usize,
163            "CB-BUDGET: graph node count {} exceeds u32 range",
164            self.nodes.len()
165        );
166        let mut dot = String::from("digraph ExecutionGraph {\n");
167        dot.push_str("  rankdir=TB;\n");
168        dot.push_str("  node [shape=box];\n\n");
169
170        // Add nodes with styling based on type
171        for (i, node) in self.nodes.iter().enumerate() {
172            let (label, style) = node_to_dot_label(node);
173            dot.push_str(&format!("  n{} [label=\"{}\",{}];\n", i, label, style));
174        }
175
176        dot.push('\n');
177
178        // Add edges with styling based on type
179        for edge in &self.edges {
180            let style = edge_to_dot_style(&edge.edge_type);
181            dot.push_str(&format!("  n{} -> n{} [{}];\n", edge.src.0, edge.dst.0, style));
182        }
183
184        dot.push_str("}\n");
185        dot
186    }
187
188    /// Export to trueno-graph CsrGraph format.
189    #[cfg(feature = "execution-graph")]
190    pub fn to_csr(&self) -> trueno_graph::CsrGraph {
191        use trueno_graph::{CsrGraph, NodeId};
192
193        let edges: Vec<(NodeId, NodeId, f32)> =
194            self.edges.iter().map(|e| (NodeId(e.src.0), NodeId(e.dst.0), e.weight)).collect();
195
196        let mut graph = CsrGraph::from_edge_list(&edges).unwrap_or_default();
197
198        // Set node names for querying
199        for (i, node) in self.nodes.iter().enumerate() {
200            graph.set_node_name(NodeId(i as u32), node.name());
201        }
202
203        graph
204    }
205
206    /// Convert to presentar-terminal TreeNode for TUI visualization.
207    ///
208    /// PAR-201: Renders the execution graph as a collapsible tree in the terminal.
209    #[cfg(feature = "presentar-tui")]
210    pub fn to_tree_node(&self) -> presentar_terminal::TreeNode {
211        use presentar_terminal::{Color, TreeNode};
212
213        let layer_color = Color::new(0.4, 0.6, 1.0, 1.0);
214        let brick_color = Color::new(0.4, 0.8, 0.4, 1.0);
215        let kernel_color = Color::new(1.0, 0.8, 0.3, 1.0);
216        let func_color = Color::new(0.7, 0.7, 0.7, 1.0);
217
218        let (children_map, root_ids) = build_children_map(&self.edges, self.nodes.len());
219
220        // Recursive function to build TreeNode
221        fn build_node(
222            graph: &ExecutionGraph,
223            id: u32,
224            children_map: &HashMap<u32, Vec<u32>>,
225            layer_color: Color,
226            brick_color: Color,
227            kernel_color: Color,
228            func_color: Color,
229        ) -> TreeNode {
230            let node = &graph.nodes[id as usize];
231            let (label, info, color) = match node {
232                ExecutionNode::Layer { index } => (format!("Layer {}", index), None, layer_color),
233                ExecutionNode::Brick { id: brick_id, timing_ns, elements } => (
234                    brick_id.name().to_string(),
235                    Some(format!("{:.1}µs ({} elem)", *timing_ns as f64 / 1000.0, elements)),
236                    brick_color,
237                ),
238                ExecutionNode::Kernel { name, grid, block, shared_mem, .. } => (
239                    name.clone(),
240                    Some(format!("<<<{},{},{}>>> smem={}B", grid.0, block.0, block.1, shared_mem)),
241                    kernel_color,
242                ),
243                ExecutionNode::Function { name, file, line } => {
244                    let loc = match (file, line) {
245                        (Some(f), Some(l)) => format!(" ({}:{})", f, l),
246                        (None, _) | (_, None) => String::new(),
247                    };
248                    (format!("{}{}", name, loc), None, func_color)
249                }
250                ExecutionNode::Transfer { src, dst, bytes, direction, timing_ns } => {
251                    let timing_str = timing_ns
252                        .map(|ns| format!(" {:.1}µs", ns as f64 / 1000.0))
253                        .unwrap_or_default();
254                    (
255                        format!("{:?}: {} → {}", direction, src, dst),
256                        Some(format!("{}B{}", bytes, timing_str)),
257                        Color::new(0.8, 0.4, 0.8, 1.0), // Transfer color (magenta)
258                    )
259                }
260                ExecutionNode::AsyncTask { name, poll_count, yield_count, total_poll_ns } => {
261                    let efficiency = if *poll_count > 0 { 100.0 / *poll_count as f64 } else { 0.0 };
262                    (
263                        name.clone(),
264                        Some(format!(
265                            "polls:{} yields:{} {:.1}µs ({:.0}% eff)",
266                            poll_count,
267                            yield_count,
268                            *total_poll_ns as f64 / 1000.0,
269                            efficiency
270                        )),
271                        Color::new(0.4, 0.8, 0.8, 1.0), // Async task color (cyan)
272                    )
273                }
274            };
275
276            let mut tree_node = TreeNode::new(id as u64, label).with_color(color);
277            if let Some(info_str) = info {
278                tree_node = tree_node.with_info(info_str);
279            }
280
281            // Add children
282            if let Some(child_ids) = children_map.get(&id) {
283                for &child_id in child_ids {
284                    let child = build_node(
285                        graph,
286                        child_id,
287                        children_map,
288                        layer_color,
289                        brick_color,
290                        kernel_color,
291                        func_color,
292                    );
293                    tree_node = tree_node.with_child(child);
294                }
295            }
296
297            tree_node
298        }
299
300        // Build root node
301        if root_ids.is_empty() {
302            TreeNode::new(0, "Empty Graph")
303        } else if root_ids.len() == 1 {
304            build_node(
305                self,
306                root_ids[0],
307                &children_map,
308                layer_color,
309                brick_color,
310                kernel_color,
311                func_color,
312            )
313        } else {
314            // Multiple roots: wrap in a synthetic root
315            let mut root = TreeNode::new(u64::MAX, "Execution Graph")
316                .with_color(Color::new(0.9, 0.9, 0.9, 1.0));
317            for &root_id in &root_ids {
318                let child = build_node(
319                    self,
320                    root_id,
321                    &children_map,
322                    layer_color,
323                    brick_color,
324                    kernel_color,
325                    func_color,
326                );
327                root = root.with_child(child);
328            }
329            root
330        }
331    }
332
333    /// Render graph to ASCII tree string (headless mode for testing/automation).
334    ///
335    /// PAR-201: Zero-dependency tree visualization for CI/CD, logging, and snapshot tests.
336    #[must_use]
337    pub fn to_ascii_tree(&self) -> String {
338        let (children_map, root_ids) = build_children_map(&self.edges, self.nodes.len());
339        let mut output = String::new();
340
341        if root_ids.is_empty() {
342            output.push_str("(empty graph)\n");
343        } else if root_ids.len() == 1 {
344            build_ascii_tree(self, root_ids[0], &children_map, "", "", &mut output);
345        } else {
346            output.push_str("Execution Graph\n");
347            let root_count = root_ids.len();
348            for (i, &root_id) in root_ids.iter().enumerate() {
349                let is_last = i == root_count - 1;
350                let connector = if is_last { "└── " } else { "├── " };
351                build_ascii_tree(self, root_id, &children_map, "", connector, &mut output);
352            }
353        }
354
355        if output.ends_with('\n') {
356            output.pop();
357        }
358        output
359    }
360}