BenchX
BenchX is a powerful and flexible benchmarking framework which aims to address multiple usage scenarios, from running one-off in-development benchmarks to tracking performance over history of large projects.
The project is grounded in user stories
collected from experience of multiple projects. We aim to address various pain points
we faced and worked around while working on Apache Arrow, NumPy, SciPy, Pandas
and related projects, so that benchx becomes the go-to solution for performance benchmarking
across the community.
We welcome contributions of all sorts: add a user story, tell us about your use case, try
benchx on your workflow/project, open an issue if you found a rough edge,
implement a missing feature.
- Documentation: https://benchx-dev.github.io/benchx/
- Issue tracker: https://github.com/benchx-dev/benchx/issues/
- A collection of worked examples: https://github.com/benchx-dev/benchx_examples
CLI API
At a glance:
# Execute benchmarks:
bx bench TARGET --suite NAME [CASE …] # the single-target form of bx compare: one order, a table
[--adapter …] [--filter REGEX] [--quantity …] [-e NAME=VALUE ...]
[--processes N] [--values N] [--min-time S | --loops N] [--warmups N] [--repetitions N]
[--project P] [--run-key KEY] [--out DIR] [--no-build] [--source-uri URI]
[--label NAME=VALUE ... --round N --slot K] # one side of a session other calls join
[--case-timeout S] [--no-ingest]
bx bench MODULE.py [FUNCTION …] [--tree DIR] [...]
# shorthand: Python functions under pyperf → results → store → table
bx bench PATH --adapter asv [CASE …] [--filter REGEX] [--tree DIR] [...]
# shorthand: an asv benchmark suite, timed by pyperf
# Comparisons:
# measure mode: run both sides, then compare
bx compare BASELINE CONTENDER --profile revisions|environments
[--adapter pyperf|google-benchmark] # default pyperf
--suite NAME [--filter REGEX] [--quantity wall-time]
--rounds R [-e NAME=VALUE ...] [--repetitions N] [--min-time S]
[--project P] [--label NAME=BASE,CONTENDER]
[--run-key KEY] [--out DIR] [--no-build] [--k 3] [--json]
# read mode: compare results already measured
bx compare --run KEY --profile revisions|environments --baseline VALUE
[--label NAME] [--results DIR] [--k 3] [--json]
# low-level command: run a handwritten work oreder
bx run <order.json> [--out DIR] [--no-ingest]
# runner + Google Benchmark adapter → result files → local store
bx ingest <dir|file> # result files and work orders → the local store
bx series [--workload … --quantity …] # list series
bx history <series> # one series' points (a query); dirty ones are marked
bx head [-n 10] [--project P] [--workload W] [--json|--table] # the latest results, newest first; dirty ones are marked
bx target describe|prepare TARGET # a target's description (a provider's answer, or a build directory's sidecar)
bx validate <file>… [--kind KIND] # every problem in each document, by schema and rules
bx config init [--dir DIR] [--adapter …] # write a benchx.conf.json stating a harness's defaults
bx config show # what benchx.conf.json and the defaults give, and where each value comes from
Technical details
One project file. benchx.conf.json in the project root (found by walking up from the
working directory; its directory is the root) says how the project is measured: the
provider, the project name and source, an adapter block (name, suite,
quantities, precision), environment_variables, timeouts, paths for the store
and the results (by default store.parquet and results/ in the root), and the
comparator's compare policy. Every value is the command line's, else the file's, else a
built-in default; a required one (adapter.name, adapter.suite, the target, and for
bx compare the profile and --rounds) that has none is a hard failure that names the
flag and the key. --no-project makes one run ad hoc whatever the file's project says.
The schema is schemas/config/0.1.0, whose examples/ hold recommended settings for
pyperf and for Google Benchmark. The store and the results directory are left out of a
measured tree's identity when they sit inside it. .benchx/setup.json (declared
environment facts) is a separate file, written by whoever prepares an environment.
Each target is BUILD_DIR[:SOURCE_DIR] or @CONFIG[=SOURCE_REF]. Two targets
select measure mode and --run selects read mode; mixing them is an error.
@CONFIG asks the project's target provider, the command named by provider.command
in benchx.conf.json (it runs with the project root as its working directory). The provider speaks the contract of
system-decomposition.md §3.4: a JSON request on stdin, a target description
(schemas/target-description/0.1.0) on stdout, build output on stderr, and a
nonzero exit for failure. prepare, the default, may build; describe
(--no-build) only reports what exists. A build directory may also carry its own
description as .benchx-target.json. A description names the target and may
declare how_built and activation text, which are copied into the work order
and into every result's provenance.info, shown in the comparison, and never
executed or checked: you activate the environment yourself, and the runner
records the allowlisted variables it inherited beside your declaration.
examples/arrow/archery-local in benchx_examples
is a complete walk-through.
bx bench is the single-target form of bx compare: the same way of naming a target
(@CONFIG[=SOURCE_REF], or a path), the same options, the same code; it writes one
order, runs it, and prints a table, where bx compare writes many and compares. A bare
path is read by the harness: a build directory for Google Benchmark
(bx bench build/ --adapter google-benchmark --suite demo-bench --filter BM_Quiet), a
working tree for pyperf, the default
(bx bench . --suite benchmarks/mod.py bench_a; uncommitted changes included, recorded
as dirty). Cases are the harness's defaults (pyperf: each bench_* or benchmark_*
function), the exact names given, or a --filter regex. A flag the chosen harness does
not take is refused. -e sets environment variables for the workers and lands in the
workload's parameters. Precision resolves as flags, then adapter.precision in
benchx.conf.json (bx config init writes a template), then the harness's defaults, and the
resolved values are written into the order. --quantity takes a comma-separated list,
and one run collects them all. The workers run the python first on PATH, which must have
pyperf.
bx bench MODULE.py [FUNCTION …] is the shorthand for the quickest case, timing a few
functions: the module's git checkout is the target, its path the suite, and the
functions are exact case names. bx run is the layer underneath: it executes a finished
order, so bx run results/KEY/orders/order.json replays any bench run.
examples/pyperf-suite in benchx_examples
is a runnable walk-through.
Environments that cannot share a process, such as two venvs with different numpy versions, are
measured by separate bx bench calls that share a --run-key; --label NAME=VALUE, --round N
and --slot K place each call in the session, and bx compare --run KEY --profile environments --label NAME --baseline VALUE compares the sides (UC-04).
examples/numpy-venvs in benchx_examples
is the worked example, with a shell script that drives it.
bx bench PATH --adapter asv runs an asv benchmark suite
unchanged, reading asv's benchmark format as a language and timing it with pyperf;
neither asv nor asv_runner is used. PATH is a directory holding asv.conf.json (its
benchmark_dir is the suite) or the benchmark directory itself, and its checkout is
the target (bx bench . --adapter asv --suite benchmarks is the same order).
--filter is asv's --bench: searched in the name, and in name(p0, p1) for each
parameter combination; a CASE is an exact name, name(p0, p1) for a combination. adapters/asv_runtime.py discovers
the benchmarks as asv does (names, params, setup/teardown/setup_cache, skips,
and asv's version hash all agree with asv_runner's; a test checks it). The
benchmarks run in the python first on PATH, which must import pyperf and the
project you are measuring. Each time_* benchmark and each of its parameter
combinations is a case, with the parameters in workload.parameters as asv's
repr() strings; other benchmark types, and modules that fail to import, are
listed and skipped. Each case is one pyperf run from a scratch directory (an unbuilt
checkout of the project cannot shadow the installed one): the manager runs
setup_cache once, and each worker runs the setups, then pyperf's warmups and
values. With setup hooks (a benchmark's own setup or a module-level one), every
timed call sees fresh state by default, as in asv_runner >= 0.3.0 (asv#966,
asv_runner#53): one call per loop, or setup re-run between calls outside the
timer. For a fast benchmark that timing is mostly timer overhead, and the result
is flagged setup-per-call-unbatched. A benchmark whose calls leave the set-up
state intact can declare setup_scope = "sample" (on the function, class or
module, as proposed in asv#1613): setup then runs once per value and pyperf
calibrates loops as usual. The flags are pyperf's
(asv's rounds are --processes, repeat --values, number --loops, sample_time
--min-time, and --warmups); what they leave out comes from each benchmark's own
repeat, number, sample_time, warmup_time and timeout, then asv's defaults
(2 processes), and each result records its own case's protocol. A case's
--case-timeout covers all its rounds (default 1800 s). The harness in
every result is pyperf. Results also record the installed project's version
(provenance.info.asv.installed) and asv's hash of the benchmark's code
(protocol.asv.benchmark_version), so editing a benchmark starts a new series.
What it does
- Work orders are the
docs/design/runner-schema.mdschema (schemas/work-order/0.1.0in the repository), extended by one optional field the schema does not itself define,slot(roundis native). One order is one side of one round; measure mode alternates them. - The runner (
bx run) validates the order, refuses what it cannot apply exactly, runs each planned case in its own process, writes one result file per attempt × quantity, plus aload-averagecondition measurement per attempt (schema §4.4), and delivers those files to the local store (--no-ingestto skip). Each result is valid against the result schema onmain(schemas/measurement-result/0.1.0). It records the zero-configuration snapshot (machine/v1identity, and underenvironment.systemthe architecture, OS, kernel, C library, governor, and the allowlisted thread and loader variables, extensible with$BENCHX_ENV_ALLOWLIST), the source's revision, dirty state, and tree id, the build configuration fromCMakeCache.txt, and declared facts from.benchx/setup.json. It changes nothing on the machine and builds nothing, and it refuses an order that names a deferred field (environment_policy, a declaredbuild,components,schedule) instead of ignoring it. - The store is one Parquet file, by default
store.parquetin the project root (paths.storeinbenchx.conf.jsonmoves it). It reads documents strictly, validates them, and applies the schema §5.4 contract: idempotency and the rejection codesmalformed,idempotency-conflict,unit-conflict,identity-violation, andattempt-conflict. Each result is one row: the columns #30 derives from the schema, the store's derived columns, the canonical document, and the work order it references. Entities and series are computed from the rows on query, under the identity policy inbenchx/identity.py: the default ofschemas/identity-policy/0.1.0with the benchmark's revision ignored. Every accepting ingest rewrites the file atomically. - The session loop (
benchx/session.py,bx comparein measure mode) is the workbench's part: it checks both targets (and, forrevisions, that both trees have a working-tree id, before running anything; it says which sides are not clean), writes one order per side and round, alternates them over the rounds, delivers every result to the local store, and hands the run to the comparator. It builds nothing: both build directories must already exist. - The comparator (
bx compare --run) runs in run mode for therevisionsandenvironmentsprofiles, each a projection built in code from the identity policy (benchx/compare.py): the axis tells the sides apart, everything the policy matches must agree, and it checks the profile's invariants (revisionsalso requires one tree id per side and, for a benchmark recorded apart from the subject, one benchmark file throughout, UC-03 §10; a dirty side makes any profile's comparison local-only), pairs the sides by round, and appliesbenchx/paired-relative/v1(effect = mean relative paired difference, noise = its standard error, change when |effect| > k × noise).environmentsnames its sides by the label--labelgives.--resultscompares result files through a throwaway store: the ad hoc path. Condition measurements (measurement.kind: condition) are not compared, and deterministic quantities are deferred.
Install
$ pip install benchx
Run it
$ uv venv && uv pip install -e '.[test]' # editable; a plain `uv pip install .` works too, the schemas ship in the package
$ .venv/bin/python -m pytest tests # 344 tests, fake Google Benchmark binary, no compiler
A regular (non-editable) install ships the test suite too, so an installed copy can test itself:
pip install 'benchx[test]' followed by python -m pytest --pyargs benchx.
The end-to-end examples that build real software are in
benchx_examples, and the e2e
workflow runs them. examples/arrow/demo/run.sh there is a walkthrough of both comparison
profiles on a small Google Benchmark suite, with real CMake builds (it needs cmake, a
C++ compiler, and network access once). It prints every command before running it,
as you would type it, so it doubles as a walkthrough.
Metadata
Release files for benchx 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| benchx-0.1.0.tar.gz | 355.8 kB | With provenance | Details for benchx-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| benchx-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for benchx-0.1.0-py3-none-any.whl |
Total release size: 597.1 kB
Release files / benchx-0.1.0.tar.gz
| Download URL | benchx-0.1.0.tar.gz |
|---|---|
| Size | 355.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
40815f23c97d6ac2cf31be3d3a568e9a04bbe6a56d82d1ed746c2a47ab38576c
|
|
BLAKE2b-256 checksum How to use checksums |
0437ae8aded0352172f92e4456c5ba1254e60231199873177c93d11243fbc7e5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 6, 2026.
Transparency logRelease files / benchx-0.1.0-py3-none-any.whl
| Download URL | benchx-0.1.0-py3-none-any.whl |
|---|---|
| Size | 241.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
edc96e24ddf651e4e28b02eff6d061df604b2d4fd083fd7d97152aef20cf7824
|
|
BLAKE2b-256 checksum How to use checksums |
88a80b543d866f6de6de962d72326a65e8167183a0ea40c4c9d838e0d765326b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 6, 2026.
Transparency logSecurity for benchx 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
Both files in this release have been uploaded with provenance metadata.
Attestation for benchx-0.1.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 6, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.