Running the protobuf decode benchmark
crates/twg-codec-protobuf/benches/decode.rs is the measurement gating ADR-0046.
It does not run today — every arm is a todo!(), because there is nothing
behind it yet. This is the runbook for getting from here to a number.
Do this first: check the premise, outside the workspace
Before implementing anything, answer the question the whole plan rests on:
does a zero-copy descriptor-driven parser actually beat prost-reflect by
enough, on schemas like yours, to justify vendoring and maintaining a
divergence?
That needs none of our code. In a scratch crate:
cargo new --lib pbbench && cd pbbench
# dev-deps: criterion, prost, prost-types, prost-reflect,
# and the upstream SDK with its parser feature enabled
protoc --include_imports \
--descriptor_set_out=wide.pb \
-I ../thalweg/crates/twg-codec-protobuf/proto \
../thalweg/crates/twg-codec-protobuf/proto/wide_128.proto
cargo bench
Decode the same payload both ways and compare. An afternoon’s work that de-risks weeks: if zero-copy does not win decisively on a 128-field schema, the vendoring plan changes and you have saved the implementation.
Use wide_128.proto. A narrow schema flatters reflection-based decoding and will
understate the gap enough to talk you out of the right answer.
What the in-repo benchmark needs, in order
- Dev-dependencies in
crates/twg-codec-protobuf/Cargo.toml:criterion,prost,prost-types,prost-reflect, and the upstream SDK behind its parser feature. The layering gate exempts[dev-dependencies]by design — a vendor SDK there is legitimate and ships to nobody; the same crate in[dependencies]would fail CI, correctly. - Compiled descriptors. The
.protofixtures exist inproto/. A build script or a committed artefact must turn them into aFileDescriptorSet:protoc --include_imports --descriptor_set_out=schemas.pb -I proto proto/*.proto--include_importsis not optional — without it the well-known types are absent andwellknown.protofails to resolve, which is the very failure mode being tested. - Payload generation at the three benchmark sizes, deterministic and seeded so runs are comparable across machines and over time.
- The four decode paths, replacing the
todo!()s:prost-reflect(the floor), the upstream parser (the baseline to beat), and our two candidates — parse-then-walk and fused-into-builders.
Running it
cargo bench -p twg-codec-protobuf
cargo bench -p twg-codec-protobuf -- decode/wide_128 # the deciding case
Criterion writes to target/criterion/. Commit baselines once the numbers are
trusted, and wire a CI failure on regression beyond a stated tolerance — a
benchmark that gates a decision belongs in CI, not in a document
(docs/testing/TESTING.md).
Reading the result
Two independent questions, deliberately measured separately:
Is vendoring worth it? Compare the upstream parser against prost-reflect on
wide_128. If the margin is large, vendoring is justified; if not, take
prost-reflect and drop the whole plan.
Is fusing worth it? Compare ours_fused against ours_parse_then_walk. If
fusing does not beat walking by a real margin, drop the fusion and keep the
simpler path — vendoring can still be right while fusion is not.
Bear in mind that fusing cannot eliminate per-record staging: protobuf fields arrive in arbitrary order and repeated fields interleave, so absent fields are only known at end of record. The upside is bounded, which is exactly why it is measured rather than assumed.