codegen: store singular message fields inline by default (PointerRepr::Inline) (#248)#250
Merged
Conversation
Adds recursion-aware inline storage for singular message fields, mirroring unbox_oneof(): - buffa::Inline<T>: a repr(transparent) ProtoBox<T> newtype, so MessageField<T, Inline<T>> is laid out as Option<T> with no heap allocation. - PointerRepr::Inline: selects it. Recursion-aware — at context build, resolve_inlined_fields precomputes the set of singular fields where the raw repr is Inline and the inline-edge DFS finds no cycle; ctx.pointer_repr() demotes any Inline not in that set to Box. So box_type(PointerRepr::Inline) is also recursion-safe, and oneof variant paths (never in the set) auto-demote to Box so they can't bypass unbox_oneof's guard. - Config::unbox_message_fields[_in](): sugar for box_type_in(PointerRepr::Inline, paths) with leading-dot normalization. The recursion DFS (renamed inline_is_recursive) now follows both edge kinds — rule-matched oneof variants and raw-Inline singular fields — so a cycle through one of each is caught for both knobs. Default codegen output is byte-identical to main (PointerRepr defaults to Box; WKT regen confirms no diff).
|
All contributors have signed the CLA ✍️ ✅ |
TriMesh is a mesh-shaped workload (256 Face elements per payload, each with four singular Vertex submessages) — the case where boxed MessageField storage costs four heap allocations per element on decode. None of the existing benchmark messages has more than one singular message field per payload, so this is the regression target for PointerRepr::Inline (#248). - bench_messages.proto + iso/mesh.proto: TriMesh / Face / Vertex - gen-datasets: gen_mesh (appended last so existing dataset RNG state is unperturbed); 50x256-face dataset (~896KB) - bench-buffa: mesh feature, isolated bench target, protobuf.rs entries, inline_fields feature that calls .unbox_message_fields() in build.rs for A/B builds Iso-guard cfg lists in the existing per-message bench targets gain feature = "mesh" so isolation still holds.
…lds() Non-recursive singular message fields are now stored inline by default (MessageField<T, ::buffa::Inline<T>>, laid out as Option<T>). The recursion guard runs unconditionally so the default is always sized; recursive fields stay on Box automatically. The opt-out is the existing box_type_in(PointerRepr::Box, paths) (or box_type(PointerRepr::Box) for the old global default), so the unbox_message_fields[_in]() builders added in commit 1 are removed as redundant. box_type_in now normalizes a missing leading dot — previously a dotless path silently matched nothing. Fallout absorbed: - json_helpers::message_field_always_present and pool::clone_options genericized over P: ProtoBox<T> (the only two runtime helpers that hardcoded the Box pointer) - Bootstrap descriptor types regenerated (FileOptions/MessageOptions/etc. now inline). WKTs are byte-identical — none has a singular message field. - DESIGN.md / docs/guide.md / MessageField docs updated to describe the inline default - resolve_unboxed_variants and resolve_inlined_fields now take the prebuilt message index so it's built once per codegen, not twice The unconditional resolve_inlined_fields is O(F*(V+E)) — a fresh DFS per candidate field. Noted in the doc; memoize per-target reachable sets if a very large schema makes it noticeable.
rpb-ant
approved these changes
Jun 27, 2026
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 subscribe to this conversation on GitHub.
Already have an account?
Sign in.
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.
Closes #248.
Summary
Makes inline storage the default for singular message fields: codegen now emits
MessageField<T, ::buffa::Inline<T>>(laid out asOption<T>, no per-field heap allocation) for every non-recursive field. Recursive fields are detected at codegen time and stay onBoxautomatically, so the default is always sized.buffa::Inline<T>— a#[repr(transparent)]ProtoBox<T>newtype that storesTdirectly.PointerRepr::Inline— now#[default]. The recursion DFS (inline_is_recursive) follows both inline-edge kinds — rule-matchedunbox_oneofvariants andInline-resolved singular fields — so a cycle through one of each is caught for both knobs.ctx.pointer_repr()demotes anyInlinenot in the precomputed safe set toBox; oneof-variant paths are never in that set, so a blanketInlinerule can't bypassunbox_oneof's guard.box_type_in(PointerRepr::Box, &[".pkg.Msg.field"])(per field/prefix) orbox_type(PointerRepr::Box)(restore the old global default).box_type_innow normalizes a missing leading dot — previously a dotless path silently matched nothing.Benchmark validation
TriMeshis a new benchmark message (256Faceelements per payload, four singularVertexsubmessages each — the workload shape where boxed storage costs four heap allocations per element). On bare-metal (c7i.metal-24xl, 10s measurement), inline vs box:AnalyticsEventhas zero singular message fields (everything message-typed isrepeated), so its flat result confirms the change doesn't perturb messages it can't help.Fallout
json_helpers::message_field_always_presentandpool::clone_optionsgenericized overP: ProtoBox<T>— the only two runtime helpers that hardcoded theBoxpointer.*Optionsfields now inline). WKTs are byte-identical — none has a singular message field.DESIGN.md,docs/guide.md, and theMessageField/ProtoBoxdocs updated for the inline default.Breaking change
Generated singular message field types change from
MessageField<T>toMessageField<T, Inline<T>>. ExplicitMessageField<Foo>annotations now mean the boxed form and will mismatch — drop the annotation and letPinfer from the field's declared type. See the changelog entry for the full migration note.