171 lines
6.1 KiB
Rust
171 lines
6.1 KiB
Rust
//! Criterion benchmark: conditional `effects_dirty` skip guard.
|
||
//!
|
||
//! ## Purpose
|
||
//! Verifies that `render_composite_region()` / `apply_effects_and_sync_tiles()`
|
||
//! skips the expensive effects pipeline for layers that have **no effects or
|
||
//! styles**. If a regression removes the `effects.is_empty() && styles.is_empty()`
|
||
//! early-exit or unconditionally sets `effects_dirty = true`, composite cost
|
||
//! on effect-free documents will increase dramatically.
|
||
//!
|
||
//! ## Logic & Workflow
|
||
//! 1. Creates a 3840×2160, 10-layer document (all filled, no effects).
|
||
//! 2. **no_effects**: Benchmarks `render_composite_region()` where every layer
|
||
//! has empty `effects` and `styles` vectors → the effects pipeline is
|
||
//! completely skipped.
|
||
//! 3. **with_one_effect**: Same document but one layer has a DropShadow style
|
||
//! → the effects pipeline runs for that single layer.
|
||
//!
|
||
//! When the conditional skip works, `no_effects` should be measurably faster
|
||
//! than `with_one_effect` because no `apply_layer_effects` calls are made.
|
||
//!
|
||
//! ## Arguments & Returns
|
||
//! Standard criterion benchmark — run with:
|
||
//! ```bash
|
||
//! cargo bench -p hcie-engine-api --bench effects_skip
|
||
//! ```
|
||
//!
|
||
//! ## Side Effects / Dependencies
|
||
//! Allocates ~400 MB (10 layers × 4K RGBA + composite buffers).
|
||
|
||
use criterion::{criterion_group, criterion_main, Criterion};
|
||
use hcie_engine_api::{BrushStyle, BrushTip, Engine, LayerStyle};
|
||
|
||
/// Canvas dimensions — 4K UHD.
|
||
const W: u32 = 3840;
|
||
const H: u32 = 2160;
|
||
/// Number of raster layers.
|
||
const LAYERS: usize = 10;
|
||
|
||
/// Builds a 10-layer 4K document with NO effects on any layer.
|
||
///
|
||
/// **Purpose:** Creates the baseline scenario where the effects pipeline
|
||
/// should be completely skipped during compositing.
|
||
/// **Returns:** `(Engine, top_layer_id, all_layer_ids)`.
|
||
fn setup_no_effects_engine() -> (Engine, u64, Vec<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, layer_ids)
|
||
}
|
||
|
||
/// Performs a stroke + composite cycle and returns.
|
||
///
|
||
/// **Purpose:** Creates a dirty region and composites it, exercising the
|
||
/// effects pipeline path.
|
||
/// **Arguments:** `engine` — mutable engine, `layer_id` — target layer,
|
||
/// `offset` — position variation.
|
||
/// **Side Effects:** Mutates layer pixels, triggers composite.
|
||
fn stroke_and_composite(engine: &mut Engine, layer_id: u64, offset: f32) {
|
||
let cx = W as f32 / 2.0 + offset;
|
||
let cy = H as f32 / 2.0 + offset;
|
||
engine.begin_stroke(layer_id, cx, cy);
|
||
for i in 1..=5 {
|
||
let d = i as f32 * 8.0;
|
||
engine.stroke_to(layer_id, cx + d, cy + d, 0.8);
|
||
}
|
||
let (_region, ptr, _size) = engine.render_composite_region();
|
||
assert!(!ptr.is_null());
|
||
engine.end_stroke(layer_id);
|
||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||
engine.commit_pending_history();
|
||
}
|
||
|
||
/// Benchmark group: effects-free composite vs single-effect composite.
|
||
///
|
||
/// **Purpose:** Detects regressions where the effects pipeline runs
|
||
/// unnecessarily on layers with no effects, causing wasted CPU cycles
|
||
/// (apply_layer_effects + 33 MB buffer clone per affected layer).
|
||
///
|
||
/// **Logic & Workflow:**
|
||
/// - `no_effects_composite`: All 10 layers have empty effects/styles.
|
||
/// `apply_effects_and_sync_tiles()` should skip the effects pass entirely.
|
||
/// - `with_one_dropshadow`: Layer 5 has a DropShadow style enabled.
|
||
/// The effects pipeline runs for that single layer but skips the other 9.
|
||
/// This should be slightly slower than `no_effects_composite` but not 10×
|
||
/// slower (which would indicate the skip logic is broken).
|
||
fn bench_effects_skip(c: &mut Criterion) {
|
||
let mut group = c.benchmark_group("effects_skip");
|
||
|
||
// ── No effects on any layer ──
|
||
group.bench_function("no_effects_composite", |b| {
|
||
let (mut engine, layer_id, _ids) = setup_no_effects_engine();
|
||
// Warm up composite scratch
|
||
stroke_and_composite(&mut engine, layer_id, 0.0);
|
||
|
||
let mut offset = 0.0_f32;
|
||
b.iter(|| {
|
||
offset += 3.0;
|
||
stroke_and_composite(&mut engine, layer_id, offset);
|
||
});
|
||
});
|
||
|
||
// ── One layer has a DropShadow effect ──
|
||
group.bench_function("with_one_dropshadow", |b| {
|
||
let (mut engine, layer_id, layer_ids) = setup_no_effects_engine();
|
||
|
||
// Add a DropShadow style to layer 5 (middle of the stack)
|
||
let target_layer_id = layer_ids[5];
|
||
engine.update_layer_style(
|
||
target_layer_id,
|
||
LayerStyle::DropShadow {
|
||
enabled: true,
|
||
opacity: 0.6,
|
||
angle: 135.0,
|
||
distance: 5.0,
|
||
spread: 0.0,
|
||
size: 10.0,
|
||
color: [0, 0, 0, 255],
|
||
blend_mode: "Normal".to_string(),
|
||
},
|
||
);
|
||
|
||
// Warm up
|
||
stroke_and_composite(&mut engine, layer_id, 0.0);
|
||
|
||
let mut offset = 0.0_f32;
|
||
b.iter(|| {
|
||
offset += 3.0;
|
||
stroke_and_composite(&mut engine, layer_id, offset);
|
||
});
|
||
});
|
||
|
||
group.finish();
|
||
}
|
||
|
||
criterion_group!{
|
||
name = benches;
|
||
config = Criterion::default().output_directory(std::path::Path::new("criterion"));
|
||
targets = bench_effects_skip
|
||
}
|
||
criterion_main!(benches);
|