Performance model¶
benchmatrix reports three views over timings produced by pytest-benchmark. The views share the same measurement engine but answer different questions.
Single-call latency¶
Single-call latency represents one synchronous target invocation. pytest-benchmark may run calibrated loops internally, but the reported value is normalized to the target call. Input construction is excluded unless it happens inside the target function.
Batch throughput¶
Batch throughput derives logical work per second from the same target invocation.
Use work_units when one call completes multiple comparable units of work, such
as 100 items processed by one function call.
work_units must describe completed work for one target call and must be
comparable across implementations. Incorrect counts produce precise-looking but
wrong throughput values.
Tail latency¶
Tail latency summarizes local pytest-benchmark timing samples. It derives p50/p90/p95/p99-style summaries from saved samples so implementations can be compared by local timing distribution.
Keep pedantic_iterations=1 for tail-latency comparisons. Higher values turn
each sample into a per-round average of multiple calls. That averaging hides
variation between the individual calls in a round.
The default evidence policy requires at least 100 round-duration observations per run and exactly one target iteration per round for tail-latency inference. With 100 observations, only about five observations are expected beyond the population p95. Tail estimates therefore need more care than means even when the process-run count is adequate.
Statistical comparison model¶
benchmatrix treats each separately launched pytest process as one independent experimental unit. pytest-benchmark rounds inside that process are repeated measurements under shared process state, not independent replicates of the code change. Collecting more rounds can improve a run's mean or p95 estimate, but it does not substitute for collecting more process runs.
For each cell, let b be the median of the baseline per-run statistics and c
the median of the candidate per-run statistics. The reported direction-aware
improvement estimand is:
higher is better: 100 * (c / b - 1)
lower is better: 100 * (1 - c / b)
Positive values always mean improvement. The underlying per-run statistic is
mean latency for single_call_latency, mean throughput for
batch_throughput, and p95 latency for tail_latency.
The default bca_bootstrap method resamples complete runs with replacement,
recalculates the estimand 50,000 times, and forms a bias-corrected and
accelerated (BCa) interval. Independent comparisons resample the baseline and
candidate groups separately. Explicit paired comparisons resample complete
matched (baseline, candidate) tuples, so shared block-level noise remains in
the analysis. Pairing changes the resampling design, not the estimand above.
The policy seed and matrix-cell identity derive a stable per-cell seed, so results do not depend on matrix ordering. Paired tuples are sorted within their declared strata before seeded resampling, which makes the result insensitive to the order in which complete pairs were supplied without breaking their matches. Pairing is never inferred from filenames, timestamps, or nearby collection times. A manifest-backed comparison supplies the recorded AB/BA orientation as a fixed stratum: resampling preserves each orientation's count, and the stratified delete-one jackknife supplies the BCa acceleration. A low-level paired comparison without strata remains available, but its inference contains an explicit exchangeability warning.
The paired bootstrap does not force every resample to retain the exact matrix-order-row composition. Instead, collection uses a joint supercycle in which each balanced-order row occurs once under AB and once under BA before manifest-backed formal inference is allowed. Schedule-row effects can still contribute pair-to-pair variation, making the interval conservative when that variation is material.
If the delete-one jackknife needed for BCa is degenerate, benchmatrix reports a
percentile_bootstrap fallback using the same run-level bootstrap samples. The
bias correction uses a midrank convention for bootstrap estimates tied with
the observed estimate. Independent acceleration centers and scales delete-one
values separately for the two groups, including unequal group sizes; paired
acceleration deletes one complete pair at a time.
The default family confidence is 95%. For m structurally comparable cells,
Bonferroni multiplicity uses a per-cell confidence level of:
1 - (1 - 0.95) / m
The family contains environment-compatible cells with matching measurement context and finite positive run statistics on both sides. It is defined before evidence outcomes are classified, so excluding a noisy result cannot make the remaining intervals artificially narrower. Missing or structurally incompatible cells are not hypotheses in the family and remain explicit non-comparable results.
Practical decisions¶
The regression threshold is a practical-effect boundary, not a significance
level. With threshold d and adjusted confidence interval [L, U]:
| Interval condition | Classification |
|---|---|
U < -d |
regressed |
L > +d |
improved |
L >= -d and U <= +d |
unchanged (practically equivalent) |
| Any other placement | inconclusive |
This rule deliberately distinguishes equivalence from absence of evidence. A
point estimate inside the practical region is not enough for unchanged; the
complete interval must fit inside it. Likewise, a point estimate beyond the
threshold is inconclusive when its interval crosses a boundary.
Selecting multiplicity = "none" keeps the configured confidence level for
each cell but provides no matrix-wide error control. Reports label that mode as
exploratory. Selecting method = "legacy_consistency" restores the earlier
observed Cartesian pairwise-range rule. That method calculates no formal
confidence interval and must not be interpreted as statistical inference.
Paired AB/BA design and balanced cell order¶
collect_paired_benchmark_runs collects each target pair as one adjacent
two-command block. Blocks alternate between baseline-first (AB) and
candidate-first (BA); the configured seed chooses the first orientation.
Baseline and candidate may use different working directories and commits, but
their matrices and environments must remain compatible.
Both members of a pair use the same deterministic Williams-style matrix-cell
order. Across a complete ordering cycle, every cell occupies each ordinal
position equally often. For odd-sized matrices larger than one, a reversed
second cycle also balances directed first-order carryover; the row-cycle length
is 2n rather than n. The collector assigns every row to two consecutive
blocks while continuing to alternate AB/BA, so every row occurs once under each
orientation. This joint supercycle has twice the row-cycle length and prevents
command orientation from being permanently confounded with matrix position.
When no pair target is supplied, the collector learns the matrix from the
first accepted command and chooses the smallest whole joint-supercycle target
that satisfies the default five-pair evidence minimum. If neither command in
the first target pair succeeds, collection stops because it cannot yet build
the row schedule; resume/retry establishes that anchor before later pairs run.
Explicit partial-cycle targets remain useful for exploratory pilots, but the
manifest-backed compare path rejects them for formal inference. This design
reduces systematic order, warm-up, and drift confounding; it cannot make an
unstable machine stable.
Pair membership is atomic. Both adjacent commands must succeed within the same
block attempt before either result enters complete_pairs or paired inference.
An interrupted block or a block with one failure remains fully auditable, but
an orphan success is excluded. Resume abandons a partially recorded block and
runs a fresh adjacent block. retry_failed=True similarly appends one fresh
two-command attempt for each still-incomplete pair; it never joins successes
from different attempts.
Fixed-design precision planning¶
Precision planning is optional and available only for explicitly paired pilot
data. For pair i, the planner forms a signed log ratio so positive means
improvement:
higher is better: log(c_i / b_i)
lower is better: log(b_i / c_i)
When AB/BA strata are available, let s be the pooled within-orientation
residual standard deviation of those log ratios; fixed command-order effects
are not counted as random pair noise. Otherwise, s is the ordinary pilot
sample standard deviation and the plan carries an exchangeability warning. The
planner uses s / sqrt(n) as the standard error of a mean signed
paired-log-ratio proxy. A requested multiplicative percentage width w
becomes h = log1p(w / 100). After applying the same Bonferroni
family-confidence adjustment as inference, the planner first finds the
unconstrained n satisfying:
t_((1 + adjusted confidence) / 2, n - strata) * s / sqrt(n) <= h
This proxy is not the ratio-of-marginal-medians estimand used by the paired BCa
comparison. Its Student-t width therefore does not estimate or guarantee the
future BCa interval width or a fixed number of percentage points around a
nonzero effect; it is a transparent heuristic for selecting a fixed design
size. The final required_pairs is at least the active evidence-policy minimum
and is rounded up to a complete paired-design multiple. The unrounded
statistical result remains available as unconstrained_required_pairs.
The calculation assumes independent pairs, stable within-stratum log-ratio variability, and a pilot representative of the future experiment. Pilot variance is plugged in rather than bounded by an assurance calculation, so small pilots remain explicitly provisional. Zero residual variability cannot support a finite planning claim.
required_pairs sizes a fresh future confirmatory collection whose total
pair count is fixed before collection starts. additional_pairs is only the
arithmetic difference between that count and the pilot size. It does not mean
that adding those runs to the observed pilot restores confirmatory coverage.
The plan is not power analysis, does not guarantee a classification, and must
not be recalculated after each result to create a sequential stopping rule.
Current limits¶
The CLI keeps the experimental design explicit: collect and measure
produce independent groups, while collect-paired produces atomic AB/BA
blocks that compare --paired preserves. The same distinction is available
through the Python APIs. benchmatrix does not estimate power, collect until a
result becomes conclusive, or provide a sequential-analysis boundary. It also
cannot remove thermal, frequency-scaling, background-load, or long-term drift
effects that remain inside a supposedly controlled block. Fix the design and
sample size before a confirmatory collection and investigate inconclusive
outcomes rather than sampling until they change category.
Guardrails¶
Do not infer service capacity, saturation throughput, production p95/p99, queueing behavior, retry behavior, or network reliability from benchmatrix output. Treat results as local comparative measurements and retain environment metadata with saved runs.
Common pitfalls:
- A target returning a generator, coroutine, future, query plan, or other lazy object may only measure object creation. Resolve lazy work inside the synchronous target wrapper.
- Fresh-input factories and copying run outside the timed target body. Put construction inside the target only when setup cost is part of the operation being measured.
- Correctness and lifecycle hooks run outside the timed target body and wrap a complete benchmark entry. They do not run once per calibrated call or pedantic round.
- Reused mutable inputs can drift across invocations. Use
fresh_inputs=True,deep_copy, or a domain-specific copier when targets mutate their inputs. - Small differences need independent runs, uncertainty intervals, and controlled environments before they become conclusions.
- A result validator prevents invalid matrix entries from passing silently, but a faster implementation still needs ordinary correctness tests.