mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-04-30 01:20:00 -07:00
global: snapshot
This commit is contained in:
251
crates/brk_bencher_visualizer/src/chart.rs
Normal file
251
crates/brk_bencher_visualizer/src/chart.rs
Normal file
@@ -0,0 +1,251 @@
|
||||
use crate::data::{DataPoint, DualRun, Result, Run};
|
||||
use crate::format;
|
||||
use plotters::prelude::*;
|
||||
use std::path::Path;
|
||||
|
||||
const FONT: &str = "monospace";
|
||||
const FONT_SIZE: i32 = 20;
|
||||
const FONT_SIZE_BIG: i32 = 30;
|
||||
const SIZE: (u32, u32) = (2000, 1000);
|
||||
const TIME_BUFFER_MS: u64 = 10_000;
|
||||
|
||||
const BG_COLOR: RGBColor = RGBColor(18, 18, 24);
|
||||
const TEXT_COLOR: RGBColor = RGBColor(230, 230, 240);
|
||||
const COLORS: [RGBColor; 6] = [
|
||||
RGBColor(255, 99, 132), // Pink/Red
|
||||
RGBColor(54, 162, 235), // Blue
|
||||
RGBColor(75, 192, 192), // Teal
|
||||
RGBColor(255, 206, 86), // Yellow
|
||||
RGBColor(153, 102, 255), // Purple
|
||||
RGBColor(255, 159, 64), // Orange
|
||||
];
|
||||
|
||||
pub enum YAxisFormat {
|
||||
Bytes,
|
||||
Number,
|
||||
}
|
||||
|
||||
pub struct ChartConfig<'a> {
|
||||
pub output_path: &'a Path,
|
||||
pub title: String,
|
||||
pub y_label: String,
|
||||
pub y_format: YAxisFormat,
|
||||
}
|
||||
|
||||
/// Generate a simple line chart from runs
|
||||
pub fn generate(config: ChartConfig, runs: &[Run]) -> Result<()> {
|
||||
if runs.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let max_time_ms = runs.iter().map(|r| r.max_timestamp()).max().unwrap_or(1000) + TIME_BUFFER_MS;
|
||||
let max_time_s = max_time_ms as f64 / 1000.0;
|
||||
let max_value = runs.iter().map(|r| r.max_value()).fold(0.0, f64::max);
|
||||
|
||||
let (time_scaled, time_divisor, time_label) = format::time(max_time_s);
|
||||
let (value_scaled, scale_factor, y_label) = scale_y_axis(max_value, &config.y_label, &config.y_format);
|
||||
let x_labels = label_count(time_scaled);
|
||||
|
||||
let root = SVGBackend::new(config.output_path, SIZE).into_drawing_area();
|
||||
root.fill(&BG_COLOR)?;
|
||||
|
||||
let mut chart = ChartBuilder::on(&root)
|
||||
.caption(&config.title, (FONT, FONT_SIZE_BIG).into_font().color(&TEXT_COLOR))
|
||||
.margin(20)
|
||||
.margin_right(40)
|
||||
.x_label_area_size(50)
|
||||
.margin_left(50)
|
||||
.right_y_label_area_size(75)
|
||||
.build_cartesian_2d(0.0..time_scaled * 1.025, 0.0..value_scaled * 1.1)?;
|
||||
|
||||
configure_mesh(&mut chart, time_label, &y_label, &config.y_format, x_labels)?;
|
||||
|
||||
for (idx, run) in runs.iter().enumerate() {
|
||||
let color = COLORS[idx % COLORS.len()];
|
||||
draw_series(&mut chart, &run.data, &run.id, color, time_divisor, scale_factor)?;
|
||||
}
|
||||
|
||||
configure_legend(&mut chart)?;
|
||||
root.present()?;
|
||||
println!("Generated: {}", config.output_path.display());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Generate a chart with dual series per run (e.g., current + peak memory)
|
||||
pub fn generate_dual(
|
||||
config: ChartConfig,
|
||||
runs: &[DualRun],
|
||||
primary_suffix: &str,
|
||||
secondary_suffix: &str,
|
||||
) -> Result<()> {
|
||||
if runs.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let max_time_ms = runs
|
||||
.iter()
|
||||
.flat_map(|r| r.primary.iter().chain(r.secondary.iter()))
|
||||
.map(|d| d.timestamp_ms)
|
||||
.max()
|
||||
.unwrap_or(1000)
|
||||
+ TIME_BUFFER_MS;
|
||||
let max_time_s = max_time_ms as f64 / 1000.0;
|
||||
let max_value = runs.iter().map(|r| r.max_value()).fold(0.0, f64::max);
|
||||
|
||||
let (time_scaled, time_divisor, time_label) = format::time(max_time_s);
|
||||
let (value_scaled, scale_factor, y_label) = scale_y_axis(max_value, &config.y_label, &config.y_format);
|
||||
let x_labels = label_count(time_scaled);
|
||||
|
||||
let root = SVGBackend::new(config.output_path, SIZE).into_drawing_area();
|
||||
root.fill(&BG_COLOR)?;
|
||||
|
||||
let mut chart = ChartBuilder::on(&root)
|
||||
.caption(&config.title, (FONT, FONT_SIZE_BIG).into_font().color(&TEXT_COLOR))
|
||||
.margin(20)
|
||||
.margin_right(40)
|
||||
.x_label_area_size(50)
|
||||
.margin_left(50)
|
||||
.right_y_label_area_size(75)
|
||||
.build_cartesian_2d(0.0..time_scaled * 1.025, 0.0..value_scaled * 1.1)?;
|
||||
|
||||
configure_mesh(&mut chart, time_label, &y_label, &config.y_format, x_labels)?;
|
||||
|
||||
for (idx, run) in runs.iter().enumerate() {
|
||||
let color = COLORS[idx % COLORS.len()];
|
||||
|
||||
// Primary series (solid)
|
||||
draw_series(
|
||||
&mut chart,
|
||||
&run.primary,
|
||||
&format!("{} {}", run.id, primary_suffix),
|
||||
color,
|
||||
time_divisor,
|
||||
scale_factor,
|
||||
)?;
|
||||
|
||||
// Secondary series (dashed)
|
||||
draw_dashed_series(
|
||||
&mut chart,
|
||||
&run.secondary,
|
||||
&format!("{} {}", run.id, secondary_suffix),
|
||||
color.mix(0.5),
|
||||
time_divisor,
|
||||
scale_factor,
|
||||
)?;
|
||||
}
|
||||
|
||||
configure_legend(&mut chart)?;
|
||||
root.present()?;
|
||||
println!("Generated: {}", config.output_path.display());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn scale_y_axis(max_value: f64, base_label: &str, y_format: &YAxisFormat) -> (f64, f64, String) {
|
||||
match y_format {
|
||||
YAxisFormat::Bytes => {
|
||||
let (scaled, unit) = format::bytes(max_value);
|
||||
let factor = max_value / scaled;
|
||||
(scaled, factor, format!("{} ({})", base_label, unit))
|
||||
}
|
||||
YAxisFormat::Number => (max_value, 1.0, base_label.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate appropriate label count to avoid duplicates when rounding to integers
|
||||
fn label_count(max_value: f64) -> usize {
|
||||
let max_int = max_value.ceil() as usize;
|
||||
// Don't exceed the range, cap at 12 for readability
|
||||
max_int.clamp(2, 12)
|
||||
}
|
||||
|
||||
type Chart<'a, 'b> = ChartContext<
|
||||
'a,
|
||||
SVGBackend<'b>,
|
||||
Cartesian2d<plotters::coord::types::RangedCoordf64, plotters::coord::types::RangedCoordf64>,
|
||||
>;
|
||||
|
||||
fn configure_mesh(chart: &mut Chart, x_label: &str, y_label: &str, y_format: &YAxisFormat, x_labels: usize) -> Result<()> {
|
||||
let y_formatter: Box<dyn Fn(&f64) -> String> = match y_format {
|
||||
YAxisFormat::Bytes => Box::new(|y: &f64| {
|
||||
if y.fract() == 0.0 {
|
||||
format!("{:.0}", y)
|
||||
} else {
|
||||
format!("{:.1}", y)
|
||||
}
|
||||
}),
|
||||
YAxisFormat::Number => Box::new(|y: &f64| format::axis_number(*y)),
|
||||
};
|
||||
|
||||
chart
|
||||
.configure_mesh()
|
||||
.disable_mesh()
|
||||
.x_desc(x_label)
|
||||
.y_desc(y_label)
|
||||
.x_label_formatter(&|x| format!("{:.0}", x))
|
||||
.y_label_formatter(&y_formatter)
|
||||
.x_labels(x_labels)
|
||||
.y_labels(10)
|
||||
.x_label_style((FONT, FONT_SIZE).into_font().color(&TEXT_COLOR.mix(0.7)))
|
||||
.y_label_style((FONT, FONT_SIZE).into_font().color(&TEXT_COLOR.mix(0.7)))
|
||||
.axis_style(TEXT_COLOR.mix(0.3))
|
||||
.draw()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw_series(
|
||||
chart: &mut Chart,
|
||||
data: &[DataPoint],
|
||||
label: &str,
|
||||
color: RGBColor,
|
||||
time_divisor: f64,
|
||||
scale_factor: f64,
|
||||
) -> Result<()> {
|
||||
let points = data
|
||||
.iter()
|
||||
.map(|d| (d.timestamp_ms as f64 / 1000.0 / time_divisor, d.value / scale_factor));
|
||||
|
||||
chart
|
||||
.draw_series(LineSeries::new(points, color.stroke_width(1)))?
|
||||
.label(label)
|
||||
.legend(move |(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], color.stroke_width(1)));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw_dashed_series(
|
||||
chart: &mut Chart,
|
||||
data: &[DataPoint],
|
||||
label: &str,
|
||||
color: RGBAColor,
|
||||
time_divisor: f64,
|
||||
scale_factor: f64,
|
||||
) -> Result<()> {
|
||||
let points: Vec<_> = data
|
||||
.iter()
|
||||
.map(|d| (d.timestamp_ms as f64 / 1000.0 / time_divisor, d.value / scale_factor))
|
||||
.collect();
|
||||
|
||||
// Draw dashed line by skipping every other segment
|
||||
chart
|
||||
.draw_series(
|
||||
points
|
||||
.windows(2)
|
||||
.enumerate()
|
||||
.filter(|(i, _)| i % 2 == 0)
|
||||
.map(|(_, w)| PathElement::new(vec![w[0], w[1]], color.stroke_width(2))),
|
||||
)?
|
||||
.label(label)
|
||||
.legend(move |(x, y)| PathElement::new(vec![(x, y), (x + 10, y), (x + 20, y)], color.stroke_width(2)));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn configure_legend<'a>(chart: &mut Chart<'a, 'a>) -> Result<()> {
|
||||
chart
|
||||
.configure_series_labels()
|
||||
.position(SeriesLabelPosition::UpperLeft)
|
||||
.label_font((FONT, FONT_SIZE).into_font().color(&TEXT_COLOR.mix(0.9)))
|
||||
.background_style(BG_COLOR.mix(0.98))
|
||||
.border_style(BG_COLOR)
|
||||
.margin(10)
|
||||
.draw()?;
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user