//! Criterion benchmark: `active_stroke_mask` buffer pooling guard. //! //! ## Purpose //! Verifies that the `active_stroke_mask` (~8 MB on a 4K canvas) is **pooled** //! across consecutive strokes rather than re-allocated on every //! `begin_stroke()` / `end_stroke()` cycle. If a regression drops the mask //! with `self.active_stroke_mask = None` inside `end_stroke()`, this benchmark //! will show a measurable increase in allocation overhead. //! //! ## Logic & Workflow //! 1. Creates a 3840×2160 document with one raster layer. //! 2. **cold** group: a single begin/end stroke pair (first allocation). //! 3. **warm** group: the 10th consecutive stroke on the same engine (buffer //! should already be allocated and simply zeroed with `fill(0)`). //! //! When pooling works correctly, the warm benchmark should be significantly //! faster than the cold one because no allocation occurs — only a memset. //! //! ## Arguments & Returns //! Standard criterion benchmark — run with: //! ```bash //! cargo bench -p hcie-engine-api --bench stroke_mask_pooling //! ``` //! //! ## Side Effects / Dependencies //! Allocates ~140 MB (4K RGBA layer + mask + before buffer). Uses //! `criterion::Criterion` for statistical measurement. use criterion::{criterion_group, criterion_main, Criterion}; use hcie_engine_api::{BrushStyle, BrushTip, Engine}; /// Canvas dimensions — 4K UHD as requested. const W: u32 = 3840; const H: u32 = 2160; /// Creates a pre-configured engine with a filled background layer and a /// standard round brush tip. /// /// **Purpose:** Shared setup for all mask pooling benchmark variants. /// **Returns:** `(Engine, layer_id)` ready for `begin_stroke()`. fn setup_engine() -> (Engine, u64) { let mut engine = Engine::new(W, H); let layer_id = engine.active_layer_id(); engine.set_active_layer(layer_id); engine.draw_filled_rect_rgba(0, 0, W, H, [200, 200, 200, 255]); let mut tip = BrushTip::default(); tip.style = BrushStyle::Round; tip.size = 24.0; tip.opacity = 1.0; tip.hardness = 0.85; tip.spacing = 0.1; engine.set_brush_tip(tip); engine.set_color([60, 60, 220, 255]); engine.set_eraser(false); (engine, layer_id) } /// Performs a single begin_stroke → short paint → end_stroke cycle. /// /// **Purpose:** Exercises the full mask allocation / reuse path. /// **Arguments:** `engine` — mutable engine reference, `layer_id` — target layer. /// **Side Effects:** Mutates layer pixels and engine caches. fn do_one_stroke(engine: &mut Engine, layer_id: u64) { let cx = W as f32 / 2.0; let cy = H as f32 / 2.0; engine.begin_stroke(layer_id, cx, cy); // Paint 5 short segments to exercise the mask for i in 1..=5 { let offset = i as f32 * 10.0; engine.stroke_to(layer_id, cx + offset, cy + offset, 0.8); } engine.end_stroke(layer_id); // Commit pending history so background thread completes std::thread::sleep(std::time::Duration::from_millis(10)); engine.commit_pending_history(); } /// Benchmark group: cold vs warm `active_stroke_mask` allocation. /// /// **Purpose:** Detects pooling regressions by comparing the first stroke /// (cold allocation) against a subsequent stroke (warm / reused buffer). /// /// **Logic & Workflow:** /// - `cold_first_stroke`: Measures `begin_stroke` + short paint + `end_stroke` /// on a fresh engine where `active_stroke_mask` is `None`. /// - `warm_10th_stroke`: Same operation but after 9 warm-up strokes have already /// populated the pooled mask buffer. If pooling works, this should be faster. fn bench_mask_pooling(c: &mut Criterion) { let mut group = c.benchmark_group("stroke_mask_pooling"); // ── Cold: first stroke on a fresh engine (allocation happens) ── group.bench_function("cold_first_stroke", |b| { b.iter_with_setup( || setup_engine(), |(mut engine, layer_id)| { do_one_stroke(&mut engine, layer_id); }, ); }); // ── Warm: 10th stroke on a warmed-up engine (buffer reuse) ── group.bench_function("warm_10th_stroke", |b| { // Pre-warm the engine outside the measured loop let (mut engine, layer_id) = setup_engine(); for _ in 0..9 { do_one_stroke(&mut engine, layer_id); } b.iter(|| { do_one_stroke(&mut engine, layer_id); }); }); group.finish(); } criterion_group!(benches, bench_mask_pooling); criterion_main!(benches);