Files
figcraft/crates/engine/src/export_bench_tests.rs
T
NetterSilverandClaude Opus 5.5 adb1b43033 Speed up raster export: faster blur and spread passes
Shadows and layer blurs dominated PNG/JPG/WebP export time (gdb stack
sampling of the example files: about 45% in fx::box_blur_h, 13% in
fx::morph). Both now produce the same bytes, faster:

- fx::blur runs its three box passes per row while the row is in cache,
  works on whole pixels instead of bounds-checked bytes, skips fully
  transparent rows, transposes in cache-friendly column blocks, and
  splits rows and transposes across rayon on desktop for pixmaps of
  64K pixels and more, only when not already on a rayon worker (a band
  of render_view stays on its thread: no nested parallelism).
- fx::morph (shadow spread on layers that aren't one outline) uses the
  van Herk / Gil-Werman running max/min: constant work per pixel instead
  of 2r + 1.

Integer arithmetic and pass order are unchanged; new tests compare both
against the previous implementations byte for byte, including sizes on
the parallel path.

An ignored timing test exports every top-level frame of examples/ as
PNG, JPG and WebP at 2x through export_file:
cargo test --release -p figcraft-engine --lib export_timings -- --ignored --nocapture

Ryzen, 16 threads, Linux; median of 7 (min):
  PNG   2195 ms (2182) ->  992 ms (989)   -55%
  JPG   2880 ms (2560) -> 1550 ms (1535)  -46%
  WebP  1964 ms (1901) -> 1022 ms (1018)  -48%
  total 7035 ms (6677) -> 3567 ms (3555)  -49%
All 80 exported files (PNG, JPG, WebP, PDF, SVG) are byte-identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 17:43:23 +01:00

97 lines
3.9 KiB
Rust

//! Export timings over the example files: every top-level frame of each `examples/*.figcraft`
//! exported as PNG, JPG and WebP at 2x through [`export_file`], as Export and `figcraft-cli export`
//! do. Run in release:
//! `cargo test --release -p figcraft-engine --lib export_timings -- --ignored --nocapture`.
//!
//! `FIGCRAFT_EXPORT_BENCH_RUNS` sets the timed runs (default 7; one warm-up run first),
//! `FIGCRAFT_EXPORT_BENCH_FORMATS` the formats (default `png,jpg,webp`) and
//! `FIGCRAFT_EXPORT_BENCH_OUT` a folder to write the files of the last run to (to compare outputs).
use std::time::Instant;
use figcraft_doc::{Document, ExportFormat, ExportSetting, NodeId};
use crate::cmd::file::{export_file, load_path};
/// The example documents and their top-level frames.
fn examples() -> Vec<(String, Document, Vec<NodeId>)> {
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../examples");
let mut paths: Vec<_> =
std::fs::read_dir(&dir).unwrap().map(|e| e.unwrap().path()).filter(|p| p.extension().is_some_and(|e| e == "figcraft")).collect();
paths.sort();
paths
.into_iter()
.map(|p| {
let (loaded, _) = load_path(&p.to_string_lossy(), None).unwrap();
let doc = loaded.doc;
let frames: Vec<NodeId> = doc.pages.iter().flat_map(|pg| doc.top_frames(*pg)).collect();
(p.file_stem().unwrap().to_string_lossy().into_owned(), doc, frames)
})
.collect()
}
fn format(name: &str) -> ExportFormat {
match name {
"png" => ExportFormat::Png,
"jpg" => ExportFormat::Jpg,
"webp" => ExportFormat::Webp,
"svg" => ExportFormat::Svg,
"pdf" => ExportFormat::Pdf,
other => panic!("unknown format {other}"),
}
}
/// Export every frame in `format` at 2x: (milliseconds, files).
fn run(docs: &[(String, Document, Vec<NodeId>)], format: ExportFormat) -> (f64, Vec<(String, Vec<u8>)>) {
let setting = ExportSetting { format, ..Default::default() };
let mut files = Vec::new();
let t = Instant::now();
for (name, doc, frames) in docs {
for f in frames {
let (bytes, _) = export_file(doc, &[*f], &setting, 2.0).unwrap();
files.push((format!("{name}-{}.{}", f.0, format.ext()), bytes));
}
}
(t.elapsed().as_secs_f64() * 1000.0, files)
}
#[test]
#[ignore = "timing: run in release with --ignored --nocapture"]
fn export_timings() {
let runs: usize = std::env::var("FIGCRAFT_EXPORT_BENCH_RUNS").ok().and_then(|v| v.parse().ok()).unwrap_or(7).max(1);
let formats = std::env::var("FIGCRAFT_EXPORT_BENCH_FORMATS").unwrap_or_else(|_| "png,jpg,webp".into());
let out = std::env::var("FIGCRAFT_EXPORT_BENCH_OUT").ok();
let docs = examples();
let frames: usize = docs.iter().map(|d| d.2.len()).sum();
println!("{} documents, {frames} frames, {runs} runs", docs.len());
let mut total = vec![0.0; runs];
for name in formats.split(',').map(str::trim).filter(|f| !f.is_empty()) {
let fmt = format(name);
let _ = run(&docs, fmt);
let mut times = Vec::with_capacity(runs);
let mut last = Vec::new();
for t in &mut total {
let (ms, files) = run(&docs, fmt);
*t += ms;
times.push(ms);
last = files;
}
if let Some(dir) = &out {
std::fs::create_dir_all(dir).unwrap();
for (file, bytes) in &last {
std::fs::write(std::path::Path::new(dir).join(file), bytes).unwrap();
}
}
report(name, &mut times);
}
report("total", &mut total);
}
/// Print the median and minimum of `times` (and every run).
fn report(name: &str, times: &mut [f64]) {
let runs: Vec<String> = times.iter().map(|t| format!("{t:.1}")).collect();
times.sort_by(f64::total_cmp);
let median = times[times.len() / 2];
println!("{name:>6}: median {median:8.1} ms min {:8.1} ms runs [{}]", times[0], runs.join(", "));
}