mirror of
https://github.com/storytold/deckcraft.git
synced 2026-11-03 00:41:28 -06:00
Add a performance baseline: deckcraft-cli bench and cargo xtask bench
Times open, per-slide render, save, the PPTX round trip and PDF export, and reports peak memory; xtask builds the CLI in release, runs it on the sample and any given decks, and compares with docs/benchmarks/baseline.json.
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
//! `cargo xtask bench`: the performance baseline. Builds `deckcraft-cli` in release, runs its
|
||||
//! `bench` on the built-in sample (and any decks given), and compares each step with the saved
|
||||
//! baseline (`docs/benchmarks/baseline.json`) when there is one; `--save` writes a new baseline.
|
||||
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
|
||||
use serde_json::{Value, json};
|
||||
|
||||
/// A step this many times slower than the baseline is flagged.
|
||||
const SLOWER: f64 = 1.5;
|
||||
|
||||
pub fn run(root: &Path, args: &[&str]) -> Result<(), String> {
|
||||
let save = args.contains(&"--save");
|
||||
let decks: Vec<&str> = args.iter().copied().filter(|a| !a.starts_with("--")).collect();
|
||||
let mut build = super::cargo();
|
||||
build.args(["build", "--release", "-q", "-p", "deckcraft-cli"]);
|
||||
super::run(build, "cargo build --release -p deckcraft-cli")?;
|
||||
let target = std::env::var_os("CARGO_TARGET_DIR").map_or_else(|| root.join("target"), Into::into);
|
||||
let cli = target.join("release").join(if cfg!(windows) { "deckcraft-cli.exe" } else { "deckcraft-cli" });
|
||||
let mut results = vec![];
|
||||
let inputs: Vec<Option<&str>> = std::iter::once(None).chain(decks.iter().map(|d| Some(*d))).collect();
|
||||
for input in inputs {
|
||||
let mut c = Command::new(&cli);
|
||||
c.arg("bench").arg("--json");
|
||||
match input {
|
||||
Some(d) => c.arg(d),
|
||||
None => c.arg("--sample"),
|
||||
};
|
||||
let out = c.output().map_err(|e| format!("{}: {e}", cli.display()))?;
|
||||
if !out.status.success() {
|
||||
return Err(format!("bench {}: {}", input.unwrap_or("--sample"), String::from_utf8_lossy(&out.stderr).trim()));
|
||||
}
|
||||
let v: Value = serde_json::from_slice(&out.stdout).map_err(|e| format!("bench output: {e}"))?;
|
||||
results.push(v);
|
||||
}
|
||||
let baseline_path = root.join("docs/benchmarks/baseline.json");
|
||||
let baseline: Option<Value> = std::fs::read(&baseline_path).ok().and_then(|b| serde_json::from_slice(&b).ok());
|
||||
let mut slower = 0;
|
||||
for r in &results {
|
||||
let file = r["file"].as_str().unwrap_or("?");
|
||||
println!("\n{file}: {} slides", r["slides"]);
|
||||
let base = baseline.as_ref().and_then(|b| b["results"].as_array()?.iter().find(|x| x["file"] == r["file"]).cloned());
|
||||
for step in r["steps"].as_array().into_iter().flatten() {
|
||||
let name = step["step"].as_str().unwrap_or("?");
|
||||
let ms = step["medianMs"].as_f64().unwrap_or(0.0);
|
||||
let was = base.as_ref().and_then(|b| b["steps"].as_array()?.iter().find(|s| s["step"] == step["step"])?["medianMs"].as_f64());
|
||||
let cmp = match was {
|
||||
Some(w) if w > 0.5 => {
|
||||
let k = ms / w;
|
||||
if k > SLOWER {
|
||||
slower += 1;
|
||||
}
|
||||
format!(" {k:>5.2}× baseline{}", if k > SLOWER { " SLOWER" } else { "" })
|
||||
}
|
||||
_ => String::new(),
|
||||
};
|
||||
println!(" {name:<24} {ms:>9.1} ms{cmp}");
|
||||
}
|
||||
if let Some(p) = r["peakRssMiB"].as_f64() {
|
||||
println!(" peak memory {p:>9.1} MiB");
|
||||
}
|
||||
}
|
||||
if save {
|
||||
if let Some(dir) = baseline_path.parent() {
|
||||
std::fs::create_dir_all(dir).map_err(|e| e.to_string())?;
|
||||
}
|
||||
let v = json!({"note": "cargo xtask bench --save; times depend on the machine, compare on the same one", "results": results});
|
||||
std::fs::write(&baseline_path, serde_json::to_vec_pretty(&v).map_err(|e| e.to_string())?).map_err(|e| e.to_string())?;
|
||||
println!("\nwrote {}", baseline_path.display());
|
||||
} else if baseline.is_none() {
|
||||
println!("\nno baseline yet: `cargo xtask bench --save` writes docs/benchmarks/baseline.json");
|
||||
}
|
||||
if slower > 0 { Err(format!("{slower} step(s) more than {SLOWER}× slower than the baseline")) } else { Ok(()) }
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
//! invoked through `std::process::Command`.
|
||||
|
||||
mod assets;
|
||||
mod bench;
|
||||
mod ico;
|
||||
mod layers;
|
||||
mod parity;
|
||||
@@ -22,6 +23,9 @@ commands:
|
||||
wasm cargo check --target wasm32-unknown-unknown for the wasm-safe crates
|
||||
ci fmt --check, clippy -D warnings, test, assets, layers, wasm (stops at first failure)
|
||||
stats [--exact] count tests and lines per crate (--exact: ask the test harness via `-- --list`)
|
||||
bench [--save] [DECK...]
|
||||
time open, render, save and export of the sample (and DECKs) in release, and
|
||||
compare with docs/benchmarks/baseline.json (--save writes it)
|
||||
parity recompute the PowerPoint feature-parity summary in docs/parity-checklist.md
|
||||
ico <out.ico> <in.png>...
|
||||
pack square PNGs (<= 256 px) into a Windows .ico (see packaging/icons.sh)
|
||||
@@ -37,6 +41,7 @@ fn main() -> ExitCode {
|
||||
Some("ci") => cmd_ci(),
|
||||
Some("stats") => stats::run(&root(), rest.contains(&"--exact")),
|
||||
Some("parity") => parity::run(&root()),
|
||||
Some("bench") => bench::run(&root(), &rest),
|
||||
Some("ico") => ico::run(&rest),
|
||||
Some("version") => version::run(&root(), &rest),
|
||||
Some("-h" | "--help" | "help") | None => {
|
||||
|
||||
Reference in New Issue
Block a user