Performance Guide
Stillwater aims for low-overhead validation, effects, and error context. Effect composition uses concrete types without combinator boxing by default. This guide documents how those properties are measured rather than treating “zero cost” as a blanket promise.
Running benchmarks locally
# All benchmark suites (validation, effects, context, parallel)
cargo bench --features async
# Individual suites
cargo bench --bench validation
cargo bench --bench effects --features async
cargo bench --bench context
cargo bench --bench parallel --features async
Criterion writes HTML reports under target/criterion/. Open any report/index.html in a browser for detailed plots.
For a quick smoke run (pass --sample-size only to criterion benches, not the library test harness):
cargo bench --features async \
--bench validation --bench effects --bench context --bench parallel \
-- --sample-size 10
Benchmark categories
| Suite | File | Compares |
|---|---|---|
| Validation | benches/validation.rs | Validation::all_vec vs manual Result accumulation |
| Effects | benches/effects.rs | Combinator chain (map + and_then) vs hand-written async |
| Context | benches/context.rs | ContextError layering vs equivalent manual struct |
| Parallel | benches/parallel.rs | par_all / par2 vs sequential run |
Each suite pairs Stillwater APIs with hand-written equivalents so overhead is measurable, not assumed.
Expected characteristics
On typical release builds:
- Validation — Within a few percent of manual accumulation for success paths; error accumulation adds work proportional to error count in both paths.
- Effects — Combinator chains should match manual async code when effects use concrete types (no
.boxed()in the hot path). - Context —
ContextErrorshould be within a few percent of a manualVec<String>trail; both allocate context strings. - Parallel —
par_allon cheappureeffects measures scheduling overhead; use I/O-heavy effects locally to validate real speedup.
The project success metric is within 5% of hand-written equivalent code on CPU-bound paths. Absolute numbers vary by machine; compare relative ratios and track trends over time.
CI integration
The Benchmarks workflow:
- On pull requests: runs benchmarks with reduced samples as a compile-and-run smoke test.
- On push to master: runs full benchmarks and stores results via github-action-benchmark for historical comparison.
Regressions are detected by comparing new results to the stored baseline on the default branch.
Size smoke tests
Checks in src/effect/combinators/size_tests.rs measure a few concrete struct sizes.
They can catch some size growth; a small struct can still contain pointers to heap
allocations. They do not prove inline storage, allocation counts, or runtime overhead.
Use allocation instrumentation for allocation claims and runtime benchmarks for hot paths.
Practices for low-overhead usage:
- Prefer concrete effect types; use
.boxed()where type erasure is needed. Borrowed async construction boxes its future, and helpers such asIO, traversal, and retry return boxed effects internally. Account for those costs too. - Use
Validation::all_vec/ tuplevalidate_allfor accumulation instead of fail-fast?when you need every error. - Add
ContextErrorat boundaries, not inside tight inner loops.
Updating this document
After significant API changes, re-run cargo bench --features async on release mode and note any ratio shifts in PR descriptions. Do not commit machine-specific timings here; rely on CI history for regression tracking.