Files
hcie-rust-v3.05/.kilo/plans/1784932432251-criterion-merge-and-optimize.md
T
Your Name 1dabd9c31e
mandatory-regression-gate / deterministic-tests (push) Has been cancelled
mandatory-regression-gate / protected-performance-path (push) Has been cancelled
Optimize stroke cache and benchmark configurations
- 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.
2026-07-25 05:20:12 +03:00

219 lines
10 KiB
Markdown

# Criterion Merge & 2x Optimization Plan
## Context
Two criterion output directories exist:
- **Root `criterion/`** — has HTML report (`report/index.html`) + all 8 `gold_standard` baseline variants
- **`hcie-engine-api/criterion/`** — partial duplicate (4 benchmark dirs, no report, no gold_standard)
Root cause: each benchmark file hardcodes `Criterion::default().output_directory(Path::new("criterion"))` — a relative path that resolves differently depending on CWD. No `.cargo/config.toml` exists to set `CRITERION_HOME`.
Gold standard timings (from root `criterion/`):
| Benchmark | Mean | Dominated by |
|-----------|------|--------------|
| `below_cache_reuse/first_stroke_cache_build` | 0.81 s | Full 9-layer below-cache composite |
| `below_cache_reuse/second_stroke_cache_reuse` | 0.015 s | Cache hit — fast |
| `composite_scratch_pooling/cold_composite` | 1.05 s | Full 10-layer composite + scratch alloc |
| `composite_scratch_pooling/warm_composite` | 0.010 s | Pooled scratch — fast |
| `effects_skip/no_effects_composite` | 0.014 s | Effects skipped — fast |
| `effects_skip/with_one_dropshadow` | 0.015 s | Single effect — fast |
| `stroke_mask_pooling/cold_first_stroke` | 0.046 s | Mask alloc + first stroke |
| `stroke_mask_pooling/warm_10th_stroke` | 0.017 s | Pooled mask — fast |
## Goal
1. Merge criterion directories into single root `criterion/`
2. Fix output path configuration so benchmarks always write to the same place
3. Achieve 2x speedup across all 8 benchmark variants vs `gold_standard` baseline
4. Update `notlar.txt` and `criterion.md` with correct paths
## Task List
### Phase 1: Directory Merge & Path Fix (config-only, no engine changes)
**1.1** Delete `hcie-engine-api/criterion/` directory entirely
```bash
rm -rf hcie-engine-api/criterion/
```
**1.2** Create `.cargo/config.toml` at workspace root:
```toml
[env]
CRITERION_HOME = { value = "criterion", relative = true }
```
This ensures all `cargo bench` invocations write to `./criterion/` relative to workspace root, regardless of CWD.
**1.3** Remove hardcoded `output_directory` from all 4 benchmark files. Change:
```rust
criterion_group!{
name = benches;
config = Criterion::default().output_directory(std::path::Path::new("criterion"));
targets = bench_xxx
}
```
To:
```rust
criterion_group!(benches, bench_xxx);
criterion_main!(benches);
```
Files to edit:
- `hcie-engine-api/benches/stroke_mask_pooling.rs:119-124`
- `hcie-engine-api/benches/composite_scratch_pooling.rs:145-150`
- `hcie-engine-api/benches/below_cache_reuse.rs:145-150`
- `hcie-engine-api/benches/effects_skip.rs:165-170`
**1.4** Remove `bench = false` from `hcie-engine-api/Cargo.toml` `[lib]` section (line 37). This was a workaround; with explicit `[[bench]]` targets it's not needed.
**1.5** Update `.gitignore` — add `criterion/` to prevent benchmark output from being committed (the gold_standard baseline should be kept but the report and run data are generated).
**1.6** Update `notlar.txt`:
- Fix line 19: `CRITERION_HOME=criterion` → reference `.cargo/config.toml`
- Fix line 20: `./criterion/report/index.html` stays correct (now guaranteed by config.toml)
- Add note about `.cargo/config.toml` setting `CRITERION_HOME`
**1.7** Update `criterion.md`:
- Fix line 40: `./criterion/report/index.html` stays correct
- Add note about `.cargo/config.toml` configuration
**1.8** Verify: run `cargo bench -p hcie-engine-api` and confirm output goes to root `criterion/` only.
### Phase 2: Save Fresh Baseline & Verify
**2.1** Save a fresh `gold_standard` baseline after path fixes:
```bash
cargo bench -p hcie-engine-api -- --save-baseline gold_standard
```
**2.2** Run regression comparison to confirm baselines match:
```bash
cargo bench -p hcie-engine-api -- --baseline gold_standard
```
**2.3** Run visual regression and performance tests to confirm no breakage:
```bash
cargo test -p hcie-engine-api --test visual_regression
cargo test -p hcie-engine-api --test performance_stroke_4k -- --nocapture
```
### Phase 3: Optimization Loop
For each optimization, follow this cycle:
1. Unlock target crate: `./unlock.sh <crate>`
2. Apply optimization
3. Build: `cargo build -p hcie-engine-api`
4. Run benchmarks: `cargo bench -p hcie-engine-api -- --baseline gold_standard`
5. Check for regressions: `cargo test -p hcie-engine-api --test visual_regression`
6. If improvement < 2x, continue to next optimization
7. Lock crate: `./lock.sh <crate>`
**Optimization targets (in priority order):**
#### 3.1 Pre-allocate pooled buffers in `Engine::new()` (hcie-engine-api)
Currently `composite_scratch`, `active_stroke_mask`, `below_cache`, and `stroke_before_buf` are all `None` at construction and allocated lazily on first use. The cold benchmarks pay this allocation cost.
Change `Engine::new_with_options()` in `hcie-engine-api/src/lib.rs` to pre-allocate:
- `composite_scratch = Some(vec![0u8; w * h * 4])` — ~33MB
- `active_stroke_mask = Some(vec![0u8; w * h])` — ~8MB
- `below_cache = Some(vec![0u8; w * h * 4])` — ~33MB
- `stroke_before_buf = Some(vec![0u8; w * h * 4])` — ~33MB
This moves allocation cost from first-stroke time to engine-creation time (which is excluded from benchmark measurement via `iter_with_setup`). Expected impact: cold benchmarks drop significantly because they no longer include allocation.
#### 3.2 Opaque-layer early-exit in composite (hcie-composite, hcie-engine-api)
In `composite_tiled_into` (`hcie-composite/src/tiled.rs`), when compositing layers bottom-to-top, if a layer is:
- Fully opaque (alpha=255 everywhere in the dirty region)
- Normal blend mode
- 100% opacity
...then all layers below it are completely occluded and can be skipped.
Implementation: Before compositing each layer, check if the output buffer is already fully opaque in the dirty region. If so, skip remaining below-layers. This is most impactful for the `first_stroke_cache_build` and `cold_composite` benchmarks where all 10 layers are filled with opaque colors.
#### 3.3 `copy_from_slice` fast-path for opaque Normal layers (hcie-composite)
In the per-pixel blend loop, add a fast-path: if the source pixel is fully opaque (alpha=255) and blend mode is Normal, use `copy_from_slice` for the entire row instead of per-pixel blending. This avoids the blend math for the common case of opaque layers.
#### 3.4 Reduce `Vec::clone()` in hot paths (hcie-engine-api)
In `draw_filled_rect_rgba` (`stroke_brush.rs:582`): `layer.pixels.clone()` creates a full ~33MB copy for undo. Replace with `copy_from_slice` into a pooled buffer.
In `begin_stroke` (`stroke_cache.rs:200-208`): `layer.effects.clone()` and `layer.styles.clone()` are small but unnecessary — effects/styles are typically empty. Add an early-return if both are empty.
#### 3.5 Parallelize `rebuild_below_cache_if_needed` (hcie-engine-api)
The below-cache rebuild in `stroke_cache.rs:514-611` composites all layers below the active one. This is currently sequential. Use Rayon to parallelize the tile compositing within this function, similar to how `render_composite_region` uses `par_chunks_mut`.
#### 3.6 SIMD blend operations (hcie-blend, if needed)
If the above optimizations don't reach 2x, explore SIMD-accelerated blend operations in `hcie-blend`. The `blend_pixel` function is called millions of times per composite. Using `std::simd` or explicit SSE/AVX intrinsics could yield 4-8x speedup on blend operations.
### Phase 4: Final Verification
**4.1** Run full benchmark suite and compare against original gold_standard:
```bash
cargo bench -p hcie-engine-api -- --baseline gold_standard
```
**4.2** Run visual regression tests:
```bash
cargo test -p hcie-engine-api --test visual_regression
cargo test -p hcie-engine-api --test performance_stroke_4k -- --nocapture
```
**4.3** Save final optimized baseline:
```bash
cargo bench -p hcie-engine-api -- --save-baseline gold_standard
```
**4.4** Update `notlar.txt` with final benchmark results.
## Files to Modify
| File | Change |
|------|--------|
| `.cargo/config.toml` | **Create** — set `CRITERION_HOME` |
| `.gitignore` | Add `criterion/` entry |
| `hcie-engine-api/Cargo.toml` | Remove `bench = false` from `[lib]` |
| `hcie-engine-api/benches/stroke_mask_pooling.rs` | Remove `output_directory`, simplify `criterion_group!` |
| `hcie-engine-api/benches/composite_scratch_pooling.rs` | Same |
| `hcie-engine-api/benches/below_cache_reuse.rs` | Same |
| `hcie-engine-api/benches/effects_skip.rs` | Same |
| `notlar.txt` | Update paths and commands |
| `criterion.md` | Update paths |
| `hcie-engine-api/src/lib.rs` | Pre-allocate buffers in `new_with_options()` |
| `hcie-engine-api/src/stroke_cache.rs` | Optimize `begin_stroke`, `rebuild_below_cache_if_needed` |
| `hcie-engine-api/src/stroke_brush.rs` | Optimize `draw_filled_rect_rgba` clone |
| `hcie-composite/src/tiled.rs` | Opaque-layer skip, `copy_from_slice` fast-path |
## Files to Delete
| Path | Reason |
|------|--------|
| `hcie-engine-api/criterion/` | Duplicate — root `criterion/` is canonical |
## Locked Crates That Need Unlocking
| Crate | Reason |
|-------|--------|
| `hcie-engine-api` | Pre-allocation, hot-path optimizations |
| `hcie-composite` | Opaque-layer skip, copy_from_slice fast-path |
| `hcie-blend` | SIMD blend (only if Phase 3.1-3.5 insufficient) |
## Validation
After each phase:
- `cargo build -p hcie-engine-api` must succeed
- `cargo bench -p hcie-engine-api` must produce output in root `criterion/` only
- `cargo test -p hcie-engine-api --test visual_regression` must pass (8/8)
- `cargo test -p hcie-engine-api --test performance_stroke_4k -- --nocapture` must not regress
- `cargo bench -p hcie-engine-api -- --baseline gold_standard` must show improvement (not regression)
## Risks
- **Pre-allocation increases engine memory footprint** by ~107MB (33+8+33+33). This is acceptable for a 4K image editor where the document itself is already ~33MB per layer.
- **Opaque-layer skip changes composite behavior** if a layer has non-Normal blend mode but appears opaque. The check must verify both alpha=255 AND blend mode=Normal.
- **SIMD requires `std::simd` (unstable)** or explicit intrinsics with `cfg(target_feature)` guards. Prefer portable approaches first.