Merge PR #8529: perf(bench): run bench_filters2 from cargo xtask perf, four heavy filters now tracked (P64-P67)

docs/scorecard.md: conflict in generated file, kept ours (b7-base); regenerated later.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
David Mann
2026-10-22 23:39:17 +02:00
co-authored by Claude Opus 5.5
2 changed files with 107 additions and 7 deletions
+57 -7
View File
@@ -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 <file>]`
//!
//! `--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<String> = 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<String> = 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<f64>)> = 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<String> = rows
.iter()
.map(|(name, samples)| {
let s: Vec<String> = 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}");
}
}
+50
View File
@@ -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