Skip to main content

benchmatrix

CI Documentation Docker CodeQL OpenSSF Scorecard Workflow lint Python 3.11-3.14 Typed with basedpyright Linted with Ruff Coverage gate: 95% SBOM: CycloneDX 1.6

Compare Python performance across implementations and inputs with fewer false alarms.

benchmatrix adds benchmark matrices, repeated-run collection, paired experiments, and regression checks to pytest-benchmark. Define your implementations and input cases once; benchmatrix measures every combination and compares a baseline with a candidate.

Before reporting a regression, it checks that both sides ran in compatible environments, measured the same matrix, and collected enough independent runs. It then calculates a run-level confidence interval and separates meaningful changes, practical equivalence, and inconclusive results.

uv run benchmatrix measure --runs 5 --output baseline tests/test_benchmarks.py
# Make a change, then measure again.
uv run benchmatrix measure --runs 5 --output candidate tests/test_benchmarks.py
uv run benchmatrix compare baseline candidate --fail-on-regression

Results are available as readable terminal output, versioned JSON, Markdown, and GitHub Actions summaries.

Preview of benchmatrix collecting and comparing repeated benchmark runs
Watch benchmatrix catch an intentional regression (1 minute)

[!TIP] Read the benchmatrix documentation for the quickstart, usage guides, and API reference.

Install

uv add --dev benchmatrix

or:

python -m pip install benchmatrix

Quickstart

Create a benchmark matrix from callables in a pytest file:

from benchmatrix import BenchmarkCase, make_benchmark_test

implementations = {
    "builtin": sum,
    "loop": lambda vs: sum(v for v in vs),
}

cases = [
    BenchmarkCase.from_values(
        "small",
        list(range(100)),
        work_units=100,
        work_unit_name="items",
    ),
]

test_sum_matrix = make_benchmark_test(implementations, cases)

Keep ordinary correctness tests alongside the generated benchmark; timing a wrong answer does not make it useful evidence.

Collect a baseline, make your change, and collect a candidate on the same machine:

uv run benchmatrix measure --runs 5 --output baseline tests/test_sum_benchmark.py
# Make the change you want to evaluate.
uv run benchmatrix measure --runs 5 --output candidate tests/test_sum_benchmark.py

Compare the repeated runs and fail the command when the evidence shows a regression:

uv run benchmatrix compare baseline candidate --threshold 5% --fail-on-regression

Each output directory contains the individual pytest-benchmark JSON files and a manifest recording the command, matrix, environment, and collection lifecycle. Resume an interrupted collection without overwriting earlier runs:

uv run benchmatrix measure --resume --output candidate
uv run benchmatrix measure --retry-failed --output candidate

The same run and comparison model is available from Python:

from benchmatrix import load_benchmark_run_group

baseline = load_benchmark_run_group("baseline")
candidate = load_benchmark_run_group("candidate")
comparison = baseline.compare_to(candidate)

for cell in comparison.regressed:
    print(cell.implementation_name, cell.case_name, cell.metric_name)

For a drift-resistant experiment, collect adjacent matched blocks with collect-paired. Baseline-first (AB) and candidate-first (BA) blocks alternate, and both members of a pair use the same balanced matrix-cell order. Separate the two commands with ::: after the usual -- delimiter:

uv run benchmatrix collect-paired \
    --random-seed 20260801 \
    --output paired-runs \
    --baseline-cwd ../project-baseline \
    --candidate-cwd . \
    -- \
    uv run pytest --benchmark-only tests/test_sum_benchmark.py \
    ::: \
    uv run pytest --benchmark-only tests/test_sum_benchmark.py

uv run benchmatrix compare paired-runs --paired --precision-target 2%

The equivalent Python API is:

from benchmatrix import collect_paired_benchmark_runs

pytest_command = (
    "uv",
    "run",
    "pytest",
    "--benchmark-only",
    "tests/test_sum_benchmark.py",
)
paired = collect_paired_benchmark_runs(
    pytest_command,
    pytest_command,
    "paired-runs",
    random_seed=20260801,
    baseline_cwd="../project-baseline",
    candidate_cwd=".",
)
comparison = paired.compare()

A pair contributes to inference only when both adjacent commands succeed in the same block attempt. Resume and retry preserve every earlier record but replace an incomplete block with a fresh two-command attempt. Use collect-paired --resume after an interruption and collect-paired --retry-failed for one new full-block attempt per incomplete pair. Ordinary measure, collect, and two-source compare workflows remain independent; compare PAIRED_DIR --paired is the explicit paired path. When --pairs/pair_count is omitted, benchmatrix learns the matrix from the first accepted command and chooses the smallest target of at least five pairs that completes the joint AB/BA-by-cell-order supercycle. An explicit target is useful for exploratory pilots, but manifest-backed formal comparison requires a complete joint supercycle.

Metrics

benchmatrix supports three ways to measure each benchmark:

Metric Meaning Better result
Single-call latency Time required to complete one function call Lower
Batch throughput Number of declared work units completed per second Higher
Tail latency The slower end of the timing distribution (e.g., 95th percentile) Lower

Comparison policy

Keep project-wide comparison rules in pyproject.toml:

[tool.benchmatrix.evidence]
minimum_runs = 5

[tool.benchmatrix.inference]
method = "bca_bootstrap"
confidence_level = 0.95
resamples = 50000
random_seed = 0
multiplicity = "bonferroni"

[tool.benchmatrix.regression]
default_threshold_percent = 5.0

[tool.benchmatrix.regression.by_metric]
tail_latency = 8.0

Thresholds can also target an implementation, case, or exact matrix cell. A CLI option overrides only the corresponding setting; it does not discard the more specific rules.

Inspect or validate the effective policy without running a benchmark:

uv run benchmatrix policy show
uv run benchmatrix policy validate --quiet

See Configuration and automation for the full schema and precedence rules.

Reports and CI

benchmatrix's text output is intended for human readers. Complementary machine- readable, versioned JSON reports can be generated and loaded later programmatically:

uv run benchmatrix compare baseline candidate --format json > comparison.json
from benchmatrix import load_comparison_report

report = load_comparison_report("comparison.json")
print(report.schema_version, report.passed, len(report.regressed))

The same decision can be rendered as Markdown or appended to a GitHub Actions step summary:

uv run benchmatrix compare baseline candidate --format markdown
uv run benchmatrix compare baseline candidate --github-summary

Comparison checks

Before classifying a change, benchmatrix checks that both sides contain compatible environments, matching matrix cells, and five independent process runs by default. Raw pytest-benchmark rounds are nested observations used for each run's statistic and diagnostics; they are not counted as independent replicates of a code change.

The formal estimand is the direction-aware percentage ratio between the median per-run statistic on each side. benchmatrix resamples complete process-run statistics, calculates a deterministic BCa bootstrap interval, and falls back to a clearly reported percentile-bootstrap interval when the BCa adjustment is degenerate. Bonferroni adjustment controls the family-wise error rate across the structurally comparable cells in the matrix by default.

Independent comparisons resample each side separately. Explicit paired comparisons resample matched baseline/candidate tuples, preserving within-pair dependence while leaving the estimand unchanged. Pairing is never guessed from filenames, timestamps, or collection proximity. Manifest-backed paired comparisons stratify resampling by the recorded AB/BA orientation, so every bootstrap sample preserves the fixed orientation counts. The bootstrap does not force each resample to preserve its exact matrix-order-row composition; the collected supercycle crosses every row with both orientations before formal comparison is allowed.

An optional PrecisionPolicy uses paired pilot log-ratio variability to estimate the pair count for a fresh, fixed-size future collection at a requested multiplicative log-ratio width proxy. This planning approximation is not power analysis or a sequential stopping rule. It applies Student-t scaling to a within-orientation mean signed paired-log-ratio proxy; that proxy is not the formal ratio-of-marginal-medians estimand used by paired BCa inference, so the planned count does not guarantee the requested BCa interval width. Plans never recommend fewer pairs than the active evidence policy permits and round up to the paired collection's complete design supercycle. Their additional_pairs value is only the arithmetic difference from the pilot size; it is not permission to append runs to the pilot and reuse those outcomes as confirmatory evidence.

A cell is regressed or improved only when its complete adjusted interval is beyond the configured practical threshold. It is unchanged only when the complete interval is inside the practical-equivalence region. An interval that crosses either boundary is inconclusive; failure to prove a regression is not treated as proof of equivalence.

Evidence output includes per-run rounds, iterations, observation counts, IQR, coefficient of variation, and outlier diagnostics. Tail-latency inference requires 100 round-duration observations and one target iteration per round by default.

A single run remains useful for descriptive comparison, but cannot produce the default run-level interval and is therefore inconclusive. The explicit legacy_consistency inference method preserves the earlier observed pairwise range rule for migration and exploratory use; it is non-inferential.

benchmatrix's positioning

When using benchmatrix, pytest-benchmark still handles timing, calibration, statistics, terminal output, and JSON export. benchmatrix adds the matrix, collection, and comparison layer.

If you only need to time one function or inspect one run, pytest-benchmark may already be enough. benchmatrix is useful when you repeatedly compare the same operation across implementations and inputs, need auditable regression rules, or want portable reports without adopting a hosted benchmarking service.

benchmatrix supports synchronous Python callables; it is not a load-testing tool or a production latency monitor.

Contributing and community

Questions, bug reports, and feature ideas are welcome in GitHub Issues. See the contributing guide to make a change, and use private vulnerability reporting for security concerns. Published changes are listed in the release history.

License

benchmatrix is distributed under the MIT License.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

benchmatrix-1.2.1.tar.gz (648.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

benchmatrix-1.2.1-py3-none-any.whl (116.3 kB view details)

Uploaded Python 3

File details

Details for the file benchmatrix-1.2.1.tar.gz.

File metadata

  • Download URL: benchmatrix-1.2.1.tar.gz
  • Upload date:
  • Size: 648.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for benchmatrix-1.2.1.tar.gz
Algorithm Hash digest
SHA256 1bfa6805ef56e054d9911f3c2772ba05e6e5d5f93d3d8086b1f10e551e15aedc
MD5 32756a22ca4cf006138da5bd242ecd39
BLAKE2b-256 33e038e4f458429dfe9a60eef58ca7f2e5ff11b33e02eb265ff09158af12d721

See more details on using hashes here.

Provenance

The following attestation bundles were made for benchmatrix-1.2.1.tar.gz:

Publisher: release.yml on ryancswallace/benchmatrix

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file benchmatrix-1.2.1-py3-none-any.whl.

File metadata

  • Download URL: benchmatrix-1.2.1-py3-none-any.whl
  • Upload date:
  • Size: 116.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for benchmatrix-1.2.1-py3-none-any.whl
Algorithm Hash digest
SHA256 aa61517007e9c1df7f7db3480a1fd63d6f6fabd387c9e521438a76621c298f92
MD5 bd6e305aa37f9309f80261c8ee7bfb77
BLAKE2b-256 cd116feef582b46f942dd46e41d465139f134829a87727e682b0e402447439b7

See more details on using hashes here.

Provenance

The following attestation bundles were made for benchmatrix-1.2.1-py3-none-any.whl:

Publisher: release.yml on ryancswallace/benchmatrix

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

1.2.1 This release

2 files

1.0.0

2 files

0.3.0

2 files

0.2.4

2 files

0.2.3

2 files

0.2.1

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page