Benchmarking & performance skill (eo-processor)
Use this skill to make performance work repeatable, measurable, and safe in a hybrid Rust+PyO3+Python Earth Observation library.
The core objective is to answer, defensibly:
- Did performance change (time, memory, allocations)?
- Did correctness change (numerics, dtype/shape contract)?
- Is the change worth the complexity?
When to activate
Activate this skill when you:
- add a new Rust kernel or change an existing one
- refactor any hot loop / ndarray expression in Rust
- adjust parallelism, chunking, or algorithmic complexity
- change dtype handling (float32/float64) or NaN/Inf behavior (performance often couples to semantics)
- plan to claim speedups in docs/CHANGELOG/PR
Do not activate for trivial doc updates or purely cosmetic refactors.
Principles (performance without breaking trust)
- Correctness is a gate: benchmarking without validating outputs is not acceptable.
- Measure the right thing: avoid benchmarking builds that differ in optimization flags; compare like-for-like.
- Warm up: compilation and caching effects can dominate first-run timings.
- Use representative shapes: EO workloads are usually large (e.g., 1k²–10k² rasters), not just toy arrays.
- Avoid accidental regressions: track both speed and memory; faster-but-alloc-heavy can be worse in real pipelines.
- No unverified claims: do not claim performance improvements without before/after numbers and parameters.
Step 1: Define the benchmark question
Write down:
- What operation is being measured? (function name and version)
- What data sizes/shapes? (e.g., 1000x1000, 5000x5000)
- What data distribution? (random uniform, realistic reflectance range, with/without NaNs)
- What environment? (CPU model optional, OS, Python version, Rust release build, number of threads)
- What metric? (wall time, throughput in pixels/s, peak RSS if available)
Acceptance criteria examples
- “New implementation must be ≥ 1.2× faster than baseline for 5000x5000 float64 arrays”
- “No more than +5% memory overhead compared to baseline”
- “Numerical difference within tolerance (float64: 1e-12; float32: 1e-5)”
Step 2: Always pin build mode & runtime settings
Build mode
- Benchmark only release builds for the Rust extension.
- Ensure you’re not comparing a debug build vs release build.
- If you compare against NumPy, ensure BLAS settings are stable.
Threading
Performance changes can be dominated by threading differences:
- Pin thread counts consistently across runs.
- If the repo uses Rayon or similar internally, control its thread count via environment (document what you used).
- For NumPy baselines, ensure you understand whether BLAS threads are involved.
Stability
- Close other heavy processes.
- Prefer running multiple trials and report median (and optionally p95).
Step 3: Correctness check BEFORE timing
Before timing, validate:
- output shape matches contract
- dtype matches contract
- outputs are finite/NaN per contract
- values match baseline within tolerance
Baseline options
Pick the best available baseline:
- an existing eo-processor implementation (before refactor)
- a pure NumPy reference implementation of the formula
- a known-correct small example with asserted values
If you can’t produce a correctness check, stop and add one first (usually a unit test in tests/).
Step 4: Benchmark protocol (repeatable)
Use this protocol for each benchmark you report:
Generate inputs
- Use seeded random generation for reproducibility.
- Use realistic ranges (e.g., reflectance in [0, 1]) unless the function expects something else.
Warm-up
- Call the function at least once to warm caches and ensure the extension is loaded.
Time multiple trials
- Run N trials (e.g., 5–20).
- Record the median and a dispersion metric (min/max or p95).
Report
- Provide: shape, dtype, trials, median time, throughput, build mode, thread count.
Example Python timing skeleton (adapt to repo norms)
- Use
time.perf_counter() not time.time()
- Avoid counting array creation time in the timed region
- Ensure outputs are consumed to avoid lazy evaluation traps (especially if using Dask wrappers)
Step 5: Evaluate results (interpretation)
Speedup thresholds
- < 1.05×: likely noise or not worth complexity unless it also reduces memory or fixes bugs.
- 1.05×–1.2×: consider whether it’s worth it; check memory/allocations and sustained performance.
- ≥ 1.2×: usually meaningful for EO rasters; still verify no semantic drift.
Check for regressions
- Small arrays sometimes get slower while large arrays get faster. That’s acceptable if the library targets large rasters—just document it.
- Watch for “fast median, slow tail”: if p95 worsened, investigate allocation spikes or scheduling.
Memory and allocations
If possible, assess:
- intermediate allocations (ndarray expression temporaries are common)
- peak memory usage (large rasters can OOM quickly)
If you can’t measure memory precisely, at least reason about allocations:
- Did you add temporaries?
- Did you switch from in-place fill to multiple intermediate arrays?
Step 6: Profiling & root-cause techniques (use selectively)
Use profiling only when the benchmark indicates a meaningful issue.
Common performance traps in this repo’s domain
- Extra temporaries from chained ndarray arithmetic
- Unintended dtype conversions / casts
- Poor cache locality due to iteration order
- Branch-heavy inner loops (NaN handling, conditional masking)
- Parallel overhead dominating small inputs
Suggested methods (choose what applies)
- Add lightweight internal instrumentation (counts, timing spans) temporarily; remove before commit.
- Compare “fused loop” vs “expression-based” implementations.
- Ensure the hottest path is in Rust (not Python glue).
If you can’t profile locally, you can still:
- reduce the workload to isolate which step dominates
- compare variants with minimal changes to infer the cause
Step 7: How to make performance changes safely
Preferred sequence:
- Implement change with correctness tests.
- Benchmark before/after with pinned settings.
- If speedup is real, clean up code and document results.
- If speedup is not real, revert or open a performance issue with data.
Safe optimizations (common wins)
- Fuse computations into a single pass over the array data.
- Precompute invariants outside inner loops.
- Reduce allocations: write into a preallocated output array.
- Avoid repeated bounds checks where safe and idiomatic (without
unsafe unless justified).
Risky optimizations (require stronger evidence)
- Introducing
unsafe
- Changing NaN/Inf behavior for speed
- Changing dtype semantics (float32 vs float64)
- Altering parallel thresholds or scheduling without benchmarks on multiple shapes
Reporting template (use in your response/PR)
When you use this skill, report results in this format:
Benchmark setup
- Function(s):
...
- Build: release (how built)
- Hardware/OS: (if known)
- Threads: (Rayon/BLAS/Python settings)
- Input: shape(s), dtype(s), distribution (seeded)
Correctness
- Baseline: (existing impl / NumPy reference)
- Tolerance: (e.g., rtol/atol)
- Result: pass/fail (and any notes about NaN behavior)
Performance results
For each shape/dtype:
- Before: median X ms (N trials)
- After: median Y ms (N trials)
- Speedup: X/Y (or %)
- Throughput: pixels/s (optional)
- Notes: memory/allocations qualitative notes
Decision
- Keep / revise / revert
- Follow-ups (tests, docs, perf issue)
Definition of done (for perf work)
You’re done when:
Local references (repo)
- Engineering rules & quality gates:
AGENTS.md
- User-facing docs and performance notes:
README.md, QUICKSTART.md
- Complex workflows:
WORKFLOWS.md
- Benchmark artifacts and harnesses:
benchmarking/, dist_bench.json, dist_bench.md, benchmark-*.json
- Scripts and maintenance tooling:
scripts/
- Tests:
tests/
Converted and distributed by TomeVault — claim your Tome and manage your conversions.
1---2name: benchmark-and-perf3description: Measure, compare, and improve eo-processor performance safely. Use when adding/changing Rust compute kernels, refactoring hot paths, or making performance claims. Provides a repeatable benchmark protocol with correctness checks, profiling hints, and reporting guidelines. Use when this capability is needed.4---56# Benchmarking & performance skill (eo-processor)78Use this skill to make performance work **repeatable**, **measurable**, and **safe** in a hybrid Rust+PyO3+Python Earth Observation library.910The core objective is to answer, defensibly:111) Did performance change (time, memory, allocations)?122) Did correctness change (numerics, dtype/shape contract)?133) Is the change worth the complexity?1415---1617## When to activate1819Activate this skill when you:20- add a new Rust kernel or change an existing one21- refactor any hot loop / ndarray expression in Rust22- adjust parallelism, chunking, or algorithmic complexity23- change dtype handling (float32/float64) or NaN/Inf behavior (performance often couples to semantics)24- plan to claim speedups in docs/CHANGELOG/PR2526Do **not** activate for trivial doc updates or purely cosmetic refactors.2728---2930## Principles (performance without breaking trust)31321. **Correctness is a gate**: benchmarking without validating outputs is not acceptable.332. **Measure the right thing**: avoid benchmarking builds that differ in optimization flags; compare like-for-like.343. **Warm up**: compilation and caching effects can dominate first-run timings.354. **Use representative shapes**: EO workloads are usually large (e.g., 1k²–10k² rasters), not just toy arrays.365. **Avoid accidental regressions**: track both speed and memory; faster-but-alloc-heavy can be worse in real pipelines.376. **No unverified claims**: do not claim performance improvements without before/after numbers and parameters.3839---4041## Step 1: Define the benchmark question4243Write down:44- What operation is being measured? (function name and version)45- What data sizes/shapes? (e.g., 1000x1000, 5000x5000)46- What data distribution? (random uniform, realistic reflectance range, with/without NaNs)47- What environment? (CPU model optional, OS, Python version, Rust release build, number of threads)48- What metric? (wall time, throughput in pixels/s, peak RSS if available)4950### Acceptance criteria examples51- “New implementation must be ≥ 1.2× faster than baseline for 5000x5000 float64 arrays”52- “No more than +5% memory overhead compared to baseline”53- “Numerical difference within tolerance (float64: 1e-12; float32: 1e-5)”5455---5657## Step 2: Always pin build mode & runtime settings5859### Build mode60- Benchmark only **release** builds for the Rust extension.61- Ensure you’re not comparing a debug build vs release build.62- If you compare against NumPy, ensure BLAS settings are stable.6364### Threading65Performance changes can be dominated by threading differences:66- Pin thread counts consistently across runs.67- If the repo uses Rayon or similar internally, control its thread count via environment (document what you used).68- For NumPy baselines, ensure you understand whether BLAS threads are involved.6970### Stability71- Close other heavy processes.72- Prefer running multiple trials and report median (and optionally p95).7374---7576## Step 3: Correctness check BEFORE timing7778Before timing, validate:79- output shape matches contract80- dtype matches contract81- outputs are finite/NaN per contract82- values match baseline within tolerance8384### Baseline options85Pick the best available baseline:86- an existing eo-processor implementation (before refactor)87- a pure NumPy reference implementation of the formula88- a known-correct small example with asserted values8990If you can’t produce a correctness check, stop and add one first (usually a unit test in `tests/`).9192---9394## Step 4: Benchmark protocol (repeatable)9596Use this protocol for each benchmark you report:97981) **Generate inputs**99 - Use seeded random generation for reproducibility.100 - Use realistic ranges (e.g., reflectance in [0, 1]) unless the function expects something else.1011022) **Warm-up**103 - Call the function at least once to warm caches and ensure the extension is loaded.1041053) **Time multiple trials**106 - Run N trials (e.g., 5–20).107 - Record the median and a dispersion metric (min/max or p95).1081094) **Report**110 - Provide: shape, dtype, trials, median time, throughput, build mode, thread count.111112### Example Python timing skeleton (adapt to repo norms)113- Use `time.perf_counter()` not `time.time()`114- Avoid counting array creation time in the timed region115- Ensure outputs are consumed to avoid lazy evaluation traps (especially if using Dask wrappers)116117---118119## Step 5: Evaluate results (interpretation)120121### Speedup thresholds122- < 1.05×: likely noise or not worth complexity unless it also reduces memory or fixes bugs.123- 1.05×–1.2×: consider whether it’s worth it; check memory/allocations and sustained performance.124- ≥ 1.2×: usually meaningful for EO rasters; still verify no semantic drift.125126### Check for regressions127- Small arrays sometimes get slower while large arrays get faster. That’s acceptable if the library targets large rasters—just document it.128- Watch for “fast median, slow tail”: if p95 worsened, investigate allocation spikes or scheduling.129130### Memory and allocations131If possible, assess:132- intermediate allocations (ndarray expression temporaries are common)133- peak memory usage (large rasters can OOM quickly)134135If you can’t measure memory precisely, at least reason about allocations:136- Did you add temporaries?137- Did you switch from in-place fill to multiple intermediate arrays?138139---140141## Step 6: Profiling & root-cause techniques (use selectively)142143Use profiling only when the benchmark indicates a meaningful issue.144145### Common performance traps in this repo’s domain146- Extra temporaries from chained ndarray arithmetic147- Unintended dtype conversions / casts148- Poor cache locality due to iteration order149- Branch-heavy inner loops (NaN handling, conditional masking)150- Parallel overhead dominating small inputs151152### Suggested methods (choose what applies)153- Add lightweight internal instrumentation (counts, timing spans) temporarily; remove before commit.154- Compare “fused loop” vs “expression-based” implementations.155- Ensure the hottest path is in Rust (not Python glue).156157If you can’t profile locally, you can still:158- reduce the workload to isolate which step dominates159- compare variants with minimal changes to infer the cause160161---162163## Step 7: How to make performance changes safely164165Preferred sequence:1661. Implement change with correctness tests.1672. Benchmark before/after with pinned settings.1683. If speedup is real, clean up code and document results.1694. If speedup is not real, revert or open a performance issue with data.170171### Safe optimizations (common wins)172- Fuse computations into a single pass over the array data.173- Precompute invariants outside inner loops.174- Reduce allocations: write into a preallocated output array.175- Avoid repeated bounds checks where safe and idiomatic (without `unsafe` unless justified).176177### Risky optimizations (require stronger evidence)178- Introducing `unsafe`179- Changing NaN/Inf behavior for speed180- Changing dtype semantics (float32 vs float64)181- Altering parallel thresholds or scheduling without benchmarks on multiple shapes182183---184185## Reporting template (use in your response/PR)186187When you use this skill, report results in this format:188189### Benchmark setup190- Function(s): `...`191- Build: release (how built)192- Hardware/OS: (if known)193- Threads: (Rayon/BLAS/Python settings)194- Input: shape(s), dtype(s), distribution (seeded)195196### Correctness197- Baseline: (existing impl / NumPy reference)198- Tolerance: (e.g., rtol/atol)199- Result: pass/fail (and any notes about NaN behavior)200201### Performance results202For each shape/dtype:203- Before: median X ms (N trials)204- After: median Y ms (N trials)205- Speedup: X/Y (or %)206- Throughput: pixels/s (optional)207- Notes: memory/allocations qualitative notes208209### Decision210- Keep / revise / revert211- Follow-ups (tests, docs, perf issue)212213---214215## Definition of done (for perf work)216217You’re done when:218- [ ] Correctness is validated against a baseline219- [ ] Benchmarks are repeatable and documented (inputs + settings)220- [ ] Any performance claim has before/after numbers221- [ ] No unacceptable memory regression is introduced (or it’s explicitly justified)222- [ ] Tests cover at least one representative correctness case and key edge cases223- [ ] The change doesn’t silently alter public semantics (or docs/versioning reflect it)224225---226227## Local references (repo)228229- Engineering rules & quality gates: `AGENTS.md`230- User-facing docs and performance notes: `README.md`, `QUICKSTART.md`231- Complex workflows: `WORKFLOWS.md`232- Benchmark artifacts and harnesses: `benchmarking/`, `dist_bench.json`, `dist_bench.md`, `benchmark-*.json`233- Scripts and maintenance tooling: `scripts/`234- Tests: `tests/`235236---237> Converted and distributed by [TomeVault](https://tomevault.io/claim/bnjam) — claim your Tome and manage your conversions.238<!-- tomevault:4.0:skill_md:2026-04-14 -->