use super::*;
use plotters::prelude::*;
use scirs2_core::ndarray::Array1;
use std::collections::HashMap;
use std::path::Path;
pub struct PerfPlot {
config: PlotConfig,
}
#[derive(Debug, Clone)]
pub struct BenchmarkResult {
pub name: String,
pub time: f64,
pub std_dev: Option<f64>,
}
#[derive(Debug, Clone)]
pub struct ScalingPoint {
pub cores: usize,
pub time: f64,
}
impl PerfPlot {
pub fn new(config: PlotConfig) -> Self {
Self { config }
}
pub fn benchmark_comparison(&self, results: &[BenchmarkResult], path: &Path) -> VizResult<()> {
if results.is_empty() {
return Err(VizError::InvalidData("No benchmark results".to_string()));
}
let root =
BitMapBackend::new(path, (self.config.width, self.config.height)).into_drawing_area();
root.fill(&WHITE)
.map_err(|e| VizError::RenderError(format!("{:?}", e)))?;
let max_time = results
.iter()
.map(|r| r.time)
.fold(f64::NEG_INFINITY, f64::max);
let mut chart = ChartBuilder::on(&root)
.caption(&self.config.title, ("sans-serif", 40))
.margin(10)
.x_label_area_size(80)
.y_label_area_size(50)
.build_cartesian_2d(0..results.len(), 0.0..(max_time * 1.1))
.map_err(|e| VizError::RenderError(format!("{:?}", e)))?;
chart
.configure_mesh()
.y_desc("Time (seconds)")
.draw()
.map_err(|e| VizError::RenderError(format!("{:?}", e)))?;
for (idx, result) in results.iter().enumerate() {
chart
.draw_series(std::iter::once(Rectangle::new(
[(idx, 0.0), (idx + 1, result.time)],
BLUE.mix(0.6).filled(),
)))
.map_err(|e| VizError::RenderError(format!("{:?}", e)))?;
}
root.present()
.map_err(|e| VizError::RenderError(format!("{:?}", e)))?;
Ok(())
}
pub fn speedup_curve(
&self,
scaling_data: &[ScalingPoint],
baseline: f64,
path: &Path,
) -> VizResult<()> {
if scaling_data.is_empty() {
return Err(VizError::InvalidData("No scaling data".to_string()));
}
let root =
BitMapBackend::new(path, (self.config.width, self.config.height)).into_drawing_area();
root.fill(&WHITE)
.map_err(|e| VizError::RenderError(format!("{:?}", e)))?;
let speedups: Vec<(usize, f64)> = scaling_data
.iter()
.map(|p| (p.cores, baseline / p.time))
.collect();
let max_cores = speedups.iter().map(|(c, _)| *c).max().unwrap_or(1);
let max_speedup = speedups
.iter()
.map(|(_, s)| *s)
.fold(f64::NEG_INFINITY, f64::max);
let mut chart = ChartBuilder::on(&root)
.caption(&self.config.title, ("sans-serif", 40))
.margin(10)
.x_label_area_size(40)
.y_label_area_size(50)
.build_cartesian_2d(0.0..(max_cores as f64 * 1.1), 0.0..(max_speedup * 1.1))
.map_err(|e| VizError::RenderError(format!("{:?}", e)))?;
chart
.configure_mesh()
.x_desc("Number of Cores")
.y_desc("Speedup")
.draw()
.map_err(|e| VizError::RenderError(format!("{:?}", e)))?;
chart
.draw_series(LineSeries::new(
speedups.iter().map(|(c, s)| (*c as f64, *s)),
&BLUE,
))
.map_err(|e| VizError::RenderError(format!("{:?}", e)))?
.label("Actual")
.legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], BLUE));
chart
.draw_series(
speedups
.iter()
.map(|(c, s)| Circle::new((*c as f64, *s), 3, BLUE.filled())),
)
.map_err(|e| VizError::RenderError(format!("{:?}", e)))?;
chart
.draw_series(LineSeries::new(
vec![(0.0, 0.0), (max_cores as f64, max_cores as f64)],
&RED,
))
.map_err(|e| VizError::RenderError(format!("{:?}", e)))?
.label("Ideal")
.legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], RED));
chart
.configure_series_labels()
.background_style(WHITE.mix(0.9))
.border_style(BLACK)
.draw()
.map_err(|e| VizError::RenderError(format!("{:?}", e)))?;
root.present()
.map_err(|e| VizError::RenderError(format!("{:?}", e)))?;
Ok(())
}
pub fn efficiency_curve(
&self,
scaling_data: &[ScalingPoint],
baseline: f64,
path: &Path,
) -> VizResult<()> {
if scaling_data.is_empty() {
return Err(VizError::InvalidData("No scaling data".to_string()));
}
let root =
BitMapBackend::new(path, (self.config.width, self.config.height)).into_drawing_area();
root.fill(&WHITE)
.map_err(|e| VizError::RenderError(format!("{:?}", e)))?;
let efficiencies: Vec<(usize, f64)> = scaling_data
.iter()
.map(|p| (p.cores, (baseline / p.time) / p.cores as f64))
.collect();
let max_cores = efficiencies.iter().map(|(c, _)| *c).max().unwrap_or(1);
let mut chart = ChartBuilder::on(&root)
.caption(&self.config.title, ("sans-serif", 40))
.margin(10)
.x_label_area_size(40)
.y_label_area_size(50)
.build_cartesian_2d(0.0..(max_cores as f64 * 1.1), 0.0..1.1)
.map_err(|e| VizError::RenderError(format!("{:?}", e)))?;
chart
.configure_mesh()
.x_desc("Number of Cores")
.y_desc("Efficiency")
.draw()
.map_err(|e| VizError::RenderError(format!("{:?}", e)))?;
chart
.draw_series(LineSeries::new(
efficiencies.iter().map(|(c, e)| (*c as f64, *e)),
&BLUE,
))
.map_err(|e| VizError::RenderError(format!("{:?}", e)))?;
chart
.draw_series(
efficiencies
.iter()
.map(|(c, e)| Circle::new((*c as f64, *e), 3, BLUE.filled())),
)
.map_err(|e| VizError::RenderError(format!("{:?}", e)))?;
chart
.draw_series(LineSeries::new(
vec![(0.0, 1.0), (max_cores as f64, 1.0)],
&RED.mix(0.5),
))
.map_err(|e| VizError::RenderError(format!("{:?}", e)))?;
root.present()
.map_err(|e| VizError::RenderError(format!("{:?}", e)))?;
Ok(())
}
pub fn time_series(&self, times: &[(f64, f64)], path: &Path) -> VizResult<()> {
if times.is_empty() {
return Err(VizError::InvalidData("No time series data".to_string()));
}
let root =
BitMapBackend::new(path, (self.config.width, self.config.height)).into_drawing_area();
root.fill(&WHITE)
.map_err(|e| VizError::RenderError(format!("{:?}", e)))?;
let x_min = times.iter().map(|(x, _)| *x).fold(f64::INFINITY, f64::min);
let x_max = times
.iter()
.map(|(x, _)| *x)
.fold(f64::NEG_INFINITY, f64::max);
let y_min = times.iter().map(|(_, y)| *y).fold(f64::INFINITY, f64::min);
let y_max = times
.iter()
.map(|(_, y)| *y)
.fold(f64::NEG_INFINITY, f64::max);
let mut chart = ChartBuilder::on(&root)
.caption(&self.config.title, ("sans-serif", 40))
.margin(10)
.x_label_area_size(40)
.y_label_area_size(50)
.build_cartesian_2d(x_min..x_max, y_min..y_max)
.map_err(|e| VizError::RenderError(format!("{:?}", e)))?;
chart
.configure_mesh()
.x_desc(&self.config.x_axis.label)
.y_desc(&self.config.y_axis.label)
.draw()
.map_err(|e| VizError::RenderError(format!("{:?}", e)))?;
chart
.draw_series(LineSeries::new(times.iter().copied(), &BLUE))
.map_err(|e| VizError::RenderError(format!("{:?}", e)))?;
root.present()
.map_err(|e| VizError::RenderError(format!("{:?}", e)))?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_perf_plot_creation() {
let config = PlotConfig::default();
let _plot = PerfPlot::new(config);
}
#[test]
fn test_empty_benchmarks() {
let config = PlotConfig::default();
let plot = PerfPlot::new(config);
let results = vec![];
let path = std::path::Path::new("/tmp/test.png");
let result = plot.benchmark_comparison(&results, path);
assert!(result.is_err());
}
#[test]
fn test_speedup_computation() {
let baseline = 10.0;
let scaling_data = [
ScalingPoint {
cores: 1,
time: 10.0,
},
ScalingPoint {
cores: 2,
time: 5.0,
},
ScalingPoint {
cores: 4,
time: 2.5,
},
];
let speedup = baseline / scaling_data[2].time;
assert!((speedup - 4.0).abs() < 1e-10);
}
}