mod chart; mod data; mod format; use data::{Cutoffs, DualRun, Result, Run, read_dual_runs, read_runs}; use std::{ fs, path::{Path, PathBuf}, }; pub struct Visualizer { workspace_root: PathBuf, } impl Visualizer { pub fn new(workspace_root: impl AsRef) -> Self { Self { workspace_root: workspace_root.as_ref().to_path_buf(), } } pub fn from_cargo_env() -> Result { let workspace_root = Path::new(env!("CARGO_MANIFEST_DIR")) .parent() .and_then(|p| p.parent()) .ok_or("Failed to find workspace root")? .to_path_buf(); Ok(Self { workspace_root }) } pub fn generate_all_charts(&self) -> Result<()> { let benches_dir = self.workspace_root.join("benches"); if !benches_dir.exists() { return Err("Benches directory does not exist".into()); } for entry in fs::read_dir(&benches_dir)? { let path = entry?.path(); if path.is_dir() { let crate_name = path .file_name() .and_then(|n| n.to_str()) .ok_or("Invalid crate name")?; println!("Generating charts for crate: {}", crate_name); self.generate_crate_charts(&path, crate_name)?; } } Ok(()) } fn generate_crate_charts(&self, crate_path: &Path, crate_name: &str) -> Result<()> { let disk_runs = read_runs(crate_path, "disk.csv")?; let memory_runs = read_dual_runs(crate_path, "memory.csv")?; let progress_runs = read_runs(crate_path, "progress.csv")?; let io_runs = read_dual_runs(crate_path, "io.csv")?; // Combined charts (all runs) self.generate_combined_charts( crate_path, crate_name, &disk_runs, &memory_runs, &progress_runs, &io_runs, )?; // Individual charts (one per run) self.generate_individual_charts( crate_path, crate_name, &disk_runs, &memory_runs, &progress_runs, &io_runs, )?; Ok(()) } fn generate_combined_charts( &self, crate_path: &Path, crate_name: &str, disk_runs: &[Run], memory_runs: &[DualRun], progress_runs: &[Run], io_runs: &[DualRun], ) -> Result<()> { let cutoffs = Cutoffs::from_progress(progress_runs); // Trim data to per-run cutoffs for fair comparison let disk_trimmed = cutoffs.trim_runs(disk_runs); let memory_trimmed = cutoffs.trim_dual_runs(memory_runs); let io_trimmed = cutoffs.trim_dual_runs(io_runs); if !disk_trimmed.is_empty() { chart::generate( chart::ChartConfig { output_path: &crate_path.join("disk.svg"), title: format!("{} — Disk Usage", crate_name), y_label: "Disk Usage".to_string(), y_format: chart::YAxisFormat::Bytes, }, &disk_trimmed, )?; } if !memory_trimmed.is_empty() { chart::generate_dual( chart::ChartConfig { output_path: &crate_path.join("memory.svg"), title: format!("{} — Memory", crate_name), y_label: "Memory".to_string(), y_format: chart::YAxisFormat::Bytes, }, &memory_trimmed, "(current)", "(peak)", )?; } if !progress_runs.is_empty() { let progress_trimmed = cutoffs.trim_runs(progress_runs); chart::generate( chart::ChartConfig { output_path: &crate_path.join("progress.svg"), title: format!("{} — Progress", crate_name), y_label: "Progress".to_string(), y_format: chart::YAxisFormat::Number, }, &progress_trimmed, )?; } if !io_trimmed.is_empty() { // I/O Read (primary column) let io_read: Vec<_> = io_trimmed .iter() .map(|r| Run { id: r.id.clone(), data: r.primary.clone(), }) .collect(); chart::generate( chart::ChartConfig { output_path: &crate_path.join("io_read.svg"), title: format!("{} — I/O Read", crate_name), y_label: "Bytes Read".to_string(), y_format: chart::YAxisFormat::Bytes, }, &io_read, )?; // I/O Write (secondary column) let io_write: Vec<_> = io_trimmed .iter() .map(|r| Run { id: r.id.clone(), data: r.secondary.clone(), }) .collect(); chart::generate( chart::ChartConfig { output_path: &crate_path.join("io_write.svg"), title: format!("{} — I/O Write", crate_name), y_label: "Bytes Written".to_string(), y_format: chart::YAxisFormat::Bytes, }, &io_write, )?; } Ok(()) } fn generate_individual_charts( &self, crate_path: &Path, crate_name: &str, disk_runs: &[Run], memory_runs: &[DualRun], progress_runs: &[Run], io_runs: &[DualRun], ) -> Result<()> { for run in disk_runs { let run_path = crate_path.join(&run.id); chart::generate( chart::ChartConfig { output_path: &run_path.join("disk.svg"), title: format!("{} — Disk Usage", crate_name), y_label: "Disk Usage".to_string(), y_format: chart::YAxisFormat::Bytes, }, std::slice::from_ref(run), )?; } for run in memory_runs { let run_path = crate_path.join(&run.id); chart::generate_dual( chart::ChartConfig { output_path: &run_path.join("memory.svg"), title: format!("{} — Memory", crate_name), y_label: "Memory".to_string(), y_format: chart::YAxisFormat::Bytes, }, std::slice::from_ref(run), "(current)", "(peak)", )?; } for run in progress_runs { let run_path = crate_path.join(&run.id); chart::generate( chart::ChartConfig { output_path: &run_path.join("progress.svg"), title: format!("{} — Progress", crate_name), y_label: "Progress".to_string(), y_format: chart::YAxisFormat::Number, }, std::slice::from_ref(run), )?; } for run in io_runs { let run_path = crate_path.join(&run.id); let read_run = Run { id: run.id.clone(), data: run.primary.clone(), }; chart::generate( chart::ChartConfig { output_path: &run_path.join("io_read.svg"), title: format!("{} — I/O Read", crate_name), y_label: "Bytes Read".to_string(), y_format: chart::YAxisFormat::Bytes, }, std::slice::from_ref(&read_run), )?; let write_run = Run { id: run.id.clone(), data: run.secondary.clone(), }; chart::generate( chart::ChartConfig { output_path: &run_path.join("io_write.svg"), title: format!("{} — I/O Write", crate_name), y_label: "Bytes Written".to_string(), y_format: chart::YAxisFormat::Bytes, }, std::slice::from_ref(&write_run), )?; } Ok(()) } }