1dabd9c31e
- Updated stroke cache to use parallel processing for pixel checks. - Modified benchmark commands in notlar.txt to ensure consistent output and baseline comparisons. - Created a plan for merging criterion directories and optimizing benchmark performance, including pre-allocation of buffers and various optimization strategies. - Removed redundant output directory settings in benchmark files and ensured all benchmarks write to a single criterion directory. - Updated documentation to reflect changes in benchmark paths and configurations.
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);
|