Parse BMF v1: benchmark parameters and named metric values - #978
Draft
epompeii wants to merge 4 commits into
Draft
Parse BMF v1: benchmark parameters and named metric values#978epompeii wants to merge 4 commits into
epompeii wants to merge 4 commits into
Conversation
Tests and fixtures for the BMF v1 wire shape, along with the results restructure and the `json_v0` leaf they run against. The `json_v1` leaf parses nothing and the named value cap does not truncate yet, so every assertion about v1 fails while the whole BMF v0 suite stays green.
The `json_v1` leaf reads a benchmark name onto an array of entries, each
carrying a canonical parameter set and its measures, and each measure
carrying named scalars. An absent `parameters` and an explicit `{}` are
the same grid point, so entries that canonicalize alike merge.
A measure keeps at most eight named values. Survival is deterministic:
the three conventional names are never dropped and the remainder is kept
lexicographically, since hash map iteration order is not a tiebreak.
Two entries of one canonical parameter set union their measures, and a measure in both unions its names with the later entry winning per name, exactly JSON object key semantics. The tests pin the union across the three canonical entries, the disjoint and the overlapping name cases, the cap applied to the union rather than to either entry, and a name written twice counting as one name and never as a drop. The overwrite case puts nine named values across two entries so that eight distinct names survive at the cap. That is stronger than a bare `dropped_names` of zero: it is red under last entry wins, and it stays red against an implementation that counts occurrences instead of names. All five fail against the current merge, which replaces a measure wholesale and so discards the earlier entry's names without counting them.
Two entries of one canonical parameter set union their measures rather than the later entry replacing the measure the earlier one reported. A measure carried by both unions its names, the later entry winning per name, exactly JSON object key semantics, so a name is never discarded without being counted. The cap already ran once the grid point was whole, so it applies to the union and reports only what it dropped. A name written by two entries is one name, not a drop.
epompeii
force-pushed
the
benchmark-parameters/single-valued-metric
branch
from
August 18, 2026 03:48
50c23dc to
c39ed9c
Compare
epompeii
force-pushed
the
benchmark-parameters/bmf-v1
branch
from
August 18, 2026 03:48
0a3d6c6 to
3dbd01c
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Parse BMF v1 payloads: a benchmark name maps to an array of entries, each carrying a parameter set and its measures, with named metric values as scalars inside each measure.
This layer is parsing only. Nothing it produces reaches the database yet; the parameter aware ingest path is the next layer.
The adapter tree
jsonbecomes a node over two new leaves, exactly likerust_gungraunis a node overrust_gungraun_jsonandrust_gungraun_stdout:json_v0(discriminant11) requires every benchmark to map to an object.json_v1(discriminant12) requires every benchmark to map to an array.Each leaf is all or nothing, so a payload mixing the two shapes fails both leaves and therefore the node. There is no version field and no magic key, and BMF v0 parses byte for byte as it always has: the node tries v0 first, and every existing v0 fixture asserts identically through the node and through the leaf.
The leaves are individually selectable through the API's adapter setting but stay undocumented, which is the established pattern for
rust_gungraun_jsonandrust_gungraun_stdout. The CLI'sCliReportAdapterenum is untouched.Jsonstays in the identity arm ofAdapter::normalize(): it is a format node, not a language node.The leaves live in
adapters/json/{mod,v0,v1}.rsrather than as flat siblings, matching every other adapter family in the crate (c_sharp/mod.rsoverc_sharp/dot_net.rs). The node/leaf structure is gungraun's.Internal results restructure
AdapterResultswasHashMap<BenchmarkNameId, HashMap<MeasureNameId, JsonNewMetric>>, which can represent neither a grid point nor a named scalar. It is now:BTreeMap<JsonParameters, AdapterMetrics>, its grid points keyed by canonical parameter set,AdapterMetric, aBTreeMap<MetricName, f64>of named scalars,Every other adapter is unchanged: their constructors go through one helper that resolves to the empty parameter set, and
From<JsonNewMetric> for AdapterMetricmaps the triple onto exactlyvalue,lower_value, andupper_value. A metric without bounds becomes exactly one name. No adapter outsidejson_v1needs to know parameter sets exist, and none of their test assertions changed.AdapterJson::parseused to deserialize straight intoAdapterResults. That shortcut cannot survive two wire shapes, so each leaf now has its own wire type and converts.bencher mockand the noise runner both emit BMF v0 and used to get it fromAdapterResults's derivedSerialize. They now build the v0 wire type (JsonV0Results) directly, so what they emit is pinned to the format rather than to an internal shape.AdapterResultsno longer derivesSerialize/Deserialize. Note thatservices/cli/src/bencher/sub/mock.rsis the only file touched underservices/cli: the CLI's adapter surface is untouched, and the mock change is the one the spec for this layer calls for.Absent
parametersand{}are the same grid pointAn entry without
parametersresolves to the benchmark's empty parameter set, and an explicit{}canonicalizes to that same set, so the two are one grid point and not two. Two entries that land on the same canonical parameter set merge their measures rather than forking a series, which is what makes key order and number spelling irrelevant. Pinned by test.Parameter values stay JSON scalars only, canonicalized per RFC 8785 by reusing
JsonParameters. There is no second canonicalizer.The named value cap
A const 8 named values per measure. Excess is dropped, never an error: a benchmarking pipe should not fail CI over excess statistics.
Survival is deterministic, because hash map iteration order is not an acceptable tiebreak.
value,lower_value, andupper_valueare never dropped; the remainder is kept in lexicographic order up to the cap. The cap fixture places seven names that sort before the conventional trio, so a naive lexicographic cut would dropvalueandupper_valueand the test would catch it.Deliberate split: the adapter truncates and exposes the drop count on its results. The log line and the otel counter land with the ingest layer, where the providers are in scope;
bencher_adapterdepends on neither a logger nor otel today and this layer does not add them.Fold
--iteris untouched. Fold over BMF v0 is unchanged and the fold machinery is retained in full.Fold is not supported for BMF v1, because the mean of per iteration
p99values is not thep99of the pooled sample. That refusal is structural rather than a runtime check:combined,Add,Div,Sum,median, andfoldmoved ontoFoldableResults/FoldableResultsArray, which are BMF v0 by construction, and the only way in isAdapterResultsArray::foldable(), which hands the array back untouched if any payload is v1. There is no arithmetic on named scalars anywhere, so a folded v1 result cannot be produced by accident.Refusal keys on the payload version, not on which names a measure happens to carry: a v1 payload that spells only the conventional trio is still v1 and still refused. One v1 payload poisons the whole array, since fold spans every iteration.
The user-visible warn-and-ingest-unfolded behavior belongs to the fold deprecation layer.
Ingest, temporarily
ReportResults::processconsumes the v0 view and returns a 400 for a v1 payload, since v1 does not reach the database yet. Before this change a v1 payload already returned a 400, from the adapter failing to convert it; the user-visible class is unchanged. The parameter aware ingest path removes this guard.Console
adapterIcon, bothadapterCommandswitches,adapterName, andViewCard's two switches take the new variants.validAdapterexcludes them by omission, which is exactly how it excludes the gungraun leaves: they are undocumented.services/api/openapi.jsonandservices/console/src/types/bencher.tsare regenerated.One note for reviewers:
adapterNamewas already a non-exhaustive switch before this change, since it never covered the language level variants (Rust,Cpp,Go, and the rest), so adding the JSON leaves was not forced by a compile error.astro checkhas a large pre-existing error baseline that includes that non-exhaustiveness and a good deal ofViewCard.tsx. It reports 758 errors both with and without this change, so this change adds none, and none of the errors reference the added variants.Documentation
BMF v1 documentation ships with the parameters API layer. Deferred, not forgotten.
Tests, red before green
The first commit is the tests and fixtures plus the restructure scaffolding, with
AdapterJsonV1::parsereturningNoneand no cap truncation. At that commitcargo nextest run -p bencher_adapter --no-fail-fastreports 158 passed, 20 failed, and every one of the twenty is a v1 assertion: v1 parsing through the leaf, the node, and magic; the absent-versus-{}merge; canonicalization; both cap tests; and all four fold refusals. Every BMF v0 test passes there, which is what the restructure had to preserve.json_v0's leaf is implemented in that commit because it is a pure refactor of behavior that already existed, and the v0 tests are regression guards rather than new behavior. Three of the new tests also pass at red for the same reason:json_v0rejecting an array,json_v1rejecting an object, and the mixed-version payload failing everything, all of which hold trivially whilejson_v1parses nothing.Gates
Run at the head of the branch:
cargo nextest run: 2058 passed, 0 failed, 3 skippedcargo nextest run -p bencher_schema --features plus: 193 passed, 0 failedcargo test --doc: no failurescargo clippy --no-deps --all-targets --all-features -- -Dwarnings: cleancargo fmt --check: cleancargo check --no-default-features: cleancargo deny check: advisories, bans, licenses, and sources okcargo gen-types:services/api/openapi.jsonandservices/console/src/types/bencher.tsregenerated and committed./scripts/clippy.shand./scripts/test.sh --linux-only(obligated by thebencher_noisechange): both clean, cross compiled tox86_64-unknown-linux-gnuwith zigastro checkreports 758 errors with and without this change, so it adds none, and none of them reference the added variants;biome checkclean on the changed files