Files

125 lines
4.5 KiB
Rust
Raw Permalink Normal View History

//! 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!{
name = benches;
config = Criterion::default().output_directory(std::path::Path::new("criterion"));
targets = bench_mask_pooling
}
criterion_main!(benches);