mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-07-18 22:48:10 -07:00
global: snapshot
This commit is contained in:
@@ -6,6 +6,26 @@ use std::{
|
||||
|
||||
type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
|
||||
|
||||
const FONT: &str = "monospace";
|
||||
|
||||
macro_rules! configure_chart_mesh {
|
||||
($chart:expr, $x_desc:expr, $y_desc:expr, $y_formatter:expr) => {
|
||||
$chart
|
||||
.configure_mesh()
|
||||
.disable_mesh()
|
||||
.x_desc($x_desc)
|
||||
.y_desc($y_desc)
|
||||
.x_label_formatter(&|x| format!("{:.0}", x))
|
||||
.y_label_formatter(&$y_formatter)
|
||||
.x_labels(12)
|
||||
.y_labels(10)
|
||||
.x_label_style((FONT, 16).into_font().color(&TEXT_COLOR.mix(0.7)))
|
||||
.y_label_style((FONT, 16).into_font().color(&TEXT_COLOR.mix(0.7)))
|
||||
.axis_style(TEXT_COLOR.mix(0.3))
|
||||
.draw()?
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct DataPoint {
|
||||
timestamp_ms: u64,
|
||||
@@ -30,6 +50,9 @@ const CHART_COLORS: [RGBColor; 6] = [
|
||||
RGBColor(255, 159, 64), // Orange
|
||||
];
|
||||
|
||||
// Time window buffer in milliseconds (5 seconds)
|
||||
const TIME_BUFFER_MS: u64 = 5000;
|
||||
|
||||
pub struct Visualizer {
|
||||
workspace_root: PathBuf,
|
||||
}
|
||||
@@ -50,7 +73,6 @@ impl Visualizer {
|
||||
Ok(Self { workspace_root })
|
||||
}
|
||||
|
||||
/// Generate all charts for all crates in the benches directory
|
||||
pub fn generate_all_charts(&self) -> Result<()> {
|
||||
let benches_dir = self.workspace_root.join("benches");
|
||||
|
||||
@@ -58,7 +80,6 @@ impl Visualizer {
|
||||
return Err("Benches directory does not exist".into());
|
||||
}
|
||||
|
||||
// Iterate through each crate directory
|
||||
for entry in fs::read_dir(&benches_dir)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
@@ -77,11 +98,10 @@ impl Visualizer {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Generate charts for a specific crate
|
||||
fn generate_crate_charts(&self, crate_path: &Path, crate_name: &str) -> Result<()> {
|
||||
// Read all benchmark runs for this crate
|
||||
let disk_runs = self.read_benchmark_runs(crate_path, "disk_usage.csv")?;
|
||||
let memory_runs = self.read_benchmark_runs(crate_path, "memory_footprint.csv")?;
|
||||
let disk_runs = self.read_benchmark_runs(crate_path, "disk.csv")?;
|
||||
let memory_runs = self.read_benchmark_runs(crate_path, "memory.csv")?;
|
||||
let progress_runs = self.read_benchmark_runs(crate_path, "progress.csv")?;
|
||||
|
||||
if !disk_runs.is_empty() {
|
||||
self.generate_disk_chart(crate_path, crate_name, &disk_runs)?;
|
||||
@@ -91,10 +111,13 @@ impl Visualizer {
|
||||
self.generate_memory_chart(crate_path, crate_name, &memory_runs)?;
|
||||
}
|
||||
|
||||
if !progress_runs.is_empty() {
|
||||
self.generate_progress_chart(crate_path, crate_name, &progress_runs)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read all benchmark runs from subdirectories
|
||||
fn read_benchmark_runs(&self, crate_path: &Path, filename: &str) -> Result<Vec<BenchmarkRun>> {
|
||||
let mut runs = Vec::new();
|
||||
|
||||
@@ -109,10 +132,15 @@ impl Visualizer {
|
||||
.ok_or("Invalid run ID")?
|
||||
.to_string();
|
||||
|
||||
// Skip directories that start with underscore
|
||||
if run_id.starts_with('_') {
|
||||
continue;
|
||||
}
|
||||
|
||||
let csv_path = run_path.join(filename);
|
||||
|
||||
if csv_path.exists()
|
||||
&& let Ok(data) = self.read_csv(&csv_path)
|
||||
&& let Ok(data) = Self::read_csv(&csv_path, 2)
|
||||
{
|
||||
runs.push(BenchmarkRun { run_id, data });
|
||||
}
|
||||
@@ -122,18 +150,17 @@ impl Visualizer {
|
||||
Ok(runs)
|
||||
}
|
||||
|
||||
/// Read a CSV file and parse data points
|
||||
fn read_csv(&self, path: &Path) -> Result<Vec<DataPoint>> {
|
||||
fn read_csv(path: &Path, expected_columns: usize) -> Result<Vec<DataPoint>> {
|
||||
let content = fs::read_to_string(path)?;
|
||||
let mut data = Vec::new();
|
||||
|
||||
for (i, line) in content.lines().enumerate() {
|
||||
if i == 0 {
|
||||
continue;
|
||||
} // Skip header
|
||||
}
|
||||
|
||||
let parts: Vec<&str> = line.split(',').collect();
|
||||
if parts.len() >= 2
|
||||
if parts.len() >= expected_columns
|
||||
&& let (Ok(timestamp_ms), Ok(value)) =
|
||||
(parts[0].parse::<u64>(), parts[1].parse::<f64>())
|
||||
{
|
||||
@@ -147,107 +174,325 @@ impl Visualizer {
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
/// Generate disk usage chart
|
||||
// Helper methods
|
||||
|
||||
fn format_bytes(bytes: f64) -> (f64, &'static str) {
|
||||
const KIB: f64 = 1024.0;
|
||||
const MIB: f64 = 1024.0 * 1024.0;
|
||||
const GIB: f64 = 1024.0 * 1024.0 * 1024.0;
|
||||
|
||||
if bytes >= GIB {
|
||||
(bytes / GIB, "GiB")
|
||||
} else if bytes >= MIB {
|
||||
(bytes / MIB, "MiB")
|
||||
} else if bytes >= KIB {
|
||||
(bytes / KIB, "KiB")
|
||||
} else {
|
||||
(bytes, "bytes")
|
||||
}
|
||||
}
|
||||
|
||||
fn format_time(time_s: f64) -> (f64, &'static str, &'static str) {
|
||||
const MINUTE: f64 = 60.0;
|
||||
const HOUR: f64 = 3600.0;
|
||||
|
||||
// Only use larger units if the value would be >= 2 (to avoid decimals)
|
||||
if time_s >= HOUR * 2.0 {
|
||||
(time_s / HOUR, "h", "Time (h)")
|
||||
} else if time_s >= MINUTE * 2.0 {
|
||||
(time_s / MINUTE, "min", "Time (min)")
|
||||
} else {
|
||||
(time_s, "s", "Time (s)")
|
||||
}
|
||||
}
|
||||
|
||||
fn format_axis_number(value: f64) -> String {
|
||||
if value >= 1000.0 {
|
||||
let k_value = value / 1000.0;
|
||||
// Show decimals only if needed
|
||||
if k_value.fract() == 0.0 || k_value >= 100.0 {
|
||||
format!("{:.0}k", k_value)
|
||||
} else if k_value >= 10.0 {
|
||||
format!("{:.1}k", k_value)
|
||||
} else {
|
||||
format!("{:.2}k", k_value)
|
||||
}
|
||||
} else {
|
||||
format!("{:.0}", value)
|
||||
}
|
||||
}
|
||||
|
||||
fn calculate_min_max_time(runs: &[BenchmarkRun]) -> u64 {
|
||||
runs.iter()
|
||||
.filter_map(|r| r.data.iter().map(|d| d.timestamp_ms).max())
|
||||
.min()
|
||||
.unwrap_or(1000)
|
||||
}
|
||||
|
||||
fn calculate_max_value(runs: &[BenchmarkRun]) -> f64 {
|
||||
runs.iter()
|
||||
.flat_map(|r| r.data.iter().map(|d| d.value))
|
||||
.fold(0.0_f64, f64::max)
|
||||
}
|
||||
|
||||
fn trim_runs_to_time_window(runs: &[BenchmarkRun], max_time_ms: u64) -> Vec<BenchmarkRun> {
|
||||
runs.iter()
|
||||
.map(|run| BenchmarkRun {
|
||||
run_id: run.run_id.clone(),
|
||||
data: run
|
||||
.data
|
||||
.iter()
|
||||
.filter(|d| d.timestamp_ms <= max_time_ms)
|
||||
.cloned()
|
||||
.collect(),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn draw_line_series<'a, DB: DrawingBackend, I>(
|
||||
chart: &mut ChartContext<
|
||||
'a,
|
||||
DB,
|
||||
Cartesian2d<
|
||||
plotters::coord::types::RangedCoordf64,
|
||||
plotters::coord::types::RangedCoordf64,
|
||||
>,
|
||||
>,
|
||||
data: I,
|
||||
label: &str,
|
||||
color: RGBColor,
|
||||
) -> Result<()>
|
||||
where
|
||||
I: Iterator<Item = (f64, f64)> + Clone,
|
||||
DB::ErrorType: 'static,
|
||||
{
|
||||
chart
|
||||
.draw_series(LineSeries::new(data, color.stroke_width(1)))?
|
||||
.label(label)
|
||||
.legend(move |(x, y)| {
|
||||
PathElement::new(vec![(x, y), (x + 20, y)], color.stroke_width(1))
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn configure_series_labels<'a, DB: DrawingBackend + 'a>(
|
||||
chart: &mut ChartContext<
|
||||
'a,
|
||||
DB,
|
||||
Cartesian2d<
|
||||
plotters::coord::types::RangedCoordf64,
|
||||
plotters::coord::types::RangedCoordf64,
|
||||
>,
|
||||
>,
|
||||
) -> Result<()>
|
||||
where
|
||||
DB::ErrorType: 'static,
|
||||
{
|
||||
chart
|
||||
.configure_series_labels()
|
||||
.position(SeriesLabelPosition::UpperLeft)
|
||||
.label_font((FONT, 16).into_font().color(&TEXT_COLOR.mix(0.9)))
|
||||
.background_style(BG_COLOR.mix(0.98))
|
||||
.border_style(BG_COLOR)
|
||||
.margin(10)
|
||||
.draw()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Chart generation methods
|
||||
|
||||
fn generate_disk_chart(
|
||||
&self,
|
||||
crate_path: &Path,
|
||||
crate_name: &str,
|
||||
runs: &[BenchmarkRun],
|
||||
) -> Result<()> {
|
||||
let output_path = crate_path.join("disk_usage_chart.svg");
|
||||
|
||||
let output_path = crate_path.join("disk_chart.svg");
|
||||
let root = SVGBackend::new(&output_path, (1200, 700)).into_drawing_area();
|
||||
root.fill(&BG_COLOR)?;
|
||||
|
||||
let max_time = runs
|
||||
.iter()
|
||||
.flat_map(|r| r.data.iter().map(|d| d.timestamp_ms))
|
||||
.max()
|
||||
.unwrap_or(1000);
|
||||
// Calculate time window based on shortest run + buffer
|
||||
let min_max_time_ms = Self::calculate_min_max_time(runs) + TIME_BUFFER_MS;
|
||||
let max_time_s = (min_max_time_ms as f64) / 1000.0;
|
||||
|
||||
let max_value = runs
|
||||
.iter()
|
||||
.flat_map(|r| r.data.iter().map(|d| d.value))
|
||||
.fold(0.0_f64, f64::max);
|
||||
// Trim all runs to the same time window
|
||||
let trimmed_runs = Self::trim_runs_to_time_window(runs, min_max_time_ms);
|
||||
|
||||
// Convert to seconds and GB
|
||||
let max_time_s = (max_time as f64) / 1000.0;
|
||||
let max_value_gb = max_value / 1024.0;
|
||||
let max_value = Self::calculate_max_value(&trimmed_runs);
|
||||
let (max_value_scaled, unit) = Self::format_bytes(max_value);
|
||||
let scale_factor = max_value / max_value_scaled;
|
||||
|
||||
// Format time based on duration
|
||||
let (max_time_scaled, _time_unit, time_label) = Self::format_time(max_time_s);
|
||||
|
||||
let mut chart = ChartBuilder::on(&root)
|
||||
.caption(
|
||||
format!("{} — Disk Usage", crate_name),
|
||||
("SF Mono", 24).into_font().color(&TEXT_COLOR),
|
||||
(FONT, 24).into_font().color(&TEXT_COLOR),
|
||||
)
|
||||
.margin(20)
|
||||
.x_label_area_size(55)
|
||||
.y_label_area_size(75)
|
||||
.build_cartesian_2d(0.0..max_time_s * 1.05, 0.0..max_value_gb * 1.1)?;
|
||||
.x_label_area_size(50)
|
||||
.margin_left(50)
|
||||
.right_y_label_area_size(75)
|
||||
.build_cartesian_2d(0.0..max_time_scaled * 1.025, 0.0..max_value_scaled * 1.1)?;
|
||||
|
||||
chart
|
||||
.configure_mesh()
|
||||
.disable_mesh()
|
||||
.x_desc("Time (s)")
|
||||
.y_desc("Disk Usage (GB)")
|
||||
.x_label_offset(10)
|
||||
.y_label_offset(10)
|
||||
.x_label_formatter(&|x| format!("{:.1}", x))
|
||||
.y_label_formatter(&|y| format!("{:.2}", y))
|
||||
.x_labels(8)
|
||||
.y_labels(6)
|
||||
.x_label_style(("SF Mono", 16).into_font().color(&TEXT_COLOR.mix(0.7)))
|
||||
.y_label_style(("SF Mono", 16).into_font().color(&TEXT_COLOR.mix(0.7)))
|
||||
.axis_style(TEXT_COLOR.mix(0.3))
|
||||
.draw()?;
|
||||
configure_chart_mesh!(
|
||||
chart,
|
||||
time_label,
|
||||
format!("Disk Usage ({})", unit),
|
||||
|y: &f64| format!("{:.2}", y)
|
||||
);
|
||||
|
||||
for (idx, run) in runs.iter().enumerate() {
|
||||
for (idx, run) in trimmed_runs.iter().enumerate() {
|
||||
let color = CHART_COLORS[idx % CHART_COLORS.len()];
|
||||
|
||||
chart
|
||||
.draw_series(LineSeries::new(
|
||||
run.data
|
||||
.iter()
|
||||
.map(|d| (d.timestamp_ms as f64 / 1000.0, d.value / 1024.0)),
|
||||
color.stroke_width(2),
|
||||
))?
|
||||
.label(&run.run_id)
|
||||
.legend(move |(x, y)| {
|
||||
PathElement::new(vec![(x, y), (x + 20, y)], color.stroke_width(2))
|
||||
});
|
||||
let time_divisor = max_time_s / max_time_scaled;
|
||||
Self::draw_line_series(
|
||||
&mut chart,
|
||||
run.data.iter().map(|d| {
|
||||
(
|
||||
d.timestamp_ms as f64 / 1000.0 / time_divisor,
|
||||
d.value / scale_factor,
|
||||
)
|
||||
}),
|
||||
&run.run_id,
|
||||
color,
|
||||
)?;
|
||||
}
|
||||
|
||||
chart
|
||||
.configure_series_labels()
|
||||
.position(SeriesLabelPosition::UpperLeft)
|
||||
.label_font(("SF Mono", 16).into_font().color(&TEXT_COLOR.mix(0.9)))
|
||||
.background_style(BG_COLOR.mix(0.98))
|
||||
.border_style(BG_COLOR)
|
||||
.margin(10)
|
||||
.draw()?;
|
||||
|
||||
Self::configure_series_labels(&mut chart)?;
|
||||
root.present()?;
|
||||
println!("Generated: {}", output_path.display());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Generate memory footprint chart
|
||||
fn generate_memory_chart(
|
||||
&self,
|
||||
crate_path: &Path,
|
||||
crate_name: &str,
|
||||
runs: &[BenchmarkRun],
|
||||
) -> Result<()> {
|
||||
let output_path = crate_path.join("memory_footprint_chart.svg");
|
||||
|
||||
let output_path = crate_path.join("memory_chart.svg");
|
||||
let root = SVGBackend::new(&output_path, (1200, 700)).into_drawing_area();
|
||||
root.fill(&BG_COLOR)?;
|
||||
|
||||
// Calculate time window based on shortest run + buffer
|
||||
let min_max_time_ms = Self::calculate_min_max_time(runs) + TIME_BUFFER_MS;
|
||||
let max_time_s = (min_max_time_ms as f64) / 1000.0;
|
||||
|
||||
// Read memory CSV files which have 3 columns: timestamp, footprint, peak
|
||||
let enhanced_runs = self.read_memory_data(crate_path, runs)?;
|
||||
|
||||
// Trim enhanced runs to the same time window
|
||||
let trimmed_enhanced_runs: Vec<_> = enhanced_runs
|
||||
.into_iter()
|
||||
.map(|(run_id, footprint, peak)| {
|
||||
let trimmed_footprint: Vec<_> = footprint
|
||||
.into_iter()
|
||||
.filter(|d| d.timestamp_ms <= min_max_time_ms)
|
||||
.collect();
|
||||
let trimmed_peak: Vec<_> = peak
|
||||
.into_iter()
|
||||
.filter(|d| d.timestamp_ms <= min_max_time_ms)
|
||||
.collect();
|
||||
(run_id, trimmed_footprint, trimmed_peak)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let max_value = trimmed_enhanced_runs
|
||||
.iter()
|
||||
.flat_map(|(_, f, p)| f.iter().chain(p.iter()).map(|d| d.value))
|
||||
.fold(0.0_f64, f64::max);
|
||||
|
||||
let (max_value_scaled, unit) = Self::format_bytes(max_value);
|
||||
let scale_factor = max_value / max_value_scaled;
|
||||
|
||||
// Format time based on duration
|
||||
let (max_time_scaled, _time_unit, time_label) = Self::format_time(max_time_s);
|
||||
|
||||
let mut chart = ChartBuilder::on(&root)
|
||||
.caption(
|
||||
format!("{} — Memory", crate_name),
|
||||
(FONT, 24).into_font().color(&TEXT_COLOR),
|
||||
)
|
||||
.margin(20)
|
||||
.x_label_area_size(50)
|
||||
.margin_left(50)
|
||||
.right_y_label_area_size(75)
|
||||
.build_cartesian_2d(0.0..max_time_scaled * 1.025, 0.0..max_value_scaled * 1.1)?;
|
||||
|
||||
configure_chart_mesh!(
|
||||
chart,
|
||||
time_label,
|
||||
format!("Memory ({})", unit),
|
||||
|y: &f64| format!("{:.2}", y)
|
||||
);
|
||||
|
||||
let time_divisor = max_time_s / max_time_scaled;
|
||||
|
||||
for (idx, (run_id, footprint_data, peak_data)) in trimmed_enhanced_runs.iter().enumerate() {
|
||||
let color = CHART_COLORS[idx % CHART_COLORS.len()];
|
||||
|
||||
Self::draw_line_series(
|
||||
&mut chart,
|
||||
footprint_data.iter().map(|d| {
|
||||
(
|
||||
d.timestamp_ms as f64 / 1000.0 / time_divisor,
|
||||
d.value / scale_factor,
|
||||
)
|
||||
}),
|
||||
&format!("{} (current)", run_id),
|
||||
color,
|
||||
)?;
|
||||
|
||||
// Draw peak line (dashed) - inline to handle time_divisor
|
||||
let dashed_color = color.mix(0.5);
|
||||
chart
|
||||
.draw_series(
|
||||
peak_data
|
||||
.iter()
|
||||
.map(|d| {
|
||||
(
|
||||
d.timestamp_ms as f64 / 1000.0 / time_divisor,
|
||||
d.value / scale_factor,
|
||||
)
|
||||
})
|
||||
.zip(peak_data.iter().skip(1).map(|d| {
|
||||
(
|
||||
d.timestamp_ms as f64 / 1000.0 / time_divisor,
|
||||
d.value / scale_factor,
|
||||
)
|
||||
}))
|
||||
.enumerate()
|
||||
.filter(|(i, _)| i % 2 == 0)
|
||||
.map(|(_, (p1, p2))| {
|
||||
PathElement::new(vec![p1, p2], dashed_color.stroke_width(2))
|
||||
}),
|
||||
)?
|
||||
.label(format!("{} (peak)", run_id))
|
||||
.legend(move |(x, y)| {
|
||||
PathElement::new(
|
||||
vec![(x, y), (x + 10, y), (x + 20, y)],
|
||||
dashed_color.stroke_width(2),
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
Self::configure_series_labels(&mut chart)?;
|
||||
root.present()?;
|
||||
println!("Generated: {}", output_path.display());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn read_memory_data(
|
||||
&self,
|
||||
crate_path: &Path,
|
||||
runs: &[BenchmarkRun],
|
||||
) -> Result<Vec<(String, Vec<DataPoint>, Vec<DataPoint>)>> {
|
||||
let mut enhanced_runs = Vec::new();
|
||||
|
||||
for run in runs {
|
||||
// Re-read the CSV to get both footprint and peak values
|
||||
let csv_path = crate_path.join(&run.run_id).join("memory_footprint.csv");
|
||||
let csv_path = crate_path.join(&run.run_id).join("memory.csv");
|
||||
if let Ok(content) = fs::read_to_string(&csv_path) {
|
||||
let mut footprint_data = Vec::new();
|
||||
let mut peak_data = Vec::new();
|
||||
@@ -255,7 +500,7 @@ impl Visualizer {
|
||||
for (i, line) in content.lines().enumerate() {
|
||||
if i == 0 {
|
||||
continue;
|
||||
} // Skip header
|
||||
}
|
||||
|
||||
let parts: Vec<&str> = line.split(',').collect();
|
||||
if parts.len() >= 3
|
||||
@@ -280,100 +525,71 @@ impl Visualizer {
|
||||
}
|
||||
}
|
||||
|
||||
let max_time = enhanced_runs
|
||||
.iter()
|
||||
.flat_map(|(_, f, p)| f.iter().chain(p.iter()).map(|d| d.timestamp_ms))
|
||||
.max()
|
||||
.unwrap_or(1000);
|
||||
Ok(enhanced_runs)
|
||||
}
|
||||
|
||||
let max_value = enhanced_runs
|
||||
.iter()
|
||||
.flat_map(|(_, f, p)| f.iter().chain(p.iter()).map(|d| d.value))
|
||||
.fold(0.0_f64, f64::max);
|
||||
fn generate_progress_chart(
|
||||
&self,
|
||||
crate_path: &Path,
|
||||
crate_name: &str,
|
||||
runs: &[BenchmarkRun],
|
||||
) -> Result<()> {
|
||||
let output_path = crate_path.join("progress_chart.svg");
|
||||
let root = SVGBackend::new(&output_path, (1200, 700)).into_drawing_area();
|
||||
root.fill(&BG_COLOR)?;
|
||||
|
||||
// Convert to seconds and GB
|
||||
let max_time_s = (max_time as f64) / 1000.0;
|
||||
let max_value_gb = max_value / 1024.0;
|
||||
// Calculate time window based on shortest run + buffer
|
||||
let min_max_time_ms = Self::calculate_min_max_time(runs) + TIME_BUFFER_MS;
|
||||
let max_time_s = (min_max_time_ms as f64) / 1000.0;
|
||||
|
||||
// Trim all runs to the same time window
|
||||
let trimmed_runs = Self::trim_runs_to_time_window(runs, min_max_time_ms);
|
||||
|
||||
let max_block = Self::calculate_max_value(&trimmed_runs);
|
||||
|
||||
// Format time based on duration
|
||||
let (max_time_scaled, _time_unit, time_label) = Self::format_time(max_time_s);
|
||||
|
||||
let mut chart = ChartBuilder::on(&root)
|
||||
.caption(
|
||||
format!("{} — Memory Footprint", crate_name),
|
||||
("SF Mono", 24).into_font().color(&TEXT_COLOR),
|
||||
format!("{} — Progress", crate_name),
|
||||
(FONT, 24).into_font().color(&TEXT_COLOR),
|
||||
)
|
||||
.margin(20)
|
||||
.x_label_area_size(55)
|
||||
.y_label_area_size(75)
|
||||
.build_cartesian_2d(0.0..max_time_s * 1.05, 0.0..max_value_gb * 1.1)?;
|
||||
.x_label_area_size(50)
|
||||
.margin_left(50)
|
||||
.right_y_label_area_size(75)
|
||||
.build_cartesian_2d(0.0..max_time_scaled * 1.025, 0.0..max_block * 1.1)?;
|
||||
|
||||
chart
|
||||
.configure_mesh()
|
||||
.disable_mesh()
|
||||
.x_desc("Time (s)")
|
||||
.y_desc("Memory (GB)")
|
||||
.x_label_offset(10)
|
||||
.y_label_offset(10)
|
||||
.x_label_formatter(&|x| format!("{:.1}", x))
|
||||
.y_label_formatter(&|y| format!("{:.2}", y))
|
||||
.x_labels(8)
|
||||
.y_labels(6)
|
||||
.x_label_style(("SF Mono", 16).into_font().color(&TEXT_COLOR.mix(0.7)))
|
||||
.y_label_style(("SF Mono", 16).into_font().color(&TEXT_COLOR.mix(0.7)))
|
||||
.x_desc(time_label)
|
||||
.y_desc("Block Number")
|
||||
.x_label_formatter(&|x| Self::format_axis_number(*x))
|
||||
.y_label_formatter(&|y| Self::format_axis_number(*y))
|
||||
.x_labels(12)
|
||||
.y_labels(10)
|
||||
.x_label_style((FONT, 16).into_font().color(&TEXT_COLOR.mix(0.7)))
|
||||
.y_label_style((FONT, 16).into_font().color(&TEXT_COLOR.mix(0.7)))
|
||||
.axis_style(TEXT_COLOR.mix(0.3))
|
||||
.draw()?;
|
||||
|
||||
for (idx, (run_id, footprint_data, peak_data)) in enhanced_runs.iter().enumerate() {
|
||||
let time_divisor = max_time_s / max_time_scaled;
|
||||
|
||||
for (idx, run) in trimmed_runs.iter().enumerate() {
|
||||
let color = CHART_COLORS[idx % CHART_COLORS.len()];
|
||||
|
||||
// Draw footprint line (solid)
|
||||
chart
|
||||
.draw_series(LineSeries::new(
|
||||
footprint_data
|
||||
.iter()
|
||||
.map(|d| (d.timestamp_ms as f64 / 1000.0, d.value / 1024.0)),
|
||||
color.stroke_width(2),
|
||||
))?
|
||||
.label(format!("{} (current)", run_id))
|
||||
.legend(move |(x, y)| {
|
||||
PathElement::new(vec![(x, y), (x + 20, y)], color.stroke_width(2))
|
||||
});
|
||||
|
||||
// Draw peak line (dashed, slightly transparent)
|
||||
let dashed_color = color.mix(0.5);
|
||||
chart
|
||||
.draw_series(
|
||||
peak_data
|
||||
.iter()
|
||||
.map(|d| (d.timestamp_ms as f64 / 1000.0, d.value / 1024.0))
|
||||
.zip(
|
||||
peak_data
|
||||
.iter()
|
||||
.skip(1)
|
||||
.map(|d| (d.timestamp_ms as f64 / 1000.0, d.value / 1024.0)),
|
||||
)
|
||||
.enumerate()
|
||||
.filter(|(i, _)| i % 2 == 0) // Create dashed effect
|
||||
.map(|(_, (p1, p2))| {
|
||||
PathElement::new(vec![p1, p2], dashed_color.stroke_width(2))
|
||||
}),
|
||||
)?
|
||||
.label(format!("{} (peak)", run_id))
|
||||
.legend(move |(x, y)| {
|
||||
PathElement::new(
|
||||
vec![(x, y), (x + 10, y), (x + 20, y)],
|
||||
dashed_color.stroke_width(2),
|
||||
)
|
||||
});
|
||||
Self::draw_line_series(
|
||||
&mut chart,
|
||||
run.data
|
||||
.iter()
|
||||
.map(|d| (d.timestamp_ms as f64 / 1000.0 / time_divisor, d.value)),
|
||||
&run.run_id,
|
||||
color,
|
||||
)?;
|
||||
}
|
||||
|
||||
chart
|
||||
.configure_series_labels()
|
||||
.position(SeriesLabelPosition::UpperLeft)
|
||||
.label_font(("SF Mono", 16).into_font().color(&TEXT_COLOR.mix(0.9)))
|
||||
.background_style(BG_COLOR.mix(0.98))
|
||||
.border_style(BG_COLOR)
|
||||
.margin(10)
|
||||
.draw()?;
|
||||
|
||||
Self::configure_series_labels(&mut chart)?;
|
||||
root.present()?;
|
||||
println!("Generated: {}", output_path.display());
|
||||
Ok(())
|
||||
|
||||
Reference in New Issue
Block a user