Benchmark Measurement Result Schema and Migration Contract¶
Status: Minimal draft incorporating source, metrology, and design-partner review
Author: Rok Mihevc
Review date: 2026-09-11
1. Purpose¶
This document defines the minimum model needed to ingest, compare, and migrate computing benchmark results without inventing statistical meaning. Detection, alerts, thresholds, scheduling, run lifecycle, and environment setup are out of scope.
A measurement result records the evidence from one measurement attempt for one quantity and one workload variant, together with its conditions and provenance. It holds individual repeated measurements as observations when the producer provides them; an aggregate-only result is valid.
One attempt may report several quantities. CPU time, GPU time, peak memory, and a correctness metric are separate scalar results with separate units, joined by a shared attempt_key. A retry is a new attempt with a new key.
Reported coordinates are the facts a producer reports about what was measured and under which conditions: project, designated source, benchmark identity, workload variant, subject, protocol, environment, and optional resource selection. They exclude the designated source revision, which is the history axis, the quantity, which names the measurement, and all provenance such as attempt/run keys and timestamps.
Comparison identity is the part of the reported coordinates that the project's identity policy requires to match before two estimates are points in the same history. Producers report coordinates; they never declare identity or series membership.
A series is one comparison identity for one quantity under one estimator, followed across revisions of the designated subject source:
policy(reported coordinates) × quantity × estimator
The estimator is chosen separately: one observation batch feeds a mean series and a median series without being stored twice (§4.5). The server fingerprints reported coordinates for audit, applies a versioned identity policy, and materializes series as rebuildable indexes (§4.3). The identity policy is one projection: other projections, such as a comparison profile or a sweep across kernel versions, follow a different axis over the same results (§4.3).
Two sources appear on every result. The top-level source/revision identify the subject component whose revisions form the history axis. benchmark identifies the code that defined and executed the workload; its exact revision is a reported coordinate that policy normally maps to a declared benchmark version or an audited continuity mapping rather than splitting history on every benchmark commit. A composite subject such as Arrow–NumPy interoperability names every component, designates one as the axis, and pins the others (§4.2).
A result records two kinds of fact (§4.2). Coordinates are fixed for the attempt: which code, build, hardware, kernel, C library, runtime, timer, and calibration rule produced the number. Measurements are what the attempt produced or passed through: the measurand, and conditions such as temperature, load, or clock frequency, each a sibling result for its own quantity sharing the attempt_key (§4.4). Like the estimator, the quantity is never a coordinate: it says what the number is, not what produced it. Whether a coordinate splits history is decided by the identity policy, not by where it is stored: a Parquet-read benchmark may let kernel and glibc vary, while a benchmark of malloc throughput must match the glibc version because it is the thing under test, and both record glibc in the same place.
For example, one series could track:
- project: Apache Arrow benchmarks; subject/source: Apache Arrow from
https://github.com/apache/arrow; - workload variant: Parquet read with Snappy compression;
- quantity: wall time in seconds;
- estimator: mean;
- subject configuration: release build with Clang 18;
- benchmark identity: the suite source/revision;
- protocol: protocol version and the warmup/calibration strategy;
- environment: runner
bench-01with a specified CPU and core policy, running Python 3.12; resource selection: absent, or a GPU UUID/device index for a per-device result.
Each subject revision contributes zero or more results, producing the history shown in a benchmark chart. Under this policy, changing a workload parameter, quantity, protocol value, or environment identity yields a different series; a different estimator yields a different series over the same coordinates; changing only the attempt/run key or timestamp never does, and changing the kernel or glibc version does not under this example's policy, which ignores them rather than treating them as a property of those components.
The common operations are to find a series, order its results by revision, plot estimates, inspect observation batches, group an attempt's quantities, and trace a result to its producer. The indexed core is therefore scalar; richer data remains representable without first-class tables.
2. Vocabulary¶
| Term | Meaning here |
|---|---|
| Run | Orchestration-level grouping created by a scheduler, CLI, CI job, or similar work-order author. |
| Attempt | One execution request and outcome for one workload variant; it may complete, fail, or be skipped. A retry is a new attempt. |
| Workload | Named benchmark operation before its parameter domain is expanded. |
| Parameter assignment | One concrete value for each parameter of a workload, such as {"size": 100} from a domain of size = [10, 100, 1000]. |
| Workload variant | A workload plus one parameter assignment. |
| Protocol | How the subject is exercised and measured: protocol version, timer, warmup, calibration, and similar requested settings. A coordinate (§4.2); what the harness then did is procedure. |
| Subject | Implementation or system under test. It may contain several named components; one source is the revision axis. |
| Quantity | Named scalar output with a canonical unit, such as wall-time in s or peak-rss in By. An open, project-extensible vocabulary (§4.4). Canonical units use UCUM when representable. Part of the measurement, not a coordinate. |
| Estimator | Rule producing a scalar estimate, such as mean, median, minimum, slope, a percentile, a function of several input quantities, or a source-defined value. A series dimension chosen by project configuration, separately from the projection; reported only inside estimate summaries, never as a coordinate. |
| Reported coordinates | Producer-reported facts describing what was measured and under which conditions, fingerprinted before any policy is applied. |
| Condition measurement | A result for a state the system passes through during an attempt, such as temperature or load, sharing the measurand's attempt_key and marked measurement.kind: condition; an influence quantity, in metrology terms. Never a coordinate. |
| Projection | A function from reported coordinates to series: it names the axis a series follows, selects the subset that must match, and normalizes their values, for example canonical source URIs or a version mapped to a declared line (§4.3). The identity policy is a project's default projection, with the subject revision as its axis; other projections reuse its roles with a different axis. |
| Role | What a projection does with a fact: match, normalize, or ignore (§4.3). Not to be confused with a subject component's role (primary or a pinned name), a compound estimator's input roles (§4.5), or the conventional role workload parameter (§5.5). |
| Comparison identity | The part of the reported coordinates a projection requires to match. |
| Series | Derived comparison-time construct: one comparison identity × one quantity × one estimator under one projection. It exists from its first point; a comparison between points is a separate document (§5.5). Materialized as an index; never declared by producers. |
| Observation | One measured value produced by a repetition after declared normalization. |
| Observation batch | Observations of one quantity from one attempt. Three repetitions producing 1.0 ms, 1.1 ms, and 1.0 ms are three observations in one batch. |
| Estimate | Scalar produced by applying an estimator to an observation batch, used for plots and analysis; not a known “true value.” Producer estimates are typed summaries on the immutable result; derived estimates are series points. |
| Statistic | Numeric summary computed from observations, such as a mean, median, percentile, or standard deviation. |
| Precision statistic | Statistic describing observation dispersion, such as standard deviation, MAD, or IQR. |
| Measurement uncertainty | Uncertainty associated with an estimate. It is not synonymous with error or observation spread. |
| Environment | Host or allocation shared by an attempt's results, especially runner and hardware identity, plus the system it ran: kernel, C library, runtime. |
In metrology, the quantity intended for a workload variant at a revision is the measurand; it needs no database entity here. Use accuracy, precision, error, bias, confidence interval, and coverage interval only with their defined meanings, and preserve source terminology when its exact meaning is unknown.
3. Design rules¶
- Producers report facts; policy derives identity. A result carries reported coordinates, never a comparison identity or series identifier (§4.3).
- The designated subject revision is the identity policy's axis. Ingest time is not historical order; dirty working-tree state is a per-result flag, never a revision property (§4.1).
- One result contains one quantity and unit. A multi-output attempt emits several results joined by
attempt_key; tuple outputs are split into named scalar quantities. - Placement never decides identity. A fact is a coordinate or a measurement by what it is; which coordinates split history is the identity policy's choice, and changing it is a new identity policy, never a change to results or mappings (§4.2, §4.3).
- Environment and selected resource are explicit. Environment identifies the shared host; optional resource selection identifies the CPU set, GPU, or partition one result applies to.
- Observations are preferred, not required. Aggregate-only history remains valid and is marked producer-supplied.
- Summaries are typed. Estimates, statistics, uncertainty, intervals, and opaque source bounds remain distinct entries (§5.3).
- Canonical units are interoperable. Use UCUM case-sensitive units when representable, normalize values before storage, and keep the original unit in provenance.
- Ingestion is idempotent and auditable. Producer identity is stable and namespaced; its software version is not part of the idempotency key (§5.4).
- Grouping keys are provenance. The run author defines
run_key; the executor defines a namespace-qualifiedattempt_key; neither enters identity. - Decision policy is separate. Direction, thresholds, guard bands, correctness verdicts, and alerts never enter identity or imply a verdict.
- The quantity vocabulary is open. Adding a quantity is configuration, not a schema change; a quantity's meaning and unit are immutable once used (§4.4).
- Results record what was, not what was asked. A coordinate is the value read from the attempt; the request lives in the work order, which the result cites (§4.2).
4. Minimal schema¶
4.1 Core tables¶
| Table | One row represents | Key or invariant |
|---|---|---|
project |
namespace for project-defined names and policy | unique project key |
source |
versioned code or data source, usually a repository | unique (project_id, canonical_uri) |
workload_variant |
workload plus one resolved parameter assignment | unique (project_id, name, parameters) |
quantity |
named scalar output with canonical unit (UCUM unit when representable) and descriptive metadata (§4.4) | unique (project_id, name); immutable after use |
environment |
host/allocation identity | unique (identity_schema, fingerprint) |
revision |
source revision plus the optional native_order and native_time reported with it |
unique (source_id, revision_key) |
series |
derived index: one comparison identity × one quantity × one estimator under one projection | unique series_fingerprint; rebuildable |
series_point |
one entry of a series: the estimate one result contributes, recording which result and which series | unique (series_fingerprint, producer, ingest_key); rebuildable |
benchmark_result |
immutable producer record for one attempt × one quantity | unique (producer, ingest_key); indexed by reported_coordinates_fingerprint |
workload_variant.parameters is a canonical JSON object holding one parameter assignment. A workload declared with size=[10, 100, 1000] yields three variants; a parameter value may itself be an array, such as a matrix shape, and the benchmark definition resolves the distinction before ingestion. A workload may also carry a dataset object with name, version, checksum, and generation parameters; it is part of the variant identity, so two runs on different data never share a variant. A baseline-versus-variant study (UC-02) is two variants that differ in a role parameter, not a separate field.
source and revision serve both subject and benchmark code. A revision key has no intrinsic order, and the revision graph is not a core table: history queries take ancestry from the source repository or from an auxiliary revision catalog keyed by (source_id, revision_key). When neither is available, ordering falls back to the native_order or native_time a producer reported with the revision, as migrated LNT or rustc-perf results do; that yields a linear order but not ancestry, so ancestor-based baselines still need a graph. Without any of these, results stay queryable but revision order is unavailable. Parent hashes are not copied into results.
A revision row is keyed by the clean VCS identifier. Whether the measured checkout had uncommitted changes is a fact about the result: subject_dirty and benchmark_dirty are required tri-state flags, clean, dirty, or unknown, in provenance. A clean and a dirty result at the same commit therefore share a revision row. unknown is distinct from clean so that a comparator can tell what is known to be clean. The flag, the working-tree id below, and a quality warning exist so that a detector or comparator can filter or annotate dirty results trivially; what it does with them is its own policy. The runner adds the quality warning dirty-subject-tree or dirty-benchmark-tree to a result whose matching flag is dirty. The failure mode to avoid is the one asv has, where uncommitted changes are silently ignored and a result is charted as if it were the commit. Comparators may compare dirty sides within one run, as a contributor does with an uncommitted change against HEAD; such a comparison is local-only and never promotes into tracked history (§5.5).
Working-tree identity. A result also records subject_tree and benchmark_tree: the content hash of the checkout that was measured, independent of its commit. For git this is the tree object id of the working tree, the same id a commit made from this checkout on this machine would carry. It is obtained by staging everything into a scratch index that does not yet exist, so the repository's own index is untouched, and writing a tree:
index=$(mktemp); rm -f "$index"
GIT_INDEX_FILE=$index sh -c 'git add -A && git write-tree'
rm -f "$index"
Tracked and untracked files count; gitignored files do not. A clean checkout yields the tree of its HEAD; a dirty one yields a tree no commit has yet. Two results with equal tree ids measured the same code whatever their commit or timestamp, so dirty runs are distinguishable from each other and from HEAD, and when that state is later committed the commit's tree equals the recorded id, so a comparator can attach the earlier results to the real revision.
Two limits follow from how git hashes a tree. Submodules and nested repositories contribute only their HEAD commit id, so their own uncommitted changes are invisible; a pinned component that lives inside the tree needs its own tree id when that matters. And what enters the hash depends on the repository's normalization settings, such as line-ending conversion, file-mode tracking, and clean filters, so ids from different producers are equal for identical code only when those settings agree. The doc does not mandate settings; equality across producers is a property of matching configuration, not of the id. The tree id identifies the code but does not reproduce it; a patch, when kept, is a provenance artifact of kind patch. Non-git sources use their native tree hash or a hash of sorted file paths and contents; unknown dirty state pairs with an absent tree id.
Benchmark code without a revision. Benchmarks are not always under version control. A suite installed into the environment being measured, such as a pip install pyperformance, has files but no revision, and a benchmark directory vendored into an image has none either. benchmark.revision is therefore optional when benchmark_tree is present: the tree, a content hash of the benchmark files, identifies the code instead, and benchmark_dirty is dirty, because an unversioned tree cannot be shown to be clean. Such a result still names its benchmark code, so it is not thin on that account; what it cannot do is take part in an identity policy that projects the exact benchmark revision onto a declared version. A policy that keys on the tree, or one that omits benchmark identity, accepts it; the choice is the project's.
Thin local results. An ad hoc run on a laptop (UC-01, one-off stories) may have no benchmark repository and no project. project and benchmark are therefore optional, and environment identity may use the local/v1 schema with only a hostname. Such a result is thin: it validates and can be compared within its own run. On ingest the store assigns it the implicit project local/<hostname> so that its vocabulary rows exist, and it joins a tracked project's series only after an approved mapping re-projects it with the missing coordinates (§6). When the logic under test lives in the benchmark scripts themselves, as in a Narwhals overhead study, the script repository is the subject: source names it, and benchmark either names the same source or is omitted.
benchmark_result stores coordinates exactly as reported plus their fingerprint, attempt_key, the outcome, observation batch, producer summaries, procedure, quality, and provenance as validated JSON. All of it is immutable after ingest. Promote a JSON structure to a child table only when a concrete query, size, or integrity requirement justifies it. No core tables exist for estimator, unit, procedure, run/attempt, observation, interval, uncertainty component, covariance, revision graph, or aliases; estimator and unit vocabularies are configuration.
series and series_point are materialized indexes. A series is the ordered list of estimates for one comparison identity and one estimator, for example the minimum wall time of a Parquet read on one runner across three revisions, [50, 10, 25]. A series point is one entry of that list, 50, and records the result it was computed from and the series it belongs to, so either can be reached from the other. The server applies the identity policy to reported coordinates, creates a series per comparison identity and estimator, and a point per eligible result. Other projections are materialized the same way on request (§4.3). Rebuilding under one projection never deletes indexes built under another. Stable annotations and external references use series_fingerprint, not a row ID.
A composite subject keeps the axis explicit and pins every non-primary component:
{
"name": "arrow-numpy-interop",
"components": [
{"role": "primary"},
{"role": "numpy", "source": "https://github.com/numpy/numpy", "revision": "v2.3.0"}
]
}
The primary component is the one whose code the benchmark run installs from the checkout under test, so its revision changes on every commit. That revision is the history axis: it is never part of comparison identity, and it is what a chart of the series follows. A pinned component is one the run installs at a fixed version; that version is a reported coordinate. In a Narwhals overhead study run from the Narwhals repository, narwhals is primary and pandas is pinned at v2.2.3, so every Narwhals commit adds a point to the series truncate × pandas v2.2.3.
A result pins the components its workload exercises and no others. The pandas overhead benchmark pins pandas, the Polars one pins Polars, and a Polars release therefore does not touch the pandas series. A new pandas release starts a new series under the default policy, because a pin is a coordinate. A project that wants pandas patch releases to share history maps them to a release line in its identity policy (§4.3); one that wants a single series across all pandas versions omits the pin from identity there and keeps it as an annotation. The pin remains a reported fact in every case, so a later policy can split what an earlier one merged, and rebuilding under a new identity schema never deletes the series built under the old one.
4.2 Field placement¶
Coordinates and measurements are told apart by one question: if this value were different, would it be a different system, or the same system in a different state? A different system makes it a coordinate; the same system in another state makes it a measurement (§4.4). A setting and the state it controls are two facts: a locked GPU clock is a coordinate, the frequency observed during the attempt a measurement.
| Destination | Put here | Examples |
|---|---|---|
| Workload-variant parameters | Resolved controlled workload/input, including dataset identity and any parameter set from outside the harness | size, compression, scenario, role, dataset name/version/checksum, OMP_NUM_THREADS |
| Subject descriptor | What is under test: components, roles, pinned non-axis revisions, build/configuration | Arrow–NumPy roles, build type, compiler/flags, JIT/AOT mode, backend |
| Benchmark identity | Exact code that defined/executed the workload | source URI, revision |
| Protocol | How the subject is exercised and measured | protocol version, warmup/GC/calibration policy, adaptive vs. pedantic timing, timer, CUDA events vs. synchronization, cache-clearing policy, interleaving schedule |
| Environment identity | Host or allocation shared by an attempt | runner, CPU model, available accelerators, memory, cluster shape |
| Environment system | The system the attempt ran on, beyond what names the host | kernel, glibc, microcode, drivers, image digest, host runtime such as Python version, the environment the measured process ran in |
| Resource selection | Optional resource one result applies to | CPU/core set, GPU UUID/device index, accelerator partition |
| Condition measurements | States the system passes through, each a sibling result sharing attempt_key (§4.4) |
load, temperature, clock frequency |
| Result procedure | Realized batch-wide acquisition details and the limits imposed | repetitions completed, inner iterations selected, warmups performed, round and slot within the run, durations, caches actually cleared, timeout imposed |
| Provenance | Audit metadata | run/attempt keys, caller labels, dirty flags and working-tree ids, CI link, logs, runner software, references to input or anchor results, patch and dependency manifest artifacts, source payload |
Three rules resolve most cases:
- Subject versus measurement. A setting that changes the artifact under test belongs in the subject descriptor; a setting that changes how it is exercised or measured belongs in the protocol; a fact about the system it ran on belongs in
environment.system. All are coordinates; the split exists for readability and adapter consistency, and a field goes in exactly one. - Intended versus realized. An adaptive timer's minimum-duration rule is protocol; the loop count it chose is procedure. A requested cache reset is protocol; whether it ran is procedure. A system fact is recorded as read from the attempt: when it cannot be read, the requested value is recorded with a warning such as
build-config-unverified, and a read value that differs from the request carriesrequested-value-mismatch. Per-observation ordinal, group, pair, and inclusion data belongs on structured observations, not in procedure. - Coordinate versus provenance. A timer, harness, probe, or data-reduction version goes in the protocol whether or not its change should split history, which is the identity policy's choice (§4.3), and in provenance only if audit-only. The test: a fact belongs in coordinates when a change in it could plausibly explain a change in the estimate, so a viewer or detector should show it beside the series and be able to group or filter by it; it belongs in provenance when it only serves to trace a result back to its run, logs, and inputs, and would be consulted once a specific result is already under investigation.
Parameters set outside the harness. A parameter need not come from the benchmark's own parameter domain. A runner may set environment variables for the measured process, or pass parameters of its own, to choose which variant is measured; these are workload-variant parameters, reported under the names used to set them. Thread control is the common case and has no single spelling (OMP_NUM_THREADS, MKL_NUM_THREADS, OPENBLAS_NUM_THREADS, and others), so the names are reported as written rather than folded into one canonical threads parameter. A variant that capped two libraries is not the variant that capped three, and reporting the names is what keeps that difference visible instead of merging two measurements into one series. This is also what lets a project compare one thread against ten within a single suite: the two are ordinary variants, as a size of 10 and a size of 1000 are. A name the harness reports for itself, such as Google Benchmark's threads:N, must not arrive twice with conflicting values; the adapter resolves that before ingest.
The environment the measured process ran in. A runner inherits the shell it was started from, so the measured process sees more than whoever ordered the run named. The environment actually passed to it is recorded per result in environment.system as process.environment, which the default identity policy matches: the variables that select the variant are already parameters, and this record exists so that a variable nobody ordered is visible after the fact and starts a new series instead of blending into one. It is the environment of the measured process, whatever process model the harness uses internally, since a harness that spawns workers must give them the same environment for the record to be true. Only allowlisted variables are recorded, as in benchmark-environments.md §3.1, because an inherited environment carries tokens and credentials in CI and a result store is shared. This applies to every artifact the result references as well: a capture of the whole environment kept as an artifact is the same exposure.
Naming the subject. subject.name is a naming decision, and it belongs to whoever ordered the run rather than to the producer. A producer that derives it from the source URI collides whenever one repository holds several subjects, since a C++ library and its Python bindings derive to the same name, and splits one subject's history across a fork, mirror, or rename. Producers are expected to be told the name; derivation is a fallback for ad hoc results.
The top-level source is the authoritative axis. The subject component with role primary may omit its source; if present it must match, and a mismatch is an identity violation (§5.4). Non-primary components carry a source URI and pinned revision as fixed coordinates.
Environment setup is out of scope. How the subject and its pinned components are obtained, built, and installed, such as a PyPI wheel versus a source build with particular flags or BLAS backend, is the job of whoever prepares the environment; the runner never builds or installs. A pinned revision identifies code, not the installed artifact, so the runner or adapter records the facts about the environment it knows to matter: in the subject descriptor when they change the artifact under test (install method, build type, compiler flags), in environment.system when they describe the system around it, and as a dependency manifest artifact in provenance when a full inventory is wanted. This document prescribes none of those facts and does not aim at reproducing an environment; it records enough to tell two environments apart.
A runner-level probe maps to a quantity plus the instrumentation used to acquire it: GPUTimeProbe might yield gpu-time via CUDA events, OSSMemoryProbe yield peak-rss via a named OS counter. Probe names are not a schema vocabulary. Protocol, environment.system, and procedure are open objects; Appendix B lists recommended keys so adapters spell the same warmup, calibration, timer, thread, or cache strategy the same way and land in the same series.
The use cases in docs/use-cases/ and their template reason in four coordinates. They map onto this schema as follows; note that the template's "environment" includes dependency versions, which are pinned subject components here, while environment in this document means the host.
| Use-case coordinate | Schema placement |
|---|---|
| Code identity | workload variant and its parameters/dataset; benchmark identity |
| Code version | subject revision, dirty flag, and working-tree id; subject configuration; pinned non-primary components |
| Environment | environment identity and system (OS, microcode); resource selection; condition measurements for load |
| Execution context | protocol; procedure (round, slot); structured observation keys; run_key and caller labels |
4.3 Fingerprints¶
The reported-coordinate fingerprint preserves the complete tuple before any policy: top-level project/source, the exact benchmark identity, and the coordinates object of §5.2. It never contains row IDs, the quantity, an estimator, the axis revision, or attempt/run keys.
reported_coordinates_fingerprint = SHA-256("benchmark-reported-coordinates-v1\0" + JCS(reported_coordinates))
A project's identity policy projects and normalizes reported coordinates into a comparison-identity object, for example by normalizing equivalent source URIs, mapping exact benchmark revisions to declared versions, or omitting a coordinate it allows to vary. It is the project's default projection: a projection names the axis a series follows and gives every other fact a role, match, normalize, or ignore. The identity policy's axis is always the designated subject revision, so its document (schemas/identity-policy/0.1.0) stores only the roles. An ignored fact never splits a series, but a change in it from one point to the next is recorded as a warning on the point, so detectors and views see it. The default (examples/default.json) matches every coordinate except a named list of environment.system fields that the former design never matched: hostname, OS and kernel, C library, CPU firmware and settings, GPU driver and ids, firmware, image digest, and cgroup limits. Any other fact matches wherever it sits, so a new or misplaced fact can over-split a series but never blend one. The policy carries an identity_schema that changes whenever projection semantics or continuity mappings change; an operational release that yields the same schema does not split history. Continuity mappings, which keep history across a benchmark rename, an added parameter, or a machine upgrade, are applied in this projection step; their shape, authoring, and audit trail are specified in a separate design document.
Any other projection, such as a comparison profile (§5.5) or a sweep across kernel versions, reuses the identity policy's roles with a different axis, is materialized on request over the same results into series of the same form, and records the ingest watermark it was built from. A materialization is never updated or rebuilt: later results, or an edited identity policy, call for a new request. Its axis may combine several facts, as in a two-factor study, and its points are ordered by the axis values in natural order: numbers by value, strings as versions read, so 6.8 precedes 6.10 and a size of 20 precedes 1000. A projection whose axis is not the code must match the subject tree unless it deliberately mixes code, as a trend over time does. Every projection has a versioned name, which includes its policy's identity_schema and takes its place in comparison_fingerprint, so series of different projections never mix.
comparison_fingerprint = SHA-256("benchmark-comparison-v1\0" + identity_schema + "\0" + JCS(comparison_identity))
estimator_fingerprint = SHA-256("benchmark-estimator-v1\0" + JCS(estimator))
series_fingerprint = SHA-256("benchmark-series-v6\0" + comparison_fingerprint + "\0" + quantity_name + "\0" + estimator_fingerprint)
JCS is RFC 8785 canonical JSON. The server computes all fingerprints; canonical source URIs, number/string normalization, omitted values, estimator method versions, and identity schema definitions are part of the contract.
4.4 Quantity vocabulary¶
Quantities are an open vocabulary: wall time is not privileged, and the schema never enumerates what may be measured. CPU, GPU, and wall time; three distinct memory semantics; hardware counters; throughput; energy; binary size; quality scores are all ordinary quantities. Appendix A gives non-normative examples.
A quantity declaration carries:
name, stable within the project and immutable once used. Distinct semantics need distinct names: bytes allocated, returned-object size, and peak RSS are three quantities, never onememory.unit, the canonical UCUM unit (rule 8). Counts are1; rates are unit-per-time such asBy/s. Synthetic units are named as what they are: a simulator's weighted cycle estimate isestimated-cyclesin1, never seconds.direction(optional):lower-is-betterorhigher-is-better. Recorded because producers know it; excluded from identity and verdicts (rule 11).deterministic(optional): repetition under fixed conditions reproduces the value, as with simulated instruction counts or binary size. One observation is then complete evidence, not a weak sample.
Percentiles are estimators, not quantities: tracked p99 latency is quantity latency with estimator p99, one series per percentile. The same physical quantity may be a measurand in one series and a condition measurement beside another; name and unit conventions are shared so a value moves between the two without translation.
Condition measurements. A state the system passes through during an attempt, such as temperature or load, is a sibling result for its own quantity sharing the measurand's attempt_key, with measurement.kind condition and protocol.probe naming the sensor and how it was read (Appendix B). The kind belongs to the result, not the quantity, so consumers tell what an attempt measured from what it recorded beside it without consulting the work order. A probe that cannot read yields status skipped with reason probe-unavailable. examples/arrow-temperature.json is the temperature sibling of examples/arrow.json.
4.5 Estimators and compound outputs¶
An estimator declaration has a name, method version, optional parameters that can alter the value such as percentile interpolation, and an input level of observations or estimates. Project configuration lists the estimators it materializes, separately from the identity policy: the estimator enters series_fingerprint on its own (§4.3), so the list is not part of what makes two results the same measurement. For every result with sufficient observations, the server writes a derived point per server-computable configured estimator; a result with only producer estimates joins only the series of the estimators it reported. Adding an estimator later creates points for results with observations, never rewrites a stored result, and leaves the policy and every existing series unchanged.
A compound estimator additionally declares semantic input roles and quantities, which enter estimator_fingerprint. A separate provenance.references list maps roles to stable (producer, ingest_key) input references, which do not enter identity. Inputs may be any results in the same run_key: siblings of one attempt, as for a CPU/GPU critical-time; or results of different attempts, as for a Narwhals-over-pandas overhead ratio across two role variants or an anchor measurement in a cross-machine study. Explicit references remove ambiguity when a run holds several candidates. Paired observations carry matching group/ordinal or pair keys.
{
"estimator": {
"name": "max",
"method_version": "benchx/max/v1",
"input_level": "estimates",
"inputs": [{"role": "cpu", "quantity": "cpu-time"}, {"role": "gpu", "quantity": "gpu-time"}]
},
"references": [
{"role": "cpu", "result": {"producer": "example.org/adapter", "ingest_key": "ci-1234:parquet-read/snappy:cpu-time"}},
{"role": "gpu", "result": {"producer": "example.org/adapter", "ingest_key": "ci-1234:parquet-read/snappy:gpu-time"}}
]
}
How a compound result is created. A compound is an ordinary result for its own quantity, produced by a short pipeline that runs after its inputs exist and reads only stored, immutable results:
- Resolve inputs by role within one
run_key, and record each as a(producer, ingest_key)reference inprovenance.references. - Check compatibility. Inputs must share what the compound's series will claim: subject revision and tree, pinned components, environment, protocol apart from quantity-specific instrumentation such as
protocol.timer,protocol.synchronization, andprotocol.probe, and, across attempts,round. Instrumentation may differ per input, as it does between CPU and GPU time; it stays on the referenced inputs, and the compound's own protocol carries only the shared keys. Attempt integrity (§5.3) guarantees this for same-attempt inputs; the consuming profile (§5.5) does for cross-attempt ones. - Pair observations by ordinal or pair key. Pairing needs equal counts, which is why the profiles require interleaved rounds and equal repetitions.
- Apply the derivation rule per pair, such as
maxof CPU and GPU time or the ratio of narwhals to native time. The results are the compound's observation batch, kept as structured observations with their ordinals, and the rule is declared next to them inmeasurement.derivationas an estimator atinput_level: observations. - Apply an ordinary estimator to that batch,
mediansay, as the producer estimate. The project may materialize other estimators from the same batch later. - Assemble coordinates: those the inputs share, minus those they differ in such as
parameters.role; the compound's own quantity goes in its measurement. A same-attempt compound keeps the inputattempt_key; a cross-attempt one gets a new attempt key in the same run. - Ingest as a normal result. The inputs are untouched.
Because the inputs keep their batches, the pipeline runs identically at measurement time in the runner or later in the comparator, and the compound can be added retroactively across a project's whole history. The comparator has two outputs: a stored compound, ingested under its own producer name, which gets a series with one point per revision and is the form a tracked question such as "does the overhead stay low across commits" needs; or a value inside one comparison document, with no series, which is the form for a one-off question. Same inputs and the same versioned declaration land in the same series whoever computed it; a different rule or estimator parameter is a different series beside the old one, never a rewrite.
A compound over paired inputs must carry the pairwise batch. input_level: estimates is permitted only when the inputs have no observations or do not pair, and it yields an estimate without a batch. A statistic that is not a per-pair rule, such as a ratio of two minimums, is not a compound batch at all; it is a comparison-time statistic over the two input series, or a separate estimate-level compound with its own series. examples/narwhals-overhead-ratio.json shows the tracked form.
The indexed contract remains scalar: median([cpu_times, gpu_times]) emits one cpu-time and one gpu-time result rather than a tuple. Vectors, covariance, and other multidimensional evidence remain artifacts (§7).
5. Result contract¶
5.1 Fields and statuses¶
| Group | Fields |
|---|---|
| Identity facts | reported source, coordinates, reported_coordinates_fingerprint, subject revision_key; project and benchmark identity when not thin |
| Attempt grouping | attempt_key |
| Outcome | quantity, kind, status, optional reason, optional quantitative constraint |
| Producer evidence | optional observation batch and immutable typed summaries |
| Method | optional realized procedure JSON, including round within the run |
| Provenance | producer/version, ingest/native/run/batch keys, caller labels, dirty tri-state flags and working-tree ids, references to input or anchor results, mapping version, timestamps, artifacts, payload URI/checksum |
| Quality | immutable producer validation, including an observed correctness outcome, and warnings; store-side quarantine/exclusion is separate state |
Derived series_point rows live outside the result. Statuses are:
success: a usable observation batch or at least one usable producer estimate;partial: the same evidence plus a reported partial failure and reason;censored: only a lower, upper, or interval constraint is known, such asruntime > 60 s;error: no usable estimate or constraint;skipped: the harness explicitly skipped this known workload variant.
Absence of a row means the variant was not reported. Telling a lost result from one that was never planned requires the run manifest, which belongs to the runner and scheduler design and is out of scope here. Detectors exclude partial, censored, error, skipped, and quarantined results by default; how they treat results not marked clean is their policy, informed by the dirty flag and tree id (§4.1).
5.2 Example ingest object¶
{
"schema_version": "benchx/measurement-result/0.1.0",
"producer": {"name": "example.org/benchmark-adapter", "version": "4.0", "mapping_version": "native-to-benchx/v5"},
"ingest_key": "ci-1234:parquet-read/snappy:wall-time",
"attempt_key": "ci.example.org/runs/ci-1234/attempts/parquet-read-snappy-1",
"project": "arrow",
"source": {"uri": "https://github.com/apache/arrow", "type": "git"},
"revision": {"key": "02addad..."},
"benchmark": {
"source": {"uri": "https://github.com/example/arrow-benchmarks", "type": "git"},
"revision": {"key": "92ef3aa..."}
},
"coordinates": {
"workload": {"name": "parquet-read", "parameters": {"compression": "snappy"}},
"subject": {
"name": "apache-arrow",
"components": [{"role": "primary"}],
"configuration": {"build_type": "release", "compiler": "clang-18"}
},
"protocol": {
"name": "benchmark-time",
"version": "v1",
"timer": "perf_counter",
"warmup": {"mode": "time", "seconds": 1},
"calibration": {"mode": "adaptive", "minimum_sample_seconds": 0.01},
"filesystem_cache": "drop-before-attempt"
},
"environment": {
"schema_version": "machine/v1",
"identity": {"runner": "bench-01", "cpu": "AMD EPYC 7R13", "cores": 8},
"system": {
"os": {"kernel": "6.8.0-31-generic"},
"libc": {"version": "2.39"},
"runtime": {"python": "3.12"}
}
}
},
"measurement": {
"quantity": {"name": "wall-time", "unit": "s"},
"status": "success",
"observations": [0.0371, 0.0364, 0.0363, 0.0370, 0.0368],
"summaries": [
{
"type": "estimate",
"estimator": {"name": "mean", "method_version": "benchx/mean/v1", "input_level": "observations"},
"value": 0.03672,
"source": "producer"
},
{
"type": "statistic",
"name": "sample_standard_deviation",
"value": 0.0003564,
"method": "n-1",
"source": "producer"
},
{
"type": "confidence_interval",
"statistic": "mean",
"level": 0.95,
"lower": 0.03628,
"upper": 0.03716,
"method": "student-t",
"source": "producer"
}
]
},
"procedure": {
"inner_iterations": 100,
"attempted_repetitions": 5,
"completed_repetitions": 5,
"warmups_performed": 1,
"caches_cleared": ["filesystem"]
},
"provenance": {
"run_key": "ci.example.org/runs/ci-1234",
"started_at": "2026-07-18T09:12:44Z",
"subject_dirty": "clean",
"subject_tree": "4b825dc...",
"benchmark_dirty": "dirty",
"benchmark_tree": "9fceb02...",
"artifacts": [
{"kind": "patch", "media_type": "text/x-diff", "uri": "file:///tmp/bench/benchmark.patch", "sha256": "aaaa..."}
],
"runner": {"name": "example-runner", "version": "2.1"},
"source_payload_sha256": "bbbb..."
},
"quality": {"warnings": ["dirty-benchmark-tree"]}
}
A thin local message (§4.1) from a contributor's laptop omits project and benchmark, uses the local/v1 environment schema, and carries the run-level facts the local profiles need: a role parameter, a pinned dependency, procedure.round, a caller label, and honest dirty flags. It validates against the same schema and is schemas/measurement-result/0.1.0/examples/adhoc.json:
{
"schema_version": "benchx/measurement-result/0.1.0",
"producer": {"name": "example.org/benchx-cli", "version": "0.1.0", "mapping_version": "timeit-to-benchx/v1"},
"ingest_key": "session-7f3a:truncate/role=variant:round-1:wall-time",
"attempt_key": "urn:uuid:7c9e6679-7425-40de-944b-e07fc1f90ae7",
"source": {"uri": "https://github.com/pandas-dev/pandas", "type": "git"},
"revision": {"key": "a1b2c3d..."},
"coordinates": {
"workload": {"name": "truncate", "parameters": {"role": "variant", "rows": 1000000}},
"subject": {
"name": "pandas",
"components": [
{"role": "primary"},
{"role": "numpy", "source": "https://github.com/numpy/numpy", "revision": "v2.0.0"}
]
},
"protocol": {
"name": "timeit",
"version": "v1",
"timer": "perf_counter",
"warmup": {"mode": "repetitions", "n_warmup": 1},
"calibration": {"mode": "adaptive", "minimum_sample_seconds": 0.2},
"repetitions": {"mode": "fixed", "n_repeat": 5},
"schedule": {"unit": "attempt", "rule": "alternating", "rounds": 5}
},
"environment": {"schema_version": "local/v1", "identity": {"hostname": "rok-laptop"}, "system": {"runtime": {"python": "3.12"}}}
},
"measurement": {
"quantity": {"name": "wall-time", "unit": "s"},
"status": "success",
"observations": [0.000131, 0.000129, 0.000133, 0.000130, 0.000128]
},
"procedure": {
"inner_iterations": 2000,
"attempted_repetitions": 5,
"completed_repetitions": 5,
"warmups_performed": 1,
"round": 1,
"slot": 3
},
"provenance": {
"run_key": "urn:uuid:3f1d2c4b-8a9e-4f60-b1c2-d3e4f5a6b7c8",
"started_at": "2026-09-14T10:02:31Z",
"subject_dirty": "dirty",
"subject_tree": "e7a1c3d...",
"benchmark_dirty": "unknown",
"labels": {"env": "numpy-2.0"}
}
}
There are no producer summaries: the server, or the local comparator, derives the minimum and median from the observations. Its sibling with role = baseline shares the run key and round, and the variants profile pairs them. Because the subject tree is dirty, any comparison built on it is local-only (§5.5).
After accepting the Arrow message above, the server may materialize points such as the following. They are not fields on the result:
{
"series_fingerprint": "cccc...",
"result": {"producer": "example.org/benchmark-adapter", "ingest_key": "ci-1234:parquet-read/snappy:wall-time"},
"estimator": {"name": "median", "method_version": "benchx/median/v1", "input_level": "observations"},
"value": 0.0368,
"source": "derived"
}
5.3 Semantics and integrity¶
Evidence
summariesis an extensible producer-evidence list with typesestimate,statistic,confidence_interval,coverage_interval,source_bounds,standard_uncertainty, andexpanded_uncertainty. Each entry records the statistic addressed, source, method, and level or factor when applicable. Values use the quantity's canonical unit unless explicitly dimensionless.- A producer
estimatehas a finite value, a complete estimator declaration, andsource = producer; at most one per estimator per result. - Observations are finite values in the quantity's unit after declared normalization. A numeric array is the common batch; structured observations carry per-observation ordinal, slot, group, pair, inclusion, or exclusion data.
- Observation standard deviation is a precision statistic, not automatically the uncertainty of a mean or median. Unknown bounds stay
source_bounds; labels such asstat,sys,range, orerrorkeep source semantics. - For a deterministic quantity (§4.4), one observation is complete evidence and must not be treated as an undersampled distribution.
- A limit the run author imposed on the attempt, such as a timeout per workload variant, is recorded as
procedure.timeout_secondswhether or not it fired, so a result shows the conditions it ran under. An attempt stopped by the limit has statuserrorwith reasontimeout. It is notcensored: the limit bounds the attempt's duration, not the quantity, so a lower bound on the quantity would claim more than is known unless the quantity is the attempt's duration itself. successandpartialrequire a non-empty usable batch or at least one finite producer estimate.censoredrequires a constraint with kind (lower_bound,upper_bound,interval), finite bounds, inclusivity, and cause.errorandskippedcarry none of these. Iteration and repetition counts are positive, completed repetitions never exceed attempted ones, durations non-negative, and interval lower bounds never exceed upper bounds.
Derived projections
- A result contributes to a series when it has a producer estimate for that estimator or enough observations for the server to derive one. The value is stored in
series_point; a producer estimate for the same estimator stays unchanged, and a material discrepancy is recorded as a point warning. - Producers never send a comparison fingerprint, series identifier, series point, or projection. Reported coordinates and producer evidence are immutable; derived indexes change only by adding or rebuilding policy-versioned materializations, and schemas are retained side by side.
- Condition measurements are ordinary results and never enter comparison identity; analyses may annotate, filter, or stratify by them.
Attempts and keys
- A harness invocation reporting several quantities yields one result per quantity sharing
attempt_key; corresponding observations share group/ordinal or pair keys. - Attempt integrity. Results sharing an
attempt_keymust agree on project, designated source, subject revision, benchmark revision, dirty flags, working-tree ids, workload variant, subject descriptor, environment identity and system, andrun_key. They may differ in quantity, resource selection, and quantity-specific instrumentation. Disagreement with a stored sibling is an attempt conflict (§5.4). Resource selection is what lets GPU 0 and GPU 1 results share one host environment. - Two results are code-identical when their
subject_treeids match, and likewise forbenchmark_tree. This is the comparison unit for local work: a dirty checkout against HEAD is two distinct trees, and repeated rounds of the same dirty checkout are one tree. Detection and history still use the revision axis; how a project charts results not markedcleanis its policy, and the tree id is what lets it annotate them honestly. run_keycomes from the run author, who may also attachprovenance.labels, a string map such as{"env": "numpy-2.0"}, for selecting sides of a comparison. The executor creates a globally namespace-qualifiedattempt_key, such as a URI-like key or UUID, and producers preserve or deterministically map it. Keys and labels never enter identity. Identical retries of(producer, ingest_key)return the existing result; different payloads conflict.- Pairing across attempts, such as interleaved baseline and contender runs (UC-03), is expressed by matching
procedure.roundon the attempts and matching pair keys on structured observations.roundis assigned by the run author, increases monotonically within a run, and is kept by a retry, which gets a new attempt key but the same round so that pairing survives. It is never placed in the protocol, which would split the series.procedure.slotis the attempt's position in the run's realized order across all sides, so a comparator can verify the schedule it was given, whether alternating or random, and regress estimates on slot to detect drift. When a harness interleaves at the observation level, as Google Benchmark's random interleaving does, each observation carries its ownslotandpairinstead. The intended schedule isprotocol.schedule(Appendix B); the slots are the realized fact. provenance.started_atis required on every result. It is the measurement start on the producer's clock and the only fact that orders attempts across runs; ingest time is never used for ordering.- Ratios, confidence bounds, and faster/slower classifications produced by a comparison are a separate comparison document, not fields of a result; the comparator design specifies it.
- Ground-truth values never leave the runner. What is recorded is what the runner derived from them: an error norm or mismatch count as another quantity under the same attempt, and the observed check as
quality.validation.correctness(passorfail). The verdict that acts on it is decision policy. A full dependency manifest is a provenance artifact; only the components policy treats as identity are pinned in the subject descriptor. - A result's designated source, workload variant, and quantity belong to one project, and its subject revision refers to its designated source.
- Large observation sets, covariance matrices, profiles, histograms, and similar outputs are external artifacts with media type, URI, and checksum. Migrations retain mapping version and source payload reference.
Physical partitioning and indexes are implementation choices; global idempotency must hold regardless.
5.4 Ingest contract¶
The ingest object (§5.2) is the message; transport is a deployment choice. The litmus test is file drop: writing result documents to a directory that is later swept into the store must be a valid delivery path.
- Acknowledgment means validated and durable. A positive response promises the result is stored and queryable; the producer may forget it.
- Entities auto-create; meaning is protected. First sight of a project, source, workload variant, quantity, or revision creates it, and series and points are materialized without changing the result. A quantity arriving with a different unit is rejected as a unit conflict, enforcing rule 12 at ingest rather than at declaration.
- No ordering, no deadline. Results may arrive late, duplicated, or out of order; an offline laptop or weekly batch is valid indefinitely.
- Batches are transport optimizations. A multi-result delivery is N independent documents with N outcomes; there is no atomic batch, and
attempt_keysurvives any framing. - Machine-readable rejection taxonomy. At minimum malformed, idempotency conflict (same
(producer, ingest_key), different payload), unit conflict, identity violation (coordinates and revision from different sources, a primary component whose source differs from the top-level source, or a payload carrying a series identifier or fingerprint), attempt conflict (§5.3), and quarantined (accepted but withheld pending review).
5.5 Comparison profiles¶
Results carry facts; a profile is a comparator-time check that a set of results sharing a run_key is fit for one kind of comparison. A profile is a built-in projection (§4.3): the identity policy's roles with the coordinate it varies as the axis, and the invariants below. Profiles are not declared by producers and add no schema fields. Each use case in docs/use-cases/ names one; the comparator validates it before producing a comparison document and refuses to compare otherwise.
| Profile | Varies | Must agree | Invariants checked over the run |
|---|---|---|---|
single-run (UC-01) |
workload parameters | protocol | none beyond attempt integrity |
variants (UC-02) |
one or more workload parameters chosen by the caller | subject tree id, subject configuration, environment, protocol, every other workload parameter | equal attempt counts per side; sides alternate by procedure.slot; a separately recorded benchmark has one benchmark_tree across both sides and all rounds, invariant one-benchmark-tree |
revisions (UC-03) |
subject tree id | workload variant, subject configuration, environment, protocol | equal attempt counts per side; sides alternate by procedure.slot; each side has exactly one tree id, so a side whose dirty state is unknown and has none fails, and the two differ; a separately recorded benchmark has one benchmark_tree across both sides and all rounds, invariant one-benchmark-tree |
environments (UC-04) |
one pinned component, subject configuration, or environment.system field, named by labels |
subject revision, workload variant, host environment, protocol | exactly two label values whose reported coordinates differ; equal attempt counts per label; sides alternate by procedure.slot |
cross-machine (template example, pending a use case) |
environment identity | subject tree id, workload variant, subject configuration | each side references an anchor result from the same run via provenance.references |
Sides are chosen at comparison time. The coordinate a profile varies is an argument to the comparator, not a declaration by the producer. Workload parameters are always recorded, so any parameter or set of parameters can be the varying one: stable for a sort-algorithm study, fast_path for a fast-path deficit, {role, size} for a two-factor design. Each distinct assignment of the chosen parameters is one side, and every other coordinate must agree. A comparison has two sides (comparator.md §1), so the chosen parameters must take exactly two joint assignments in the run. role is only the conventional name that UC-02's --baseline/--variant flags write; nothing in the schema privileges it. A comparison that was not planned when the run was made is therefore the same operation as one that was, provided the run contains the sides; what a run did not measure, the comparator cannot invent, and a planned run is what guarantees the interleaving invariant.
A comparison in which any side is not clean is local-only under the profiles that allow it (single-run, variants, revisions, environments): the comparator produces it, labels it so, names the sides that are not clean, and never promotes it into tracked history. The tree id is what makes each side internally consistent even though neither may have a commit. Under revisions this is the comparison of an uncommitted change against HEAD, or against another revision, that the one-off and Arrow local stories want (UC-03 §10). What revisions still requires is that every side can be identified: a side whose dirty state is unknown has no tree id (§4.1), so it cannot be told apart from the other side or checked for a change during the run, and it fails the invariant that each side has exactly one tree id, so its units get no verdict.
A benchmark is recorded separately from the subject when its suite lives outside the subject's checkout (runner-schema.md §3.3). Its benchmark_tree is then the suite file's own id and differs from the subject_tree; this includes a suite in another checkout of the same repository, which has the same source URI but its own file id. A benchmark inside the subject's checkout has benchmark_tree equal to subject_tree and the subject's own source and revision as benchmark.source and benchmark.revision; a result whose two trees are equal but whose benchmark.source differs from its source is malformed. Under revisions and variants a separately recorded benchmark must be one file on both sides and in every round (invariant one-benchmark-tree): the suite is part of the code identity, which UC-03 requires the sides to agree on and UC-02 holds controlled (UC-02 §5), and an edit or commit of the suite during the run is otherwise invisible, because the identity policy maps exact benchmark revisions to declared versions (§4.1) and benchmark_dirty only compares the file with its own HEAD. Benchmarks inside the subject's checkout differ between the sides exactly as the subject trees do under revisions and are not checked; under variants an edit of one changes the subject tree, which the sides must share (sides-agree). A result without a benchmark_tree, such as a thin result, is outside the rule.
Thin results (§4.1) may claim single-run, variants, and revisions within their own run; the other profiles require a full environment identity.
A revision may hold several attempts with identical coordinates, since every attempt is its own point, so viewers and comparators must expect more than one result per coordinates and revision. A comparison document therefore lists the (producer, ingest_key) of every result it consumed. That is also what lets a saved baseline be reused by reference instead of re-measured (Arrow local story).
A trend over ad hoc attempts, ordered by run and round and then by started_at, is likewise a projection over the stored facts, mixing code unless it matches the subject tree (§4.3); no timestamp or counter in a result implies code identity.
6. Migration contract¶
Each import records whether observations are structured, flat, or absent; whether several quantities share a native attempt; whether benchmark and subject revisions can be distinguished; whether reported coordinates are complete for the identity policy or thin; and whether summaries are typed, source-defined, or absent.
Thin means the source lacks coordinates the target policy requires. The importer records the missing attributes in provenance rather than inventing defaults, and thin imports are not matched across sources without an approved mapping. An importer preserves a native attempt identifier when its grouping semantics are known and otherwise generates a distinct attempt key per result.
Importers must be idempotent, retain source payload and mapping version, report identity collisions before writing, preserve failures/skips/limits, reject unit conflicts, and use the same mapping rules as live adapters. They must not parse parameters from names without project rules or copy source-local fingerprints.
6.1 Tool mapping summary¶
| Source | Core mapping | Important caveat |
|---|---|---|
| ASV | hardware → environment; OS → environment system; environment/dependencies/build → subject descriptor, protocol, or environment system by semantics; benchmark + parameters → workload variant; result → median estimate; samples → observations; CI99 → median confidence interval | null is failed and NaN is skipped. Preserve exact benchmark version as benchmark identity; map its declared semantic version conservatively into protocol. |
| Conbench | native case → workload variant; hardware → environment; source → designated source/revision; context fields split among subject, protocol, and environment system; data → observations; times → procedure | Derive quantity and estimator; do not copy history_fingerprint. Use the source deployment's configured summary policy. |
| Bencher | Project/Testbed/Benchmark/Measure → project/environment/workload variant/quantity; Metric → source-defined estimate; Report → provenance | Bounds are adapter-defined. Keep opaque bounds as source_bounds; threshold direction is decision policy. |
| Codespeed | Project/source/revision → project and designated axis; Executable → subject descriptor; Benchmark → workload variant + quantity; Environment → environment identity/system; Result aggregates → summaries | Benchmark.data_type selects mean or median. Snapshot mutable unit definitions. |
| LNT | suite → project; Machine fields split among subject, environment identity, protocol, and environment system; Test → workload variant; metric → quantity; list/scalar → observations/estimate; Run → provenance | Preserve native order, status/hash, and source metadata. Opaque machine identity is thin. |
| Nyrkiö | test path → workload variant; metric name/unit → quantity; value → source-defined estimate; source attributes → revision | Preserve per-metric direction as source decision metadata; conflicts require review. |
| github-action-benchmark | suite/project; name → workload variant; tool/unit → quantity; value → source-defined estimate; commit/date → revision/provenance | Interpret range by adapter version; otherwise retain it as an opaque summary. History may be truncated. |
| OpenTelemetry Benchmarks | scenario ID/workload → workload variant/protocol; tracked package version → revision; name/unit/value → quantity/estimate; extra → environment identity/system/provenance |
Its current output is github-action-benchmark's {name, unit, value, extra} format. Estimator and direction come from the scenario document; observations and uncertainty are absent. |
| rustc-perf | artifact → revision; pstat dimensions → workload variant/protocol/quantity; collection → run/attempt/observation grouping | Group compatible collection rows into observations. Environment is thin unless collector metadata can be joined. |
| CodSpeed | aggregate-only source estimate | No reviewed public export schema supports a migration contract. |
6.2 Live harness adapters¶
- Google Benchmark: iteration rows become observations; aggregate rows remain producer summaries; secondary metrics become sibling results sharing
attempt_key; split mixed context into protocol, environment identity, environment system, procedure, and provenance. - pytest-benchmark: round data becomes observations; requested warmup/timing strategy goes in protocol, realized rounds, iterations, and warmups in procedure.
- JMH: preserve fork grouping; map
scoreConfidenceas a 99.9% confidence interval; mode and secondary metrics define quantity/estimator semantics. - cargo-criterion: pair
iteration_countwithmeasured_values; retain typical/mean/median/MAD/slope estimates and bounds. - BenchmarkDotNet: detailed measurements become observations; retain parameters, job, host, statistics, confidence interval, and percentiles with source semantics.
6.3 OpenTelemetry interoperability¶
OpenTelemetry defines no general benchmark-result convention; its benchmark repository reports through github-action-benchmark, and OTLP Metrics is a generic time-series transport. Compatibility is therefore an adapter concern.
For OTLP export, map environment to Resource, producer name/version to Scope, quantity/unit to a Metric, the exported series_point estimate to a Gauge point, and project/source/workload variant/protocol/resource selection/revision/estimator to point attributes. Export only usable scalar estimates: OTLP has no lossless representation for observations, source bounds, intervals, errors/skips, or idempotency keys, so the result store remains the system of record. Because OTLP treats attributes as stream identity, do not export ignored environment.system fields as metric attributes when one continuous downstream series is required; put them on a linked log/span instead. When ingesting OpenTelemetry CI/CD metadata, map vcs.repository.url.full to source URI, vcs.ref.head.revision to revision key, cicd.pipeline.run.id to run key, and cicd.worker.id to environment identity, preserving the original attributes and schema URL in provenance.
7. Scope and simplifications¶
The following do not justify core tables for current use cases:
- Measurement procedure, measuring system, and model: covered by protocol, environment system, condition measurements, procedure, and provenance.
- Runs/events:
run_keyandattempt_keygroup results without a transactional parent. - Observation rows, uncertainty budgets, covariance, and multidimensional data: inline typed JSON for the common case; artifacts for the large or specialist case.
- Unit and estimator tables: the quantity stores its unit; project configuration lists the estimators it materializes.
- Revision graph and alias tables: source integrations or an optional revision catalog provide topology; audited display mappings provide aliases.
A structure becomes first-class only when it participates in comparison identity, referential integrity, or frequent indexed queries. This keeps the core at nine tables. The store also excludes alert policy, CI orchestration, source-code hashes as workload-variant identity, source-local fingerprints, large blobs, and branch/fork metadata as revision properties.
8. Reviewed sources¶
Measurement science and experimental physics¶
- JCGM 200:2012, International Vocabulary of Metrology (VIM)
- JCGM 100:2008, Guide to the Expression of Uncertainty in Measurement (GUM)
- JCGM 106:2012, The Role of Measurement Uncertainty in Conformity Assessment
- BIPM, SI Brochure, 9th edition
- HEPData data format, pinned
e54de2d - OpenTelemetry: performance benchmark guidance, Metrics data model, and CI/CD semantic conventions
Benchmark systems and harnesses¶
- ASV result format (
7032df7) and statistics (01d4a25) - Conbench (
3af4a55), Bencher (d2895af), and Codespeed (263860b) - LNT (
0e08c62), Nyrkiö (5b63790), github-action-benchmark (86d8bcf), and rustc-perf (3bbef83) - CodSpeed CPU Simulation
- OpenTelemetry Benchmarks: S001 scenario and harness reporting format
- Harnesses: Google Benchmark (
8b66b54), pytest-benchmark (47d66c8), JMH (a194eea), Criterion (3dbc6c6), and BenchmarkDotNet (2365829)
The pinned primary-source links above are the review record included with this draft.
9. Open questions from the placement revision¶
- Scoped roles. A projection gives a fact one role for every result it covers, so an allocator suite that must match the glibc version and a Parquet suite that should not cannot share one identity policy without matching it for both. Roles scoped by workload or quantity would solve this at the cost of precedence rules.
- Conditions on measurements. A projection cannot yet keep only results whose sibling measurement satisfies a bound, such as attempts with no throttle events. Conditioning on a state the workload itself drives, such as temperature, would bias the series.
- Envelope weight. Each condition measurement is a full result repeating the attempt's coordinates. Should one message be able to carry several sibling measurements?
- Addressing pinned components. JSON Pointers address array members by index; a release-line rule for one pinned component needs to address
subject.componentsbyrole. - The system boundary. For self-modifying systems, such as a warmed JIT or an on-disk kernel cache, whether a value is a coordinate or a measurement depends on whether "the system" is the code or the running process.
- Re-fingerprinting on policy edits.
identity_schemaenters every comparison fingerprint, so any edit to the identity policy gives all of its series new fingerprints, including those whose identity object did not change, and annotations and detector state stay with the old ones. Hashing the identity object and axis instead, and recording the projection version on each series point, would re-fingerprint only the series an edit affects, at the cost of marking where a series' definition changed. Deferred: edits should be batched until then. - Sources of facts. Whether a fact was declared, detected, or reported by a collector (
benchmark-environments.md§9 question 1) is not recorded; one option is a provenance map from JSON Pointer to source.
Appendix A: Example quantities (non-normative)¶
Illustrations of the open vocabulary (§4.4), with canonical UCUM units. None of these names is required; projects declare what they measure.
| Name | Unit | Meaning | Notes |
|---|---|---|---|
wall-time |
s |
elapsed real time | universal |
cpu-time |
s |
process CPU time | Google Benchmark default; hyperfine user/sys |
gpu-time |
s |
device-side time, synchronized | cupyx, torch benchmarks |
latency |
s |
per-request time; percentiles via estimators | vLLM, MLPerf server scenarios |
time-to-first-token |
s |
streaming first-response time | LLM inference |
peak-rss |
By |
peak process resident set size | ASV peakmem_, pyperf, rustc-perf max-rss |
allocated-bytes |
By |
bytes allocated per operation | JMH GC profiler, BenchmarkDotNet, Go B/op |
allocations |
1 |
allocation count per operation | Go allocs/op |
object-size |
By |
size of a produced object | ASV mem_ |
gpu-peak-memory |
By |
peak device memory | torch max_memory_allocated |
instructions |
1 |
retired instructions; deterministic when simulated | rustc-perf default metric |
cycles |
1 |
CPU cycles | noisier than instructions |
estimated-cycles |
1 |
simulator-weighted cycles (synthetic) | CodSpeed, Cachegrind models |
cache-misses |
1 |
cache misses, level named if specific | perf, JMH perfnorm |
branch-misses |
1 |
branch mispredictions | perf, BenchmarkDotNet |
page-faults |
1 |
page faults | perf, rustc-perf |
throughput-bytes |
By/s |
bytes processed per second | Google Benchmark, criterion |
throughput-items |
1/s |
items/operations per second | ops/s, tokens/s, IOPS |
energy |
J |
energy consumed | RAPL, Scaphandre |
power |
W |
average power draw | SPECpower, mobile benchmarks |
cpu-frequency |
MHz |
observed frequency; often a condition measurement instead | pyperf metadata |
cpu-temperature |
Cel |
sensor temperature; often a condition measurement instead | pyperf metadata |
load-average |
1 |
1-minute load average; a condition measurement | /proc/loadavg, pyperf |
throttle-events |
1 |
thermal or power throttle events across the attempt; a condition measurement | throttle counters, NVML |
binary-size |
By |
linked artifact size; deterministic | rustc-perf size:*, LNT, Chromium |
build-time |
s |
time to build the artifact | LNT, Bencher tutorials |
compression-ratio |
1 |
output/input size ratio | zstd, lzbench |
score |
1 |
quality metric paired with performance | LNT score, MLPerf accuracy |
Appendix B: Recommended protocol, system, and procedure keys (non-normative)¶
Protocol, environment.system, and procedure are open objects (§4.2). These keys are recommended spellings for common acquisition settings so that adapters agree. Intended settings go under protocol; realized facts go under procedure; facts about the system go under environment.system; settings that change the artifact under test go under subject.configuration.
| Key | Placement | Meaning | Example values |
|---|---|---|---|
protocol.name, protocol.version |
protocol | Declared measurement protocol identity | benchmark-time / v1 |
protocol.timer |
protocol | Clock or event source for a time quantity | perf_counter, process_time, cuda-events, cuda-synchronize, rdtsc |
protocol.synchronization |
protocol | Device synchronization before reading a timer | none, stream, device |
protocol.warmup |
protocol | Warmup policy | {"mode": "none"}, {"mode": "repetitions", "n_warmup": 3}, {"mode": "time", "seconds": 1} |
protocol.calibration |
protocol | How inner iterations are chosen; an adaptive target may be a compound of several quantities | {"mode": "adaptive", "minimum_sample_seconds": 0.01}, {"mode": "adaptive", "minimum_sample_seconds": 0.01, "target": "max(cpu-time, gpu-time)"}, {"mode": "fixed", "inner_iterations": 100} |
protocol.repetitions |
protocol | How many observations are requested. A harness that repeats at more than one level, as pyperf does with worker processes each producing several values, states the levels outermost first | {"mode": "fixed", "n_repeat": 10}, {"mode": "adaptive", "max_seconds": 5}, {"mode": "fixed", "levels": [{"unit": "process", "n": 20}, {"unit": "value", "n": 3}]} |
protocol.filesystem_cache |
protocol | Requested cache action | unchanged, drop-before-attempt, drop-before-repetition |
protocol.gc |
protocol | Garbage-collector policy during timing | enabled, disabled, collect-before-repetition |
protocol.probe |
protocol | Instrumentation for a non-time quantity, including a condition measurement and how it was sampled | {"name": "OSSMemoryProbe", "counter": "rss", "sampling_ms": 10}, {"name": "hwmon", "sensor": "coretemp/package", "sampling": {"mode": "interval", "seconds": 1}} |
protocol.threads |
protocol | A harness's own threading mode, when it is not settable per variant. Thread caps set through environment variables are workload-variant parameters instead (§4.2), so that one thread and ten can be compared within a suite | {"mode": "harness-managed"} |
protocol.affinity |
protocol | Requested CPU or NUMA pinning | {"cpus": "0-7", "numa_node": 0} |
protocol.schedule |
protocol | Intended interleaving: the unit alternated between sides, the ordering rule, and the number of rounds | {"unit": "attempt", "rule": "alternating", "rounds": 5}, {"unit": "observation", "rule": "random", "rounds": 12}, {"unit": "pair", "rule": "random"}, {"unit": "attempt", "rule": "concurrent"} |
configuration.execution_mode |
subject descriptor | JIT versus ahead-of-time execution of the subject | jit, aot |
os.family, os.release, os.kernel |
system | Operating system and kernel | linux, ubuntu-24.04, 6.8.0-31-generic |
libc.name, libc.version |
system | C library | glibc, 2.39 |
cpu.microcode, cpu.governor, cpu.boost, cpu.smt |
system | CPU firmware and settings as read before the attempt | 0xa20120e, performance, false, true |
gpu.driver, gpu.uuids, gpu.clocks_locked_mhz |
system | Accelerator driver, device ids, and a clock lock if set | 550.90.07, ["GPU-3f2a..."], 1500 |
container.image_digest |
system | Image the attempt ran in | sha256:4f1c... |
runtime.python, runtime.implementation, runtime.compiler, runtime.cflags |
system | Language runtime of the measured process | 3.12.4, cpython, GCC 13.2.0, -O3 |
process.environment, process.cgroup |
system | Allowlisted environment variables and cgroup limits of the measured process | {"OMP_NUM_THREADS": "1"}, {"cpu_max": "max"} |
<collector>.* |
system | Facts a context collector reported, under its name (harness-adapter.md §5.1) |
{"numpy-build": {"blas": "openblas-0.3.27"}} |
inner_iterations |
procedure | Inner iterations actually used per observation | 100 |
attempted_repetitions, completed_repetitions |
procedure | Observations requested and obtained | 5, 5 |
repetition_levels |
procedure | Per level, outermost first, repetitions requested and obtained, as totals over the attempt. For harnesses that repeat at several levels | [{"unit": "process", "attempted": 20, "completed": 18}, {"unit": "value", "attempted": 60, "completed": 54}] |
warmups_performed |
procedure | Warmup repetitions actually run | 1 |
caches_cleared |
procedure | Caches actually dropped, in order | ["filesystem"], ["filesystem", "cuda-jit"] |
slot |
procedure | Position of this attempt in the run's realized order across all sides, zero-based | 0, 3 |
round |
procedure | Position of this attempt in an interleaved run, monotone per run and kept by a retry; the same round number joins one baseline and one variant attempt | 0, 1 |
duration_seconds |
procedure | Wall time spent on the attempt including warmup | 2.31 |
timeout_seconds |
procedure | Limit the attempt ran under, as ordered; an attempt stopped by it is error with reason timeout |
1800 |
attempted_repetitions and completed_repetitions are the totals at the innermost level. A harness that repeats at several levels also reports procedure.repetition_levels, outermost first, each {unit, attempted, completed} as totals over the attempt: twenty processes of three values attempted 20 processes and 60 values, and if two processes died the report is process 20 attempted and 18 completed, value 60 and 54. This keeps a lost process distinguishable from dropped values. Completed never exceeds attempted at any level.
A pedantic harness mode is calibration.mode: fixed plus repetitions.mode: fixed; an adaptive mode is calibration.mode: adaptive with the minimum sample duration it enforces. Changing any protocol value creates a new series under the default identity policy; changing a procedure value never does.