121 lines
4.4 KiB
Rust
121 lines
4.4 KiB
Rust
|
|
//! 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);
|