# pipe-io v0.4.0 - Polish and benchmarking

**Date:** 2026-05-19
**Compare:** `v0.3.0...v0.4.0`

## Headline

Roadmap milestone `0.4.0` - benchmarks and a measured performance
section in the docs. No public API changes from `0.3.0`. The
release adds `benches/pipeline.rs`, `docs/BENCH.md`, and a
performance summary inside `docs/API.md`.

The numbers are healthy. On a developer laptop (Windows, x86_64,
release with `lto = "thin"`), a single map runs at ~260 M items / s
through the sync driver and each added stage costs roughly 1-2 ns
per item - one vtable dispatch through the boxed stage chain. That
gap to the raw-iterator baseline is the architectural cost of the
type-erased builder, documented in `BENCH.md`. No optimization pass
was needed for this release.

## What changed since `v0.3.0`

### New - `benches/pipeline.rs`

Hand-rolled (no Criterion) throughput bench covering:

- Raw `Iterator::map().filter()` baseline (for context).
- `source -> null sink` (driver overhead floor).
- `source -> map -> sink`.
- `source -> map -> filter -> map -> sink`.
- `source -> filter(false) -> sink` (drop-everything path).
- `source -> batch(100) -> sink`.
- `source -> try_map(ok) -> sink` (happy path).
- `source -> try_map(50% err, Continue)` (error policy cost).
- `threaded driver: source -> map -> sink`.

Each scenario warms up once, then times one timed pass of 200,000
items. Per-item nanoseconds and millions of items per second are
printed to stdout. Reproduce with:

```text
cargo bench --bench pipeline
```

### New - `docs/BENCH.md`

Methodology, hardware, full results table, an interpretation of
each scenario, and a note on the architectural cost. Cross-linked
from `docs/API.md`.

### Changed - `docs/API.md` gets a Performance section

Summary table with headline ns / item and M items / s for the
common shapes. Links out to `BENCH.md` for the full picture.

### Changed - `Cargo.toml`

```toml
[[bench]]
name = "pipeline"
harness = false
required-features = ["std"]
```

## Public surface

Unchanged from `0.3.0`. See [`docs/API.md`](../API.md).

### MSRV

`1.75`, unchanged.

### Runtime dependencies

Zero, unchanged.

## Headline numbers

200,000 items per run. Hardware: developer laptop (Windows 11,
x86_64-pc-windows-msvc, release with `lto = "thin"`).

| Scenario                                  | ns / item | M items / s |
|-------------------------------------------|----------:|------------:|
| raw `Iterator::map().filter()` baseline   |     ~0    |   ~2,200    |
| source -> null sink                       |      1    |     ~500    |
| source -> map -> sink                     |      3    |     ~260    |
| source -> map -> filter -> map -> sink    |      5    |     ~170    |
| source -> filter(false) -> sink           |      2    |     ~340    |
| source -> batch(100) -> sink              |      7    |     ~140    |
| source -> try_map(ok) -> sink             |      3    |     ~290    |
| source -> try_map(50% err, Continue)      |      3    |     ~325    |
| threaded driver: source -> map -> sink    |     46    |      ~21    |

Reading the numbers:

- Each stage edge costs one boxed-dyn vtable call (~1-2 ns per item).
  Three stages in a chain adds ~4-5 ns vs the source-only baseline.
- Filtered-out items pay only the filter closure cost; downstream
  stages are skipped.
- The batching cost (7 ns / item at batch size 100) is the per-item
  `Vec::push` plus the amortized cost of emitting one batch every
  100 items.
- `try_map` matches plain `map` in the happy path. With 50% errors
  routed through `Continue`, throughput is unchanged - the swallowed
  error is one vtable hop.
- The threaded driver pays for one thread spawn + join plus moving
  the pipeline closure into the worker. For 200,000 items these
  fixed costs amortize to ~40 ns / item. Long-running pipelines or
  blocking source pulls amortize better; this driver is not a
  speedup for trivially-fast stages.

## Verification

Local matrix (Windows x86_64-pc-windows-msvc, stable 1.95):

- `cargo build` (default features) [OK]
- `cargo build --no-default-features` [OK]
- `cargo build --all-features` [OK]
- `cargo +1.75 build --all-features` (MSRV) [OK]
- `cargo fmt --all -- --check` [OK]
- `cargo clippy --all-targets --no-default-features -- -D warnings` [OK]
- `cargo clippy --all-targets --all-features -- -D warnings` [OK]
- `cargo test --all-features` - **50 tests pass, 0 ignored**
  (27 unit + 15 integration + 8 doctest)
- `cargo bench --bench pipeline` - runs to completion with the
  headline numbers above
- `RUSTDOCFLAGS="-D warnings -D rustdoc::broken-intra-doc-links" cargo doc --all-features --no-deps` [OK]
- Banned-word and em-dash scan across `src/`, `tests/`, `benches/`,
  `docs/`, `README.md`, `CHANGELOG.md`, `REPS.md`: 0 hits

CI matrix continues to run on ubuntu-latest, macos-latest,
windows-latest.

## Migration

### From `v0.3.0`

No source changes required. The public API surface is unchanged.

## Stability commitment

Unchanged. `0.x.y` releases are not API-stable; API freeze begins
at `1.0.0` (see `REPS.md` section 8). The three slices deferred
in `0.3.0` (the `window` module, the unified `Driver` trait, and
the dead-letter sink wiring) remain deferred and may evolve before
they land.

## Release ceremony

```bash
git tag -a v0.4.0 -m "Release v0.4.0 - Polish and benchmarking"
git push origin main
git push origin v0.4.0

cargo publish

cargo search pipe-io | head -3
```

GitHub release title: `v0.4.0 - Polish and benchmarking`. Not
tagged as pre-release.

## Acknowledgements

`pipe-io 0.4.0` closes out the third roadmap milestone (Polish and
benchmarking). The implementation is steady-state: no public API
shifts, no dependency adds, MSRV holds at 1.75, and the crate ships
zero runtime dependencies with `#![forbid(unsafe_code)]`.

The next roadmap stop is the `0.9.x` pre-1.0 stabilization line
(property tests, fuzz targets, `cargo-semver-checks` in CI, final
documentation audit, migration guide). The three locked-but-not-
shipped surfaces (window, Driver trait, dead-letter wiring) sit in
between.

---

**Full Changelog:** https://github.com/jamesgober/pipe-io/compare/v0.3.0...v0.4.0
