use crate::profiler::ExecutionProfile;
use std::fs::File;
use std::io::Write;
pub fn generate_dot_graph(
tasks: &[(usize, String)],
dependencies: &[(usize, usize)],
) -> String {
let mut dot = String::from("digraph Taskflow {\n");
dot.push_str(" rankdir=LR;\n");
dot.push_str(" node [shape=box, style=rounded];\n\n");
for (id, name) in tasks {
dot.push_str(&format!(" {} [label=\"{}\"];\n", id, name));
}
dot.push_str("\n");
for (from, to) in dependencies {
dot.push_str(&format!(" {} -> {};\n", from, to));
}
dot.push_str("}\n");
dot
}
pub fn save_dot_graph(dot: &str, filename: &str) -> std::io::Result<()> {
let mut file = File::create(filename)?;
file.write_all(dot.as_bytes())?;
Ok(())
}
pub fn generate_timeline_svg(profile: &ExecutionProfile) -> String {
let width = 1200;
let height = 100 + profile.num_workers * 60;
let margin_left = 150;
let margin_top = 50;
let timeline_width = width - margin_left - 50;
let mut svg = String::new();
svg.push_str(&format!(
"<svg width=\"{}\" height=\"{}\" xmlns=\"http://www.w3.org/2000/svg\">\n",
width, height
));
svg.push_str(&format!(
" <text x=\"{}\" y=\"30\" font-size=\"20\" font-weight=\"bold\">Execution Timeline</text>\n",
width / 2 - 80
));
let timeline = profile.worker_timeline();
let max_time = profile.total_duration.as_secs_f64();
for worker_id in 0..profile.num_workers {
let y = margin_top + worker_id * 60;
svg.push_str(&format!(
" <text x=\"10\" y=\"{}\" font-size=\"14\">Worker {}</text>\n",
y + 25, worker_id
));
svg.push_str(&format!(
" <rect x=\"{}\" y=\"{}\" width=\"{}\" height=\"40\" fill=\"#f0f0f0\" stroke=\"#999\"/>\n",
margin_left, y, timeline_width
));
if let Some(tasks) = timeline.get(&worker_id) {
for task in tasks {
let start_offset = (task.start_time - profile.start_time).as_secs_f64();
let duration = task.duration.as_secs_f64();
let x = margin_left + (start_offset / max_time * timeline_width as f64) as i32;
let task_width = ((duration / max_time * timeline_width as f64) as i32).max(2);
let color = if duration > max_time * 0.3 {
"#e74c3c" } else if duration > max_time * 0.1 {
"#f39c12" } else {
"#2ecc71" };
svg.push_str(&format!(
" <rect x=\"{}\" y=\"{}\" width=\"{}\" height=\"35\" fill=\"{}\" stroke=\"#333\"/>\n",
x, y + 2, task_width, color
));
if task_width > 30 {
let label_text = match &task.name {
Some(name) => name.clone(),
None => format!("T{}", task.task_id),
};
svg.push_str(&format!(
" <text x=\"{}\" y=\"{}\" font-size=\"10\" fill=\"white\">{}</text>\n",
x + 5, y + 22, label_text
));
}
}
}
}
let axis_y = margin_top + profile.num_workers * 60 + 20;
svg.push_str(&format!(
" <line x1=\"{}\" y1=\"{}\" x2=\"{}\" y2=\"{}\" stroke=\"#333\" stroke-width=\"2\"/>\n",
margin_left, axis_y, margin_left + timeline_width, axis_y
));
for i in 0..=10 {
let x = margin_left + (i * timeline_width / 10);
let time = (i as f64 / 10.0) * max_time;
svg.push_str(&format!(
" <line x1=\"{}\" y1=\"{}\" x2=\"{}\" y2=\"{}\" stroke=\"#333\"/>\n",
x, axis_y - 5, x, axis_y + 5
));
svg.push_str(&format!(
" <text x=\"{}\" y=\"{}\" font-size=\"10\" text-anchor=\"middle\">{:.2}s</text>\n",
x, axis_y + 20, time
));
}
svg.push_str("</svg>\n");
svg
}
pub fn save_timeline_svg(svg: &str, filename: &str) -> std::io::Result<()> {
let mut file = File::create(filename)?;
file.write_all(svg.as_bytes())?;
Ok(())
}
pub fn generate_html_report(profile: &ExecutionProfile) -> String {
let mut html = String::new();
html.push_str("<!DOCTYPE html>\n<html>\n<head>\n");
html.push_str(" <meta charset=\"UTF-8\">\n");
html.push_str(" <title>TaskFlow Execution Report</title>\n");
html.push_str(" <style>\n");
html.push_str(" body { font-family: Arial, sans-serif; margin: 20px; background: #f5f5f5; }\n");
html.push_str(" .container { max-width: 1200px; margin: 0 auto; background: white; padding: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }\n");
html.push_str(" h1 { color: #333; border-bottom: 2px solid #2ecc71; padding-bottom: 10px; }\n");
html.push_str(" .stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin: 20px 0; }\n");
html.push_str(" .stat-box { background: #ecf0f1; padding: 15px; border-radius: 5px; }\n");
html.push_str(" .stat-label { font-size: 12px; color: #7f8c8d; }\n");
html.push_str(" .stat-value { font-size: 24px; font-weight: bold; color: #2c3e50; }\n");
html.push_str(" .timeline { margin: 20px 0; overflow-x: auto; }\n");
html.push_str(" table { width: 100%; border-collapse: collapse; margin: 20px 0; }\n");
html.push_str(" th, td { padding: 10px; text-align: left; border-bottom: 1px solid #ddd; }\n");
html.push_str(" th { background: #34495e; color: white; }\n");
html.push_str(" </style>\n");
html.push_str("</head>\n<body>\n");
html.push_str(" <div class=\"container\">\n");
html.push_str(" <h1>TaskFlow Execution Report</h1>\n");
html.push_str(" <div class=\"stats\">\n");
html.push_str(&format!(
" <div class=\"stat-box\"><div class=\"stat-label\">Total Duration</div><div class=\"stat-value\">{:.2}s</div></div>\n",
profile.total_duration.as_secs_f64()
));
html.push_str(&format!(
" <div class=\"stat-box\"><div class=\"stat-label\">Tasks Executed</div><div class=\"stat-value\">{}</div></div>\n",
profile.task_stats.len()
));
html.push_str(&format!(
" <div class=\"stat-box\"><div class=\"stat-label\">Workers Used</div><div class=\"stat-value\">{}</div></div>\n",
profile.num_workers
));
html.push_str(&format!(
" <div class=\"stat-box\"><div class=\"stat-label\">Parallelism</div><div class=\"stat-value\">{:.1}%</div></div>\n",
profile.parallelism_efficiency()
));
html.push_str(" </div>\n");
html.push_str(" <h2>Execution Timeline</h2>\n");
html.push_str(" <div class=\"timeline\">\n");
html.push_str(&generate_timeline_svg(profile));
html.push_str(" </div>\n");
html.push_str(" <h2>Task Details</h2>\n");
html.push_str(" <table>\n");
html.push_str(" <tr><th>Task ID</th><th>Name</th><th>Duration</th><th>Worker</th></tr>\n");
let mut sorted_tasks = profile.task_stats.clone();
sorted_tasks.sort_by_key(|s| s.start_time);
for task in &sorted_tasks {
let name = task.name.as_ref().map(|s| s.as_str()).unwrap_or("-");
html.push_str(&format!(
" <tr><td>{}</td><td>{}</td><td>{:.2}ms</td><td>{}</td></tr>\n",
task.task_id, name, task.duration.as_secs_f64() * 1000.0, task.worker_id
));
}
html.push_str(" </table>\n");
html.push_str(" </div>\n");
html.push_str("</body>\n</html>\n");
html
}
pub fn save_html_report(html: &str, filename: &str) -> std::io::Result<()> {
let mut file = File::create(filename)?;
file.write_all(html.as_bytes())?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::profiler::{ExecutionProfile, TaskStats};
use std::time::{Duration, Instant};
#[test]
fn test_dot_graph_generation() {
let tasks = vec![
(1, "Task A".to_string()),
(2, "Task B".to_string()),
(3, "Task C".to_string()),
];
let deps = vec![(1, 2), (1, 3)];
let dot = generate_dot_graph(&tasks, &deps);
assert!(dot.contains("digraph Taskflow"));
assert!(dot.contains("Task A"));
assert!(dot.contains("1 -> 2"));
}
#[test]
fn test_timeline_svg_generation() {
let start = Instant::now();
let profile = ExecutionProfile {
start_time: start,
total_duration: Duration::from_secs(1),
task_stats: vec![
TaskStats {
task_id: 1,
name: Some("task1".to_string()),
start_time: start,
duration: Duration::from_millis(100),
worker_id: 0,
num_dependencies: 0,
},
],
num_workers: 2,
};
let svg = generate_timeline_svg(&profile);
assert!(svg.contains("<svg"));
assert!(svg.contains("Execution Timeline"));
}
}