diff --git a/crates/algo/examples/bench_filters2.rs b/crates/algo/examples/bench_filters2.rs index 9849c4f843..f6b34aca28 100644 --- a/crates/algo/examples/bench_filters2.rs +++ b/crates/algo/examples/bench_filters2.rs @@ -1,13 +1,33 @@ //! Timing for the heavier second-batch filters on a ~24 MP (6000×4000) RGBA8 layer. -//! `cargo run --release -p photocraft-algo --example bench_filters2 [filter-name-substring]` +//! +//! `cargo run --release -p photocraft-algo --example bench_filters2 [name-substring] [--reps N] [--quick] [--json ]` +//! +//! `--json` writes the report `cargo xtask perf` reads: `{"rows": [{"name", "samples_ms"}]}`. +//! `--quick` runs the list on a 1500×1000 layer instead, for `cargo xtask perf --quick`. use photocraft_algo::*; use photocraft_color::PixelFormat; use photocraft_geom::Rect; use photocraft_raster::Surface; fn main() { - let only = std::env::args().nth(1); - let (w, h) = (6000, 4000); + let a: Vec = std::env::args().skip(1).collect(); + let flag = |k: &str| a.iter().position(|v| v == k); + let value = |k: &str| flag(k).and_then(|i| a.get(i + 1)).cloned(); + let mut only: Option = None; + let mut i = 0; + while i < a.len() { + match a[i].as_str() { + "--json" | "--reps" => i += 2, + "--quick" => i += 1, + _ => { + only.get_or_insert_with(|| a[i].clone()); + i += 1; + } + } + } + let reps: usize = value("--reps").and_then(|v| v.parse().ok()).unwrap_or(1).max(1); + let quick = flag("--quick").is_some(); + let (w, h) = if quick { (1500, 1000) } else { (6000, 4000) }; let r = Rect::new(0, 0, w, h); let mut s = Surface::new(PixelFormat::RGBA8); // Busy deterministic content (gradients plus texture) so edge-aware filters do real work. @@ -55,19 +75,49 @@ fn main() { }, FilterParams::Relight { angle: 45.0, elevation: 40.0, intensity: 40.0, ambient: 55.0, warmth: 0.0, softness: 25.0 }, ]; + let mut rows: Vec<(String, Vec)> = Vec::new(); for p in cases { if only.as_deref().is_some_and(|o| !p.label().to_lowercase().contains(&o.to_lowercase())) { continue; } let area = output_area(&p, s.content_bounds(), r, None).intersect(&r); - let t = std::time::Instant::now(); - let out = apply(&s, &p, area, r, None); // Smart Blur's cost peaks near radius 12, where High quality visits every pixel in the circle. let name = match p { FilterParams::SmartBlur { radius, .. } => format!("{} r{radius}", p.label()), _ => p.label().to_string(), }; - println!("{name:<18} {w}x{h} RGBA8: {:.2?}", t.elapsed()); - drop(out); + let mut samples = Vec::with_capacity(reps); + for _ in 0..reps { + let t = std::time::Instant::now(); + let out = apply(&s, &p, area, r, None); + let ms = t.elapsed().as_secs_f64() * 1e3; + samples.push(ms); + drop(out); + } + let mut sorted = samples.clone(); + sorted.sort_by(f64::total_cmp); + let median = match sorted.len() % 2 { + 1 => sorted[sorted.len() / 2], + _ => (sorted[sorted.len() / 2 - 1] + sorted[sorted.len() / 2]) / 2.0, + }; + println!("{name:<18} {w}x{h} RGBA8: {median:.1} ms{reps_note}", reps_note = if reps > 1 { format!(" (median of {reps})") } else { String::new() }); + rows.push((name, samples)); + } + if let Some(path) = value("--json") { + // Two fields per row and ASCII filter labels: hand-written rather than a serde dependency + // this crate does not otherwise need. + let body: Vec = rows + .iter() + .map(|(name, samples)| { + let s: Vec = samples.iter().map(|v| format!("{v:.4}")).collect(); + format!("{{\"name\":\"{name}\",\"samples_ms\":[{}]}}", s.join(",")) + }) + .collect(); + let text = format!("{{\"rows\":[{}]}}", body.join(",")); + if let Err(e) = std::fs::write(&path, text) { + eprintln!("write {path}: {e}"); + std::process::exit(1); + } + println!("report: {path}"); } } diff --git a/perf/budgets.toml b/perf/budgets.toml index 0b068ede0f..83983e4206 100644 --- a/perf/budgets.toml +++ b/perf/budgets.toml @@ -560,3 +560,53 @@ title = "File > New 10 x 5 m at 200 ppi (3.1 GP) + first reduced canvas refresh" issue = 4017 bench = "perf_scenarios" row = "new 3 GP document + first reduced canvas refresh" + +# ---- #211: the heavy algo filters nothing measured yet ---------------------------------------- +# +# `bench_filters2` times the second-batch filters on a 24 MP RGBA8 layer. The example existed but was +# never wired into this file, so nothing caught a regression in any of them. The target is the one +# second a filter applied once to a 24 MP layer should cost; all four rows are over it today +# (2026-10-22, 4-core laptop, default rayon pool), which is what #211's slow-algorithm ledger is for. + +[[bench]] +name = "bench_filters2" +package = "photocraft-algo" +args = ["--reps", "3"] +quick_args = ["--quick", "--reps", "1"] +note = "24 MP RGBA8: Oil Paint, Relight, Reduce Noise, Smart Blur, the blur gallery, Crystallize, Color Halftone" + +[[scenario]] +id = "P64" +title = "Oil Paint on 24 MP" +issue = 211 +bench = "bench_filters2" +row = "Oil Paint" +budget_ms = 1000 +enforce = false # 8.8 s measured 2026-10-22 + +[[scenario]] +id = "P65" +title = "Smart Blur r 12 on 24 MP (High quality)" +issue = 211 +bench = "bench_filters2" +row = "Smart Blur r12" +budget_ms = 1000 +enforce = false # 11.6 s measured 2026-10-22: the peak of the radius sweep, where High quality visits every pixel in the circle + +[[scenario]] +id = "P66" +title = "Relight on 24 MP" +issue = 211 +bench = "bench_filters2" +row = "Relight" +budget_ms = 1000 +enforce = false # 3.7 s measured 2026-10-22 + +[[scenario]] +id = "P67" +title = "Reduce Noise on 24 MP" +issue = 211 +bench = "bench_filters2" +row = "Reduce Noise" +budget_ms = 1000 +enforce = false # 2.8 s measured 2026-10-22