Parse BMF v1: benchmark parameters and named metric values - #990
Draft
epompeii wants to merge 6 commits into
Draft
Parse BMF v1: benchmark parameters and named metric values#990epompeii wants to merge 6 commits into
epompeii wants to merge 6 commits into
Conversation
epompeii
force-pushed
the
u/ep/benchmark-parameters/bmf-v1
branch
from
August 22, 2026 16:27
68ed3f2 to
e002a7a
Compare
Contributor
|
| Project | Bencher |
| Branch | u/ep/benchmark-parameters/bmf-v1 |
| Testbed | intel-v1 |
🚨 4 Alerts
| Benchmark | Measure Units | View | Benchmark Result (Result Δ%) | Upper Boundary (Limit %) |
|---|---|---|---|---|
| Adapter::Json | Latency microseconds (µs) | 📈 plot 🚷 threshold 🚨 alert (🔔) | 5.25 µs(+11.80%)Baseline: 4.69 µs | 4.99 µs (105.17%) |
| Adapter::Magic (JSON) | Latency microseconds (µs) | 📈 plot 🚷 threshold 🚨 alert (🔔) | 5.10 µs(+11.93%)Baseline: 4.55 µs | 4.82 µs (105.77%) |
| Adapter::Rust | Latency microseconds (µs) | 📈 plot 🚷 threshold 🚨 alert (🔔) | 4.72 µs(+33.62%)Baseline: 3.53 µs | 3.93 µs (120.04%) |
| Adapter::RustBench | Latency microseconds (µs) | 📈 plot 🚷 threshold 🚨 alert (🔔) | 4.67 µs(+32.25%)Baseline: 3.53 µs | 3.93 µs (118.73%) |
Click to view all benchmark results
| Benchmark | Latency | Benchmark Result microseconds (µs) (Result Δ%) | Upper Boundary microseconds (µs) (Limit %) |
|---|---|---|---|
| Adapter::Json | 📈 view plot 🚷 view threshold 🚨 view alert (🔔) | 5.25 µs(+11.80%)Baseline: 4.69 µs | 4.99 µs (105.17%) |
| Adapter::Magic (JSON) | 📈 view plot 🚷 view threshold 🚨 view alert (🔔) | 5.10 µs(+11.93%)Baseline: 4.55 µs | 4.82 µs (105.77%) |
| Adapter::Magic (Rust) | 📈 view plot 🚷 view threshold | 27.06 µs(+4.80%)Baseline: 25.82 µs | 27.12 µs (99.79%) |
| Adapter::Rust | 📈 view plot 🚷 view threshold 🚨 view alert (🔔) | 4.72 µs(+33.62%)Baseline: 3.53 µs | 3.93 µs (120.04%) |
| Adapter::RustBench | 📈 view plot 🚷 view threshold 🚨 view alert (🔔) | 4.67 µs(+32.25%)Baseline: 3.53 µs | 3.93 µs (118.73%) |
epompeii
force-pushed
the
u/ep/benchmark-parameters/bmf-v1
branch
from
August 22, 2026 21:59
e002a7a to
f3f3d7e
Compare
epompeii
force-pushed
the
u/ep/benchmark-parameters/bmf-v1
branch
from
August 23, 2026 03:21
f3f3d7e to
629abac
Compare
epompeii
force-pushed
the
u/ep/benchmark-parameters/bmf-v1
branch
from
August 23, 2026 05:22
629abac to
389e0e4
Compare
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.
… tree
An empty shape is a well formed payload that measured nothing. A benchmark
mapping to zero entries and an entry naming zero measures both parse without
error and yield no metric.
The entry that names no measure is the v1 spelling of BMF v0's
{"bench": {}}, so it is pinned against a v0 parse of that payload rather than
against a hand written expectation, and v0's own behavior is pinned alongside
it. That is the mirror the ingest layer above has to respect.
report_v1_canonical.json was only ever exercised through the v1 leaf. It
joins the node equality list and gains a node test and a magic test, which is
what every other v1 fixture already has. Its assertions move into
validate_adapter_json_v1_canonical so all three call sites share them.
The parameter set bounds live in the deserializer, so a v1 payload that breaks one is not a v1 payload. What matters at this layer is what happens next, because a tree of adapters that each fall back to the next has an obvious way to get this wrong: claim the payload at a lower version and drop the parameters on the way through. That is not what happens, and these tests pin it. For a payload carrying one key over the cap, the v0 leaf returns None, the v1 leaf returns None, and the json node returns None. Magic returns None too, since no other adapter recognizes the shape either. The report fails to parse and the run is rejected, which is the loud outcome rather than the quiet one. The leaf test walks every bound: over the key cap, a key and a string value one byte past MAX_LEN, and keys and values that are empty or untrimmed. It then walks the same bounds at their limits, so the test would catch a rule that is off by one in either direction.
epompeii
force-pushed
the
u/ep/benchmark-parameters/bmf-v1
branch
from
August 23, 2026 06:34
389e0e4 to
ecf605c
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