Files
hcie-rust-v3.05/hcie-engine-api/benches/composite_scratch_pooling.rs
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

151 lines
5.6 KiB
Rust
Raw Permalink 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: `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!{
name = benches;
config = Criterion::default().output_directory(std::path::Path::new("criterion"));
targets = bench_composite_scratch_pooling
}
criterion_main!(benches);