1use std::collections::HashMap;
4
5use super::core::ExecutionGraph;
6use crate::brick::exec_graph::node::{EdgeType, ExecutionNode, TransferDirection};
7
8fn 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
28fn 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
68fn 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
97fn 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
146fn 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 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 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 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 #[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 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 #[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 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), )
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), )
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 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 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 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 #[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}