use std::collections::HashMap;
use petgraph::graph::NodeIndex;
use crate::Website;
use crate::executor::TaskExecution;
#[derive(Debug, Default)]
pub struct Diagnostics {
pub execution_times: HashMap<NodeIndex, TaskExecution>,
}
impl Diagnostics {
pub fn render_waterfall<G>(&self, site: &Website<G>) -> String
where
G: Send + Sync,
{
use std::fmt::Write;
let mut output = String::new();
let mut ran_tasks: Vec<(NodeIndex, &TaskExecution)> =
self.execution_times.iter().map(|(k, v)| (*k, v)).collect();
if ran_tasks.is_empty() {
return "<svg width=\"200\" height=\"50\" xmlns=\"http://www.w3.org/2000/svg\"><text x=\"10\" y=\"30\" font-family=\"sans-serif\">No tasks ran</text></svg>".to_string();
}
ran_tasks.sort_by_key(|(_, t)| t.start);
let global_start = ran_tasks.first().unwrap().1.start;
let global_end = ran_tasks
.iter()
.map(|(_, t)| t.start + t.duration)
.max()
.unwrap();
let total_duration = global_end.duration_since(global_start);
let total_micros = total_duration.as_micros().max(1) as f64;
let row_height = 30;
let label_width = 300;
let chart_width = 800;
let padding = 10;
let header_height = 30;
let text_space = 80;
let width = label_width + chart_width + (padding * 3) + text_space;
let height = header_height + (ran_tasks.len() as u32 * row_height) + padding;
write!(output, r#"<svg width="{}" height="{}" xmlns="http://www.w3.org/2000/svg">
<style>
.task-row:nth-child(even) {{ fill: #f9f9f9; }}
.task-row:nth-child(odd) {{ fill: #ffffff; }}
text {{ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; font-size: 12px; }}
.bar {{ fill: #3b82f6; rx: 4; }}
.bar:hover {{ fill: #2563eb; }}
.label {{ fill: #333; }}
.time {{ fill: #666; font-size: 11px; }}
.grid-line {{ stroke: #e5e7eb; stroke-width: 1; }}
.axis-label {{ fill: #9ca3af; font-size: 10px; }}
</style>
<rect width="100%" height="100%" fill="white" />
"#, width, height).unwrap();
for i in 0..=5 {
let pct = i as f64 / 5.0;
let current_micros = total_micros * pct;
let time_label = if total_micros < 1000.0 {
format!("{:.0}µs", current_micros)
} else {
format!("{:.2}ms", current_micros / 1000.0)
};
let x = label_width as f64 + padding as f64 + (chart_width as f64 * pct);
write!(output, r#" <line x1="{x}" y1="{header_height}" x2="{x}" y2="{height}" class="grid-line" />
<text x="{x}" y="{y}" text-anchor="middle" class="axis-label">{text}</text>
"#, x=x, y=header_height - 5, text=time_label, header_height=header_height, height=height).unwrap();
}
for (i, (node_idx, exec)) in ran_tasks.iter().enumerate() {
let task = &site.graph[*node_idx];
let name = task.get_name();
let y_pos = header_height + (i as u32 * row_height);
write!(
output,
r#" <rect x="0" y="{y}" width="{w}" height="{h}" class="task-row" />"#,
y = y_pos,
w = width,
h = row_height
)
.unwrap();
write!(output, r#" <text x="{x}" y="{y}" class="label" dominant-baseline="middle">{name}</text>"#,
x=padding, y=y_pos + (row_height/2), name=name).unwrap();
let offset_micros = exec.start.duration_since(global_start).as_micros() as f64;
let duration_micros = exec.duration.as_micros() as f64;
let bar_start_x = label_width as f64
+ padding as f64
+ (offset_micros / total_micros * chart_width as f64);
let bar_width = (duration_micros / total_micros * chart_width as f64).max(1.0);
write!(
output,
r#" <rect x="{x}" y="{y}" width="{w}" height="{h}" class="bar">
<title>{name}: {dur:.2?}</title>
</rect>"#,
x = bar_start_x,
y = y_pos + 5, w = bar_width,
h = row_height - 10,
name = name,
dur = exec.duration
)
.unwrap();
let text_label = if duration_micros < 1000.0 {
format!("{:.0}µs", duration_micros)
} else {
format!("{:.2}ms", duration_micros / 1000.0)
};
write!(
output,
r#" <text x="{x}" y="{y}" class="time" dominant-baseline="middle">{lbl}</text>"#,
x = bar_start_x + bar_width + 5.0,
y = y_pos + (row_height / 2),
lbl = text_label
)
.unwrap();
}
output.push_str("</svg>");
output
}
pub fn render_waterfall_to_file<G>(
&self,
site: &Website<G>,
path: impl AsRef<std::path::Path>,
) -> Result<(), std::io::Error>
where
G: Send + Sync,
{
let svg = self.render_waterfall(site);
std::fs::write(path, svg)
}
pub fn render_mermaid<G>(&self, site: &Website<G>) -> String
where
G: Send + Sync,
{
use std::fmt::Write;
let mut f = String::new();
writeln!(f, "graph LR").unwrap();
let times = &self.execution_times;
let mut min_time = f64::MAX;
let mut max_time = f64::MIN;
for t in times.values() {
let secs = t.duration.as_secs_f64();
if secs < min_time {
min_time = secs;
}
if secs > max_time {
max_time = secs;
}
}
if min_time > max_time {
min_time = 0.0;
max_time = 0.0;
}
if (max_time - min_time).abs() < f64::EPSILON {
max_time = min_time + 1.0;
}
for index in site.graph.node_indices() {
let task = &site.graph[index];
let name = task.get_name().replace('"', "\\\"");
let (label_extra, color_code) = if let Some(exec) = times.get(&index) {
let duration = exec.duration;
let duration_str = format!("{:.2?}", duration);
let val = duration.as_secs_f64();
let t = (val - min_time) / (max_time - min_time);
let (r, g, b) = if t < 0.5 {
let t_scaled = t * 2.0; let r = (255.0 * t_scaled) as u8;
(r, 255, 0)
} else {
let t_scaled = (t - 0.5) * 2.0; let g = (255.0 * (1.0 - t_scaled)) as u8;
(255, g, 0)
};
(duration_str, format!("#{:02X}{:02X}{:02X}", r, g, b))
} else {
("Cached".to_string(), "#ADD8E6".to_string()) };
writeln!(f, " {:?}[\"{}\\n{}\"]", index.index(), name, label_extra).unwrap();
writeln!(f, " style {:?} fill:{}", index.index(), color_code).unwrap();
if task.is_output() {
writeln!(f, " {:?} --> Output", index.index()).unwrap();
}
}
writeln!(f, " Output[Output]").unwrap();
for edge in site.graph.edge_indices() {
let (source, target) = site.graph.edge_endpoints(edge).unwrap();
let source_task = &site.graph[source];
let type_name = source_task
.get_output_type_name()
.replace('<', "<")
.replace('>', ">");
writeln!(
f,
" {:?} -- \"{}\" --> {:?}",
source.index(),
type_name,
target.index()
)
.unwrap();
}
f
}
}