Files

147 lines
5.5 KiB
Rust
Raw Permalink Normal View History

//! Criterion benchmark: `composite_scratch` buffer pooling guard.
//!
//! ## Purpose
//! Verifies that the ~33 MB `composite_scratch` buffer inside
//! `render_composite_region()` is allocated once and reused across calls.
//! If a regression replaces the pooled `Option<Vec<u8>>` with a fresh
//! `Vec::new()` on every call, this benchmark detects the allocation storm.
//!
//! ## Logic & Workflow
//! 1. Creates a 3840×2160, 10-layer document with all layers filled.
//! 2. Performs a stroke + composite to warm the scratch buffer.
//! 3. **cold_composite**: Measures a `render_composite_region()` call on a
//! fresh engine where `composite_scratch` is `None`.
//! 4. **warm_composite**: Measures the same call after the buffer has been
//! allocated (should be faster — no allocation, just memset + composite).
//!
//! ## Arguments & Returns
//! Standard criterion benchmark — run with:
//! ```bash
//! cargo bench -p hcie-engine-api --bench composite_scratch_pooling
//! ```
//!
//! ## Side Effects / Dependencies
//! Allocates ~400 MB (10 layers × 4K RGBA + scratch + tile caches).
use criterion::{criterion_group, criterion_main, Criterion};
use hcie_engine_api::{BrushStyle, BrushTip, Engine};
/// Canvas dimensions — 4K UHD.
const W: u32 = 3840;
const H: u32 = 2160;
/// Number of raster layers to create (worst-case workload).
const LAYERS: usize = 10;
/// Builds a multi-layer 4K document ready for compositing.
///
/// **Purpose:** Creates the worst-case composite scenario with every layer
/// filled so tile and pixel compositing operate on real data.
/// **Returns:** `(Engine, top_layer_id)`.
fn setup_multilayer_engine() -> (Engine, u64) {
let mut engine = Engine::new(W, H);
let bg_id = engine.active_layer_id();
engine.set_active_layer(bg_id);
engine.draw_filled_rect_rgba(0, 0, W, H, [240, 240, 240, 255]);
let mut layer_ids = vec![bg_id];
for i in 1..LAYERS {
let id = engine.add_layer(&format!("Layer {}", i));
engine.set_active_layer(id);
let color = match i % 4 {
0 => [180, 80, 80, 255],
1 => [80, 180, 80, 255],
2 => [80, 80, 180, 255],
_ => [160, 140, 100, 255],
};
engine.draw_filled_rect_rgba(0, 0, W, H, color);
layer_ids.push(id);
}
let top_id = *layer_ids.last().unwrap();
engine.set_active_layer(top_id);
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([220, 60, 60, 255]);
engine.set_eraser(false);
(engine, top_id)
}
/// Forces a dirty region and composites it.
///
/// **Purpose:** Exercises the `render_composite_region()` path including
/// scratch buffer allocation/reuse, tile compositing, and dirty rect handling.
/// **Arguments:** `engine` — mutable engine, `layer_id` — target layer.
/// **Side Effects:** Paints a short stroke segment to create a dirty region,
/// then calls `render_composite_region()`.
fn stroke_and_composite(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);
engine.stroke_to(layer_id, cx + 50.0, cy + 50.0, 0.8);
let (region, ptr, _size) = engine.render_composite_region();
assert!(!ptr.is_null());
let _ = region;
engine.end_stroke(layer_id);
std::thread::sleep(std::time::Duration::from_millis(10));
engine.commit_pending_history();
}
/// Benchmark group: cold vs warm `composite_scratch` allocation.
///
/// **Purpose:** Detects regressions where `composite_scratch` is dropped or
/// reallocated on every `render_composite_region()` call. When pooling works,
/// the warm benchmark avoids a ~33 MB allocation and should be measurably faster.
///
/// **Logic & Workflow:**
/// - `cold_composite`: Fresh engine, `composite_scratch = None`. First call
/// allocates the buffer.
/// - `warm_composite`: Engine with pre-allocated scratch buffer. Measures
/// pure composite cost without allocation overhead.
fn bench_composite_scratch_pooling(c: &mut Criterion) {
let mut group = c.benchmark_group("composite_scratch_pooling");
// ── Cold: composite on a fresh engine (scratch allocation included) ──
group.bench_function("cold_composite", |b| {
b.iter_with_setup(
|| setup_multilayer_engine(),
|(mut engine, layer_id)| {
stroke_and_composite(&mut engine, layer_id);
},
);
});
// ── Warm: composite on a pre-warmed engine (scratch already pooled) ──
group.bench_function("warm_composite", |b| {
let (mut engine, layer_id) = setup_multilayer_engine();
// Warm up: allocate the scratch buffer
stroke_and_composite(&mut engine, layer_id);
b.iter(|| {
// Paint a new segment and composite — scratch should be reused
let cx = W as f32 / 2.0;
let cy = H as f32 / 2.0;
engine.begin_stroke(layer_id, cx, cy);
engine.stroke_to(layer_id, cx + 30.0, cy - 30.0, 0.7);
let (region, ptr, _size) = engine.render_composite_region();
assert!(!ptr.is_null());
let _ = region;
engine.end_stroke(layer_id);
std::thread::sleep(std::time::Duration::from_millis(5));
engine.commit_pending_history();
});
});
group.finish();
}
criterion_group!(benches, bench_composite_scratch_pooling);
criterion_main!(benches);