Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6b02f19624 |
@@ -0,0 +1 @@
|
||||
,dev-user,hc-20w1s30u2j,24.07.2026 16:44,file:///home/dev-user/.config/libreoffice/4;
|
||||
Generated
+166
-1
@@ -133,6 +133,12 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anes"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "1.0.0"
|
||||
@@ -759,6 +765,12 @@ dependencies = [
|
||||
"wayland-client",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cast"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.65"
|
||||
@@ -798,6 +810,58 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ciborium"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e"
|
||||
dependencies = [
|
||||
"ciborium-io",
|
||||
"ciborium-ll",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ciborium-io"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757"
|
||||
|
||||
[[package]]
|
||||
name = "ciborium-ll"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9"
|
||||
dependencies = [
|
||||
"ciborium-io",
|
||||
"half",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d91e0c145792ef73a6ad36d27c75ac09f1832222a3c209689d90f534685ee5b7"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f09628afdcc538b57f3c6341e9c8e9970f18e4a481690a64974d7023bd33548b"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
|
||||
|
||||
[[package]]
|
||||
name = "clipboard-win"
|
||||
version = "5.4.1"
|
||||
@@ -1027,6 +1091,42 @@ dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "criterion"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f"
|
||||
dependencies = [
|
||||
"anes",
|
||||
"cast",
|
||||
"ciborium",
|
||||
"clap",
|
||||
"criterion-plot",
|
||||
"is-terminal",
|
||||
"itertools 0.10.5",
|
||||
"num-traits",
|
||||
"once_cell",
|
||||
"oorandom",
|
||||
"plotters",
|
||||
"rayon",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"tinytemplate",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "criterion-plot"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1"
|
||||
dependencies = [
|
||||
"cast",
|
||||
"itertools 0.10.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-deque"
|
||||
version = "0.8.6"
|
||||
@@ -2651,6 +2751,7 @@ name = "hcie-engine-api"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"approx",
|
||||
"criterion",
|
||||
"dirs 6.0.0",
|
||||
"hcie-blend",
|
||||
"hcie-brush-engine",
|
||||
@@ -3553,12 +3654,32 @@ version = "2.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
|
||||
|
||||
[[package]]
|
||||
name = "is-terminal"
|
||||
version = "0.4.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46"
|
||||
dependencies = [
|
||||
"hermit-abi",
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.10.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.14.0"
|
||||
@@ -4619,6 +4740,12 @@ version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||
|
||||
[[package]]
|
||||
name = "oorandom"
|
||||
version = "11.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
|
||||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.81"
|
||||
@@ -4953,6 +5080,34 @@ version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
|
||||
|
||||
[[package]]
|
||||
name = "plotters"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
"plotters-backend",
|
||||
"plotters-svg",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "plotters-backend"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a"
|
||||
|
||||
[[package]]
|
||||
name = "plotters-svg"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670"
|
||||
dependencies = [
|
||||
"plotters-backend",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "png"
|
||||
version = "0.17.16"
|
||||
@@ -5333,7 +5488,7 @@ dependencies = [
|
||||
"built",
|
||||
"cfg-if",
|
||||
"interpolate_name",
|
||||
"itertools",
|
||||
"itertools 0.14.0",
|
||||
"libc",
|
||||
"libfuzzer-sys",
|
||||
"log",
|
||||
@@ -6519,6 +6674,16 @@ dependencies = [
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinytemplate"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec"
|
||||
version = "1.11.0"
|
||||
|
||||
@@ -40,3 +40,20 @@ rstest = "0.23"
|
||||
proptest = "1.5"
|
||||
approx = "0.5"
|
||||
sha2 = "0.10"
|
||||
criterion = { version = "0.5", features = ["html_reports"] }
|
||||
|
||||
[[bench]]
|
||||
name = "stroke_mask_pooling"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "composite_scratch_pooling"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "below_cache_reuse"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "effects_skip"
|
||||
harness = false
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
//! Criterion benchmark: `below_cache` reuse across consecutive strokes.
|
||||
//!
|
||||
//! ## Purpose
|
||||
//! Verifies that painting multiple strokes on the **same layer** reuses the
|
||||
//! pre-built `below_cache` (composite of all layers below the active one)
|
||||
//! instead of rebuilding it from scratch on every `begin_stroke()`.
|
||||
//!
|
||||
//! On a 4K 10-layer document the below-layer composite costs ~15–25 ms.
|
||||
//! If `below_cache_dirty` is incorrectly set to `true` after every
|
||||
//! `end_stroke()`, each subsequent stroke pays that cost again.
|
||||
//!
|
||||
//! ## Logic & Workflow
|
||||
//! 1. Creates a 3840×2160, 10-layer document (all filled).
|
||||
//! 2. **first_stroke**: Benchmarks a full stroke including below_cache build.
|
||||
//! 3. **second_stroke_same_layer**: Benchmarks a stroke on the same layer
|
||||
//! where `below_cache` should be reused (much cheaper `begin_stroke`).
|
||||
//!
|
||||
//! When the cache works correctly, the second stroke's `begin_stroke()` is
|
||||
//! essentially free (just a dirty-flag check), while the first one must
|
||||
//! composite 9 below-layers into a 33 MB buffer.
|
||||
//!
|
||||
//! ## Arguments & Returns
|
||||
//! Standard criterion benchmark — run with:
|
||||
//! ```bash
|
||||
//! cargo bench -p hcie-engine-api --bench below_cache_reuse
|
||||
//! ```
|
||||
//!
|
||||
//! ## Side Effects / Dependencies
|
||||
//! Allocates ~400 MB (10 layers × 4K RGBA + below_cache + composite_scratch).
|
||||
|
||||
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.
|
||||
const LAYERS: usize = 10;
|
||||
|
||||
/// Builds a 10-layer 4K document. The top layer is the active drawing target.
|
||||
///
|
||||
/// **Purpose:** Creates a worst-case scenario where the below-layer composite
|
||||
/// is expensive (9 filled layers below the active one).
|
||||
/// **Returns:** `(Engine, top_layer_id)`.
|
||||
fn setup_10layer_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)
|
||||
}
|
||||
|
||||
/// Performs a short stroke: begin → 5 segments → composite → end.
|
||||
///
|
||||
/// **Purpose:** Exercises the full `begin_stroke()` (below_cache build/reuse)
|
||||
/// through `end_stroke()` lifecycle.
|
||||
/// **Arguments:** `engine` — mutable engine, `layer_id` — target layer,
|
||||
/// `offset` — slight position offset to avoid degenerate repeat painting.
|
||||
/// **Side Effects:** Mutates layer pixels, triggers composite, commits history.
|
||||
fn do_stroke(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: below_cache build (first stroke) vs reuse (second stroke).
|
||||
///
|
||||
/// **Purpose:** Detects regressions where `below_cache_dirty` is incorrectly
|
||||
/// set to `true` in `end_stroke()` or where `below_cache` is dropped to `None`,
|
||||
/// forcing a full 9-layer re-composite on every stroke.
|
||||
///
|
||||
/// **Logic & Workflow:**
|
||||
/// - `first_stroke_cache_build`: On a fresh engine, `begin_stroke()` must
|
||||
/// build the below_cache from scratch (composite 9 layers → 33 MB buffer).
|
||||
/// - `second_stroke_cache_reuse`: After the first stroke on the same layer,
|
||||
/// `begin_stroke()` should find `below_cache_dirty == false` and
|
||||
/// `below_cache_active_idx == current`, skipping the entire composite.
|
||||
fn bench_below_cache_reuse(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("below_cache_reuse");
|
||||
|
||||
// ── First stroke: cache must be built from scratch ──
|
||||
group.bench_function("first_stroke_cache_build", |b| {
|
||||
b.iter_with_setup(
|
||||
|| setup_10layer_engine(),
|
||||
|(mut engine, layer_id)| {
|
||||
do_stroke(&mut engine, layer_id, 0.0);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
// ── Second stroke on same layer: cache should be reused ──
|
||||
group.bench_function("second_stroke_cache_reuse", |b| {
|
||||
let (mut engine, layer_id) = setup_10layer_engine();
|
||||
// Build the cache with the first stroke
|
||||
do_stroke(&mut engine, layer_id, 0.0);
|
||||
|
||||
let mut offset = 0.0_f32;
|
||||
b.iter(|| {
|
||||
offset += 5.0;
|
||||
do_stroke(&mut engine, layer_id, offset);
|
||||
});
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
criterion_group!(benches, bench_below_cache_reuse);
|
||||
criterion_main!(benches);
|
||||
@@ -0,0 +1,146 @@
|
||||
//! 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);
|
||||
@@ -0,0 +1,166 @@
|
||||
//! 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: hcie_engine_api::BlendMode::Normal,
|
||||
},
|
||||
);
|
||||
|
||||
// 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!(benches, bench_effects_skip);
|
||||
criterion_main!(benches);
|
||||
@@ -0,0 +1,120 @@
|
||||
//! 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);
|
||||
Binary file not shown.
Reference in New Issue
Block a user