River Benchmarks
July 18, 2026 · View on GitHub
River benchmarks use airspeed velocity (asv) to track the timing performance of models across git history.
Each benchmark runs progressive validation (evaluate.iter_progressive_val_score) for a model on a dataset, measuring wall-clock time. Results are tracked per commit so that performance regressions can be detected automatically.
Prerequisites
pip install asv virtualenv
Running benchmarks locally
All commands should be run from the benchmarks/ directory.
Quick run (current Python, no environment setup)
asv run --python=same --quick --show-stderr
Run a specific benchmark class
asv run --python=same --quick --bench BinaryClassification
asv run --python=same --quick --bench Regression
Run a specific model/dataset combination
asv run --python=same --quick --bench "BinaryClassification.*Logistic.*Bananas"
Compare two branches
asv continuous main my-feature-branch --bench BinaryClassification
This runs benchmarks on both branches and highlights significant timing changes.
Find which commit caused a regression
asv find v0.22.0..main --bench "Regression.time_progressive_val_score\(Linear Regression, ChickWeights\)"
Viewing results
asv publish
asv preview
This generates a static website and serves it locally. Open the printed URL in your browser.
Adding a new model
- Edit
benchmarks/common.py - Add the model name to the appropriate
*_MODELSlist (e.g.,BINARY_CLF_MODELS) - Add a factory lambda to the corresponding
_*_MODEL_REGISTRYdict - Run
asv checkto verify discovery
Adding a new track
- Create
benchmarks/bench_<track_name>.pyfollowing the pattern in existing files - Add the model/dataset lists and registries to
benchmarks/common.py - Run
asv checkto verify discovery
CI
Benchmarks run automatically on push to main via GitHub Actions. Results are stored on the asv branch and published to GitHub Pages.
CodSpeed
CodSpeed runs the small pytest benchmark suite in benchmarks/codspeed/python/ and the
Rust criterion benchmarks in benchmarks/codspeed/rust/ on every pull request and every
push to main. Both jobs use CodSpeed's CPU simulation mode, which produces deterministic
instruction-count measurements and flamegraphs. Pull requests get a CodSpeed comment plus
status checks for the Python benchmarks, Rust benchmarks, and the aggregate performance
analysis.
To keep CI fast, the Python job is split into two parallel shards (see the matrix in
.github/workflows/codspeed.yml): a "heavy" shard listing the most expensive files
explicitly, and a "rest" shard running everything else. New benchmark files land in the
"rest" shard automatically; if the two shards drift far apart in duration, move files
between them. Sharding does not affect benchmark identity or the CodSpeed report — all
uploads for a commit are aggregated. Keep each benchmark body around 100 ms of real time
or less: the CPU simulator runs it once at roughly 30–100× overhead, so oversized
workloads slow every PR without adding signal.
Run CodSpeed benchmarks locally
make benchmark # all Python benchmarks, local walltime table
make benchmark K=logistic # filter by pytest -k expression
make benchmark-rust # all Rust criterion benches
make benchmark-rust BENCH=stats_bench
Local Python runs are smoke tests, not the merge-gating measurement. Outside CodSpeed CI,
pytest-codspeed falls back to a normal walltime table, so results depend on your
machine. The PR comment is the source of truth for comparisons. On Linux, power users can
run simulation locally with the CodSpeed CLI:
curl -fsSL https://codspeed.io/install.sh | bash
codspeed auth login
codspeed run -m simulation -- uv run --no-sync pytest benchmarks/codspeed --codspeed -o addopts=""
Add a CodSpeed benchmark
Copy this template into benchmarks/codspeed/python/test_<module>.py and keep the benchmark
name stable after it lands; renaming a benchmark loses its CodSpeed history.
from __future__ import annotations
import pytest
from river import <module>
from workloads import binary_stream # or regression_stream, scalar_series, ...
pytestmark = pytest.mark.benchmark(group="<module>")
def test_<estimator>_learn(benchmark) -> None:
stream = binary_stream()
def run() -> None:
model = <module>.<Estimator>(seed=42)
for x, y in stream:
model.learn_one(x, y)
benchmark(run)
Rust benchmarks live in benchmarks/codspeed/rust/. Add each new Rust bench target to
Cargo.toml explicitly:
[[bench]]
name = "<module>_bench"
path = "benchmarks/codspeed/rust/<module>_bench.rs"
harness = false
Use deterministic workloads only:
- Seed every generator and every stochastic estimator.
- Do not use network access or read the clock inside a benchmark.
- Materialize data in
workloads.py, outside the measured callable. - Treat cached workload lists as frozen; do not mutate shared data.
- Build a fresh estimator inside each measured learning callable.
Reading CodSpeed results
The PR comment links to each benchmark, its comparison against main, and the
differential flamegraph. A red CodSpeed performance status means at least one benchmark
regressed beyond the configured threshold. The default threshold is 10%; noisy benchmarks
can get a per-benchmark threshold in CodSpeed instead of loosening the global one.
Intentional regressions should be explained in the PR description and acknowledged by an admin in the CodSpeed UI. Do not bypass GitHub branch protection for performance regressions; acknowledgement keeps the dashboard and branch status consistent.