use super::{GraphInner, NodeStats};
use std::sync::atomic::Ordering;
fn avg_process_us(st: &NodeStats) -> f64 {
let n = st.processed.load(Ordering::Relaxed);
if n == 0 {
0.0
} else {
st.total_us.load(Ordering::Relaxed) as f64 / n as f64
}
}
fn heat_color(v: f64, max: f64) -> String {
if max <= 0.0 {
return "white".to_string();
}
let t = (v / max).clamp(0.0, 1.0);
let lerp = |a: u8, b: u8| (a as f64 + (b as f64 - a as f64) * t).round() as u8;
format!(
"#{:02X}{:02X}{:02X}",
lerp(0xB7, 0xE8),
lerp(0xE1, 0x8A),
lerp(0xA1, 0x7D)
)
}
impl GraphInner {
pub(super) fn to_dot(&self, with_stats: bool) -> String {
const COLORS: &[&str] = &[
"#cde4ff", "#d7f0d0", "#ffe4c7", "#f0d0e8", "#d0eeee", "#efe6b0", "#e0d4f0", "#ffd6d6",
];
fn esc(s: &str) -> String {
s.replace('\\', "\\\\").replace('"', "\\\"")
}
#[derive(Default)]
struct Ns {
children: std::collections::BTreeMap<String, Ns>,
leaves: Vec<usize>,
}
impl Ns {
fn insert(&mut self, path: &[&str], node: usize) {
match path {
[] | [_] => self.leaves.push(node),
[head, rest @ ..] => self
.children
.entry((*head).to_string())
.or_default()
.insert(rest, node),
}
}
fn emit(&self, lines: &[String], out: &mut String, cid: &mut usize) {
for &i in &self.leaves {
out.push_str(&lines[i]);
}
for (name, child) in &self.children {
let id = *cid;
*cid += 1;
out.push_str(&format!(
" subgraph cluster_{id} {{ label=\"{}\"; style=dashed; color=\"#888888\";\n",
esc(name)
));
child.emit(lines, out, cid);
out.push_str(" }\n");
}
}
}
let mut out = String::new();
out.push_str("digraph lmflow {\n");
out.push_str(" rankdir=LR;\n");
out.push_str(" node [shape=box, style=\"rounded,filled\", fillcolor=white];\n");
out.push_str(" edge [fontsize=10];\n");
let mut lines = vec![String::new(); self.nodes.len()];
let mut tree = Ns::default();
let max_avg_us = if with_stats {
self.nodes
.iter()
.map(|n| avg_process_us(&n.stats))
.fold(0.0f64, f64::max)
} else {
0.0
};
for (i, n) in self.nodes.iter().enumerate() {
let short = n.name.rsplit('/').next().unwrap_or(n.name.as_str());
let (exec, mut fill) = match n.executor {
None => ("@main".to_string(), "white".to_string()),
Some(ei) => (
format!("@{}", self.executors[ei].name()),
COLORS[ei % COLORS.len()].to_string(),
),
};
let mut extra = String::new();
if with_stats {
let st = &n.stats;
let processed = st.processed.load(Ordering::Relaxed);
let avg = avg_process_us(st);
extra = format!(
"\\n{} pkts · {:.0}µs avg\\nin {} / out {} · peakQ {}",
processed,
avg,
st.packets_in.load(Ordering::Relaxed),
st.packets_out.load(Ordering::Relaxed),
st.peak_queue_depth.load(Ordering::Relaxed),
);
let errs = st.errors.load(Ordering::Relaxed);
if errs > 0 {
extra.push_str(&format!(" · {errs} err"));
}
fill = heat_color(avg, max_avg_us);
}
lines[i] = format!(
" n{i} [label=\"{}\\n({})\\n{}{}\", fillcolor=\"{}\"];\n",
esc(short),
esc(&n.kernel_name),
esc(&exec),
extra,
fill
);
let path: Vec<&str> = n.name.split('/').collect();
tree.insert(&path, i);
}
let mut cid = 0usize;
tree.emit(&lines, &mut out, &mut cid);
for &e in &self.graph_inputs {
out.push_str(&format!(
" pin{e} [shape=cds, style=filled, fillcolor=\"#e8e8e8\", label=\"{}\"];\n",
esc(&self.edges[e].name)
));
}
for &e in &self.graph_outputs {
out.push_str(&format!(
" pout{e} [shape=cds, style=filled, fillcolor=\"#e8e8e8\", label=\"{}\"];\n",
esc(&self.edges[e].name)
));
}
for (ei, e) in self.edges.iter().enumerate() {
let label = esc(&e.name);
if e.is_graph_input {
for &(c, _) in &e.consumers {
out.push_str(&format!(" pin{ei} -> n{c} [label=\"{label}\"];\n"));
}
} else if let Some(p) = e.producer {
for &(c, _) in &e.consumers {
out.push_str(&format!(" n{p} -> n{c} [label=\"{label}\"];\n"));
}
}
if e.is_graph_output {
if let Some(p) = e.producer {
out.push_str(&format!(" n{p} -> pout{ei} [label=\"{label}\"];\n"));
}
}
}
if !self.executors.is_empty() {
out.push_str(
" subgraph cluster_legend {\n label=\"executors (node fill = placement)\"; style=dashed; color=\"#888888\";\n",
);
for (i, ex) in self.executors.iter().enumerate() {
let cores = if ex.affinity().is_empty() {
"cores: any".to_string()
} else {
format!(
"cores[{}]",
ex.affinity()
.iter()
.map(|c| c.to_string())
.collect::<Vec<_>>()
.join(",")
)
};
let prio = if ex.priority() > 0 {
format!(" · rt{}", ex.priority())
} else {
String::new()
};
out.push_str(&format!(
" legend_e{i} [shape=box, style=filled, fillcolor=\"{}\", label=\"{}\\n{}t · {}{}\"];\n",
COLORS[i % COLORS.len()],
esc(ex.name()),
ex.num_threads(),
cores,
prio
));
}
out.push_str(
" legend_main [shape=box, style=filled, fillcolor=white, label=\"main thread\\n(default)\"];\n",
);
out.push_str(" }\n");
}
out.push_str("}\n");
out
}
}