A lightweight Python microbenchmarking library with a Rust core (PyO3 + maturin). Designed as a "criterion for Python" — minimal harness overhead, statistically rigorous output, and a small CLI that drops into CI.
import blackbox_bench
bench = blackbox_bench.Bench()
@bench.benchmark
def hash_1kb():
import hashlib
hashlib.sha256(b"x" * 1024).digest()
bench.run()
bench.report() # human-readable table
bench.report(format="json", path="results.json")blackbox-bench results
─────────────────────────────────────────────────────────────────────────────────
Name Mean Median StdDev Min Max Ops/sec CI 95% Outliers
─────────────────────────────────────────────────────────────────────────────────
hash_1kb 1.4 µs 1.4 µs 12.0 ns 1.4 µs 1.5 µs 710,221 [1.4 µs, 1.4 µs] 3
─────────────────────────────────────────────────────────────────────────────────
- Per-batch timing with auto-batch-sizing — sub-microsecond functions are batched until each sample takes ≥5 µs, well above the timer resolution floor.
- Per-iteration overhead measurement and subtraction — the harness probes itself at startup so reported nanoseconds attribute time to your code, not blackbox-bench.
- Bootstrap 95% confidence intervals for the mean, plus Tukey or MAD outlier detection.
blackbox_bench.black_box(value)— opaque pass-through that the optimiser can't see through.bench.iter_batched(setup, routine)— setup runs once per sample (untimed); routine runs the timed batch.throughput=—@bench.benchmark(throughput=1024)reports MB/s alongside ops/sec.params=[...]— parameterised benchmarks; one result per parameter.- Opt-in HDR histograms —
Bench(histogram=True)attaches a percentile-queryableHdrHistogramto each result. blackbox_bench.compare(baseline_json, current_json)— classifies each row asunchanged/regressed/improved/new/removedusing CI overlap, not rawchange_pct.- Four reporters — table, JSON, self-contained HTML (with inline SVG sparklines), JUnit-compatible XML (with a raw alternative).
blackbox-bench run --profile— wraps each benchmark inpy-spyand emits SVG flamegraphs alongside the results.
pip install blackbox-benchPre-built abi3 wheels are published for cpython 3.10 / 3.11 / 3.12 / 3.13 on linux (x86_64 + aarch64), macOS (x86_64 + aarch64), and windows x86_64. Building from source requires a Rust toolchain.
Optional extras:
pip install blackbox-bench[profile] # adds py-spy for `blackbox-bench run --profile`import blackbox_bench
bench = blackbox_bench.Bench(
warmup=5,
target_time_ns=1_000_000_000,
outlier_method="tukey", # or "mad" / "none"
overhead_subtract=True,
histogram=False,
)
@bench.benchmark
def quick(): sum(range(100))
@bench.benchmark(name="hash_kb", throughput=1024, params=[10, 100, 1000])
def hashing(n):
import hashlib
hashlib.sha256(b"x" * n).digest()
results = bench.run()
bench.report(format="html", path="results.html")import random
@bench.benchmark
def sort_random():
return bench.iter_batched(
setup=lambda: random.sample(range(1_000), 1_000),
routine=lambda xs: sorted(xs),
)setup runs once per sample and isn't timed; routine is the timed call inside the batch.
with bench.measure("payload_build"):
payload = build_huge_payload()
bench.run() # appends measured contexts to results# bench_hashing.py
import blackbox_bench
@blackbox_bench.benchmark
def sha256_1kb():
import hashlib
hashlib.sha256(b"x" * 1024).digest()blackbox-bench run bench_hashing.py --warmup 5 --iterations 100
blackbox-bench run benches/ --format html --output report.html
blackbox-bench run benches/ --save baseline.json
blackbox-bench compare baseline.json current.json # CI-classified diffimport json
report = blackbox_bench.compare(
open("baseline.json").read(),
open("current.json").read(),
)
for row in report.rows:
print(row.name, row.classification, row.change_pct)ComparisonReport.format("xml") emits JUnit with <failure> on regressed rows — drop the file into Jenkins/GitHub Actions test reporters.
The harness has to stay small relative to the user's function:
- Tight sampling loop —
Instant::now()and the per-batch call dispatch are raw FFI (PyObject_CallNoArgs/PyObject_CallObject), skipping PyO3's higher-level wrappers inside the timed window. - GIL released during stats — bootstrap CI, Tukey/MAD, and the histogram run inside
py.detach(...)so other Python threads aren't blocked while blackbox-bench crunches its samples. - Reused scratch buffers —
median,tukey,mad, andbootstrap_ci_meanshare twoVecs owned by theRunner; a 100-benchmark suite still allocates only once for the lot.
The criterion benchmarks at benches/rust_internals.rs measure these primitives directly. benches/bench_dogfood.py measures the assembled harness end-to-end (the empty-pass benchmark should report ~0–1 ns after overhead subtraction).
See MIGRATION.md. Most v0.1.0 code only needs an import swap; the one surface that changed without alias is Bench.report(json_output=True) → Bench.report(format="json"). For exact v0.1.0 semantics:
from blackbox_bench.legacy import Bench, benchmark, BenchmarkResultThe legacy shim is removed in v1.1.
MIT — see LICENSE.