`MessageFieldView` boxes every present nested message on decode, while owned `MessageField` defaults to inline storage

Open
#429 1 comment 1 reaction 0 assignees View on GitHub

Maintainers usually reply within 3 days

Nobody has claimed this yet.

Assessment

Difficulty
4/5
Estimated time
3-5 days
Newbie friendliness
68/100
Issue type
Feature
Clarity
Mostly clear
Activity status
Active
Tech stack
rust

Research direction

Start with MessageFieldViewV storage and the codegen logic that selects boxed storage for recursive owned fields, then compare the generated owned and view types. Run the small-unary benchmark in benches/rpc, especially BenchService/Unary with small_request(), to establish the allocation baseline. Done means nonrecursive present nested view fields avoid per-message boxing while recursive schemas retain the required boxed storage.

Written by the indexing model from the issue text.

Description

MessageFieldView<V> stores a set field as Option<Box<V>>, so decoding a view of a message allocates once for every nested message field that is present, however small the nested view is. The owned side made the opposite default: MessageField<T> is Inline<T> with recursive fields boxed automatically and box_type_in as the opt-out, and the ProtoBox docs note in passing that "view types are unaffected". So the zero-copy decode path, which is the one chosen for allocation-sensitive servers, pays a per-nested-message allocation that the owned path was redesigned to avoid.

Measured in connect-rust's small-unary gRPC benchmark (benches/rpc, BenchService/Unary with the ~300-byte small_request() payload: a BenchRequest holding a Payload with 15 scalar/string fields, a nested Metadata, two small maps, a packed repeated int32 and a repeated string), with a counting global allocator on the server: the request's decode_view makes 7 heap allocations, and 2 of them are MessageFieldView::set boxing the PayloadView (200 B) and the MetadataView (72 B). The other 5 are Vec growth in MapView::push (128 B then 256 B for a 4-entry map<string,string>, 128 B for a 2-entry one), RepeatedView<&str>::push (64 B) and extend_packed_int32 (20 B). For scale, the whole connect-rust request path around that decode (HTTP/2 framing, routing, response encoding) is about 20 allocations after connectrpc/connect-rust#296, so the view decode is a third of the framework-side total for this message shape.

The box exists to break the size recursion for self-referential messages, which is the same problem MessageField solves with a per-field storage parameter chosen by codegen (Inline by default, boxed where the codegen's cycle detection or a user option asks for it). Carrying that policy over, MessageFieldView<V, S = Inline<V>> with codegen emitting the boxed storage on exactly the fields where it already boxes the owned type, would make the common case allocation-free without changing what recursive schemas generate. The DefaultViewInstance / Deref behaviour is independent of where the value is stored.

The Vec-backed MapView / RepeatedView growth is a separate and smaller question (a first-push capacity larger than 1, or a small inline buffer, would remove most of it for short maps and lists); mentioning it here only because it is the rest of the same measurement.

Dominant language
Rust
Stars
927
Forks
119
Avg merge
3d 6h
Merged PRs (30d)
151

Getting set up

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from anthropics/buffa

All issues in anthropics/buffa

Similar issues

More Rust issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.