Add a BENCHMARKS.md with rendered benchmarks (#1211)

As a precursor to the release, I want to include a structured document
with detailed benchmarks.

Closes https://github.com/astral-sh/puffin/issues/1210.
This commit is contained in:
Charlie Marsh 2024-01-31 12:11:52 -08:00 committed by GitHub
parent b9d89e7624
commit c4bfb6efee
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 631 additions and 1 deletions

View file

@ -36,6 +36,8 @@ puffin-resolver = { path = "../puffin-resolver" }
pypi-types = { path = "../pypi-types" }
puffin-traits = { path = "../puffin-traits" }
# Any dependencies that are exclusively used in `puffin-dev` should be listed as non-workspace
# dependencies, to ensure that we're forced to think twice before including them in other crates.
anstream = { workspace = true }
anyhow = { workspace = true }
chrono = { workspace = true }
@ -46,7 +48,12 @@ indicatif = { workspace = true }
itertools = { workspace = true }
owo-colors = { workspace = true }
petgraph = { workspace = true }
poloto = { version = "19.1.2" }
resvg = { version = "0.29.0" }
rustc-hash = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
tagu = { version = "0.1.6" }
tempfile = { workspace = true }
tokio = { workspace = true }
tracing = { workspace = true }

View file

@ -22,6 +22,7 @@ use resolve_many::ResolveManyArgs;
use crate::build::{build, BuildArgs};
use crate::install_many::InstallManyArgs;
use crate::render_benchmarks::RenderBenchmarksArgs;
use crate::resolve_cli::ResolveCliArgs;
use crate::wheel_metadata::WheelMetadataArgs;
@ -43,6 +44,7 @@ static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc;
mod build;
mod install_many;
mod render_benchmarks;
mod resolve_cli;
mod resolve_many;
mod wheel_metadata;
@ -66,6 +68,7 @@ enum Cli {
/// Resolve requirements passed on the CLI
Resolve(ResolveCliArgs),
WheelMetadata(WheelMetadataArgs),
RenderBenchmarks(RenderBenchmarksArgs),
}
#[instrument] // Anchor span to check for overhead
@ -86,6 +89,7 @@ async fn run() -> Result<()> {
resolve_cli::resolve_cli(args).await?;
}
Cli::WheelMetadata(args) => wheel_metadata::wheel_metadata(args).await?,
Cli::RenderBenchmarks(args) => render_benchmarks::render_benchmarks(&args)?,
}
Ok(())
}

View file

@ -0,0 +1,113 @@
use std::path::{Path, PathBuf};
use anyhow::{anyhow, Result};
use clap::Parser;
use poloto::build;
use resvg::usvg_text_layout::{fontdb, TreeTextToPath};
use serde::Deserialize;
use tagu::prelude::*;
#[derive(Parser)]
pub(crate) struct RenderBenchmarksArgs {
/// Path to a JSON output from a `hyperfine` benchmark.
path: PathBuf,
/// Title of the plot.
#[clap(long, short)]
title: Option<String>,
}
pub(crate) fn render_benchmarks(args: &RenderBenchmarksArgs) -> Result<()> {
let mut results: BenchmarkResults = serde_json::from_slice(&std::fs::read(&args.path)?)?;
// Replace the command with a shorter name. (The command typically includes the benchmark name,
// but we assume we're running over a single benchmark here.)
for result in &mut results.results {
if result.command.starts_with("puffin") {
result.command = "puffin".into();
} else if result.command.starts_with("pip-compile") {
result.command = "pip-compile".into();
} else if result.command.starts_with("pip-sync") {
result.command = "pip-sync".into();
} else if result.command.starts_with("poetry") {
result.command = "poetry".into();
} else {
return Err(anyhow!("unknown command: {}", result.command));
}
}
let fontdb = load_fonts();
render_to_png(
&plot_benchmark(args.title.as_deref().unwrap_or("Benchmark"), &results)?,
&args.path.with_extension("png"),
&fontdb,
)?;
Ok(())
}
/// Render a benchmark to an SVG (as a string).
fn plot_benchmark(heading: &str, results: &BenchmarkResults) -> Result<String> {
let mut data = Vec::new();
for result in &results.results {
data.push((result.mean, &result.command));
}
let theme = poloto::render::Theme::light();
let theme = theme.append(tagu::build::raw(
".poloto0.poloto_fill{fill: #6340AC !important;}",
));
let theme = theme.append(tagu::build::raw(
".poloto_background{fill: white !important;}",
));
Ok(build::bar::gen_simple("", data, [0.0])
.label((heading, "Time (s)", ""))
.append_to(poloto::header().append(theme))
.render_string()?)
}
/// Render an SVG to a PNG file.
fn render_to_png(data: &str, path: &Path, fontdb: &fontdb::Database) -> Result<()> {
let mut tree = resvg::usvg::Tree::from_str(data, &resvg::usvg::Options::default())?;
tree.convert_text(fontdb);
let fit_to = resvg::usvg::FitTo::Width(1600);
let size = fit_to
.fit_to(tree.size.to_screen_size())
.ok_or_else(|| anyhow!("failed to fit to screen size"))?;
let mut pixmap = resvg::tiny_skia::Pixmap::new(size.width(), size.height()).unwrap();
resvg::render(
&tree,
fit_to,
resvg::tiny_skia::Transform::default(),
pixmap.as_mut(),
)
.ok_or_else(|| anyhow!("failed to render"))?;
std::fs::create_dir_all(path.parent().unwrap())?;
pixmap.save_png(path)?;
Ok(())
}
/// Load the system fonts and set the default font families.
fn load_fonts() -> fontdb::Database {
let mut fontdb = fontdb::Database::new();
fontdb.load_system_fonts();
fontdb.set_serif_family("Times New Roman");
fontdb.set_sans_serif_family("Arial");
fontdb.set_cursive_family("Comic Sans MS");
fontdb.set_fantasy_family("Impact");
fontdb.set_monospace_family("Courier New");
fontdb
}
#[derive(Debug, Deserialize)]
struct BenchmarkResults {
results: Vec<BenchmarkResult>,
}
#[derive(Debug, Deserialize)]
struct BenchmarkResult {
command: String,
mean: f64,
}