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    // NOTE: `to_csr()` (export to trueno-graph CsrGraph) removed (APR-MONO
189    // self-containment): aprender-graph depends on trueno, so compute→graph closed a
190    // compute→graph→compute cycle. CSR export belongs in aprender-graph (which already
191    // depends on compute), not in the compute foundation. `to_dot`/`to_ascii_tree` remain.
192
193    /// Convert to presentar-terminal TreeNode for TUI visualization.
194    ///
195    /// PAR-201: Renders the execution graph as a collapsible tree in the terminal.
196    #[cfg(feature = "presentar-tui")]
197    pub fn to_tree_node(&self) -> presentar_terminal::TreeNode {
198        use presentar_terminal::{Color, TreeNode};
199
200        let layer_color = Color::new(0.4, 0.6, 1.0, 1.0);
201        let brick_color = Color::new(0.4, 0.8, 0.4, 1.0);
202        let kernel_color = Color::new(1.0, 0.8, 0.3, 1.0);
203        let func_color = Color::new(0.7, 0.7, 0.7, 1.0);
204
205        let (children_map, root_ids) = build_children_map(&self.edges, self.nodes.len());
206
207        // Recursive function to build TreeNode
208        fn build_node(
209            graph: &ExecutionGraph,
210            id: u32,
211            children_map: &HashMap<u32, Vec<u32>>,
212            layer_color: Color,
213            brick_color: Color,
214            kernel_color: Color,
215            func_color: Color,
216        ) -> TreeNode {
217            let node = &graph.nodes[id as usize];
218            let (label, info, color) = match node {
219                ExecutionNode::Layer { index } => (format!("Layer {}", index), None, layer_color),
220                ExecutionNode::Brick { id: brick_id, timing_ns, elements } => (
221                    brick_id.name().to_string(),
222                    Some(format!("{:.1}µs ({} elem)", *timing_ns as f64 / 1000.0, elements)),
223                    brick_color,
224                ),
225                ExecutionNode::Kernel { name, grid, block, shared_mem, .. } => (
226                    name.clone(),
227                    Some(format!("<<<{},{},{}>>> smem={}B", grid.0, block.0, block.1, shared_mem)),
228                    kernel_color,
229                ),
230                ExecutionNode::Function { name, file, line } => {
231                    let loc = match (file, line) {
232                        (Some(f), Some(l)) => format!(" ({}:{})", f, l),
233                        (None, _) | (_, None) => String::new(),
234                    };
235                    (format!("{}{}", name, loc), None, func_color)
236                }
237                ExecutionNode::Transfer { src, dst, bytes, direction, timing_ns } => {
238                    let timing_str = timing_ns
239                        .map(|ns| format!(" {:.1}µs", ns as f64 / 1000.0))
240                        .unwrap_or_default();
241                    (
242                        format!("{:?}: {} → {}", direction, src, dst),
243                        Some(format!("{}B{}", bytes, timing_str)),
244                        Color::new(0.8, 0.4, 0.8, 1.0), // Transfer color (magenta)
245                    )
246                }
247                ExecutionNode::AsyncTask { name, poll_count, yield_count, total_poll_ns } => {
248                    let efficiency = if *poll_count > 0 { 100.0 / *poll_count as f64 } else { 0.0 };
249                    (
250                        name.clone(),
251                        Some(format!(
252                            "polls:{} yields:{} {:.1}µs ({:.0}% eff)",
253                            poll_count,
254                            yield_count,
255                            *total_poll_ns as f64 / 1000.0,
256                            efficiency
257                        )),
258                        Color::new(0.4, 0.8, 0.8, 1.0), // Async task color (cyan)
259                    )
260                }
261            };
262
263            let mut tree_node = TreeNode::new(id as u64, label).with_color(color);
264            if let Some(info_str) = info {
265                tree_node = tree_node.with_info(info_str);
266            }
267
268            // Add children
269            if let Some(child_ids) = children_map.get(&id) {
270                for &child_id in child_ids {
271                    let child = build_node(
272                        graph,
273                        child_id,
274                        children_map,
275                        layer_color,
276                        brick_color,
277                        kernel_color,
278                        func_color,
279                    );
280                    tree_node = tree_node.with_child(child);
281                }
282            }
283
284            tree_node
285        }
286
287        // Build root node
288        if root_ids.is_empty() {
289            TreeNode::new(0, "Empty Graph")
290        } else if root_ids.len() == 1 {
291            build_node(
292                self,
293                root_ids[0],
294                &children_map,
295                layer_color,
296                brick_color,
297                kernel_color,
298                func_color,
299            )
300        } else {
301            // Multiple roots: wrap in a synthetic root
302            let mut root = TreeNode::new(u64::MAX, "Execution Graph")
303                .with_color(Color::new(0.9, 0.9, 0.9, 1.0));
304            for &root_id in &root_ids {
305                let child = build_node(
306                    self,
307                    root_id,
308                    &children_map,
309                    layer_color,
310                    brick_color,
311                    kernel_color,
312                    func_color,
313                );
314                root = root.with_child(child);
315            }
316            root
317        }
318    }
319
320    /// Render graph to ASCII tree string (headless mode for testing/automation).
321    ///
322    /// PAR-201: Zero-dependency tree visualization for CI/CD, logging, and snapshot tests.
323    #[must_use]
324    pub fn to_ascii_tree(&self) -> String {
325        let (children_map, root_ids) = build_children_map(&self.edges, self.nodes.len());
326        let mut output = String::new();
327
328        if root_ids.is_empty() {
329            output.push_str("(empty graph)\n");
330        } else if root_ids.len() == 1 {
331            build_ascii_tree(self, root_ids[0], &children_map, "", "", &mut output);
332        } else {
333            output.push_str("Execution Graph\n");
334            let root_count = root_ids.len();
335            for (i, &root_id) in root_ids.iter().enumerate() {
336                let is_last = i == root_count - 1;
337                let connector = if is_last { "└── " } else { "├── " };
338                build_ascii_tree(self, root_id, &children_map, "", connector, &mut output);
339            }
340        }
341
342        if output.ends_with('\n') {
343            output.pop();
344        }
345        output
346    }
347}