Files
hcie-rust-v3.05/hcie-engine-api/benches/stroke_mask_pooling.rs
T
Your Name 8563a44211
mandatory-regression-gate / deterministic-tests (push) Has been cancelled
mandatory-regression-gate / protected-performance-path (push) Has been cancelled
Add SVG report for typical stroke mask pooling at warm 10th stroke
2026-07-24 23:43:42 +03:00

125 lines
4.5 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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);