Repository navigation
feat: zero-copy views over encoded messages, part 1 - #1005
jasperpotts wants to merge 20 commits into
Conversation
Adds step 4 of docs/fast-codegen-plan.md (section 6.5), part 1, opt-in with pbj.views: a reusable, zero-copy View nested in each flagged model (for example Timestamp.View). - wrap(Bytes) / wrap(byte[], offset, length) index the message in one pass, with an in-order fast path and the same validation as parsing; numbers are decoded into primitive fields once, strings, bytes and nested messages are recorded as offsets and lengths; - accessors return defaults when a field is absent; strings and bytes also offer offset, length, array and equals accessors that do not allocate; nested messages return child views created once and re-wrapped; toModel() parses into the model; - ArrayCursor gains reset, array, skip and static decoders. Part 1 covers messages whose fields are all singular. Generated tests check every view accessor and toModel against the model, from canonical and reversed-order bytes; AllocationTest checks that wrap and reading every field allocate nothing; ViewBench compares a view with parsing. Fixes #1004 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Jasper Potts <1466205+jasperpotts@users.noreply.github.com>
Refs #1004 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Jasper Potts <1466205+jasperpotts@users.noreply.github.com>
|
Merged in the review follow-ups from below in the stack: exact-size bulk varint writes (#994), unique writer local names (#995), dead-code removal (#996), map entries without try/finally plus tests (#997), and strict string decoding without per-call allocations (#1010). On #999 the array parser's |
Signed-off-by: Jasper Potts <1466205+jasperpotts@users.noreply.github.com>
Views indexed strings like bytes and decoded them leniently, so malformed UTF-8 that parsing rejects read back as U+FFFD. wrap now checks each string with the same validator as parsing (ArrayCursor.stringLength), and the generated view tests check every string field rejects a 0xFF byte. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Jasper Potts <1466205+jasperpotts@users.noreply.github.com>
Signed-off-by: Jasper Potts <1466205+jasperpotts@users.noreply.github.com>
Signed-off-by: Jasper Potts <1466205+jasperpotts@users.noreply.github.com>
Signed-off-by: Jasper Potts <1466205+jasperpotts@users.noreply.github.com>
|
Review follow-up (second review, 1.2): views now check The other view items (limits and strict mode, child view reuse, cycles, the |
Signed-off-by: Jasper Potts <1466205+jasperpotts@users.noreply.github.com>
Signed-off-by: Jasper Potts <1466205+jasperpotts@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Jasper Potts <1466205+jasperpotts@users.noreply.github.com>
Signed-off-by: Jasper Potts <1466205+jasperpotts@users.noreply.github.com>
Signed-off-by: Jasper Potts <1466205+jasperpotts@users.noreply.github.com>
Signed-off-by: Jasper Potts <1466205+jasperpotts@users.noreply.github.com>
On this branch ArrayCursor knows the Bytes it reads, so the cursor for the input Bytes.toReadableSequentialData() returns now carries that Bytes too, and bytes fields are views of it, as the stream parser over Bytes returns them, rather than copies. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Jasper Potts <1466205+jasperpotts@users.noreply.github.com>
Signed-off-by: Jasper Potts <1466205+jasperpotts@users.noreply.github.com>
Signed-off-by: Jasper Potts <1466205+jasperpotts@users.noreply.github.com>
Signed-off-by: Jasper Potts <1466205+jasperpotts@users.noreply.github.com>
What and why
This PR is step 4 of
docs/fast-codegen-plan.md(section 6.5), part 1, opt-in withpbj.views. It adds reusable, zero-copy views that read fields in place from encoded bytes, with no model objects and no allocation, for routing, filtering and hashing block items.Performance (AMD EPYC 9275F): reading every field of a flat message through a view is 1.6× faster than parsing it (26.5 → 16.4 ns) and allocates 0 B instead of 152 B. Tier B, opt-in.
Xxx.View, nested in each flagged model (for exampleMixedFastInner.View). The plan names the classXxxView; nesting it means nested messages get nested views, and no new generated file type is needed.wrap(Bytes)andwrap(byte[], offset, length)index the message in one pass. They use an in-order fast path, then a general loop with the same validation as parsing: tags, wire types, lengths,maxSize, bools,maxDepth, and unknown fields skipped. Numbers are decoded into primitive fields once. Strings, bytes and nested messages are recorded as offsets and lengths. The view reuses its ownArrayCursor.xProtoOrdinal()andx().xOffset(),xLength(),xArray()andxEquals(byte[]), none of which allocate, plusx(), which returns aStringor aBytesview.hasX()andx(), which returns a child view created once and re-wrapped on each call, or null when absent.toModel()parses the wrapped message into the model.ArrayCursorgainsreset(…),array(), a publicskip, and staticvarintAt/fixed32At/fixed64At.-PpbjPerformance=max. Part 2 adds repeated fields (reusable cursors), maps, oneofs (xKind()) and wrapper types, which is whatBlockItemneeds.Also in this PR, a design change after measuring. The first version decoded each number on every accessor call and indexed with the general loop only. On EPYC it was slower than parsing (31.9 vs 26.2 ns) for a flat message. Decoding once in
wrapand adding the in-order fast path fixed it; numbers below.Generated code:
MixedFastInner.View(excerpt)Tests
test<Message>Viewfor every message with a view. For every test object it checks each accessor andtoModel()against the model. It does this from canonical bytes, then re-wraps the same view over reversed-order bytes with an unknown field and checks again.AllocationTest: wrapping a view and reading every field allocates 0 bytes.pbj-core:spotlessApply qualityGate buildpasses.pbj-integration-tests:./gradlew buildpasses, 326,416 tests;-PpbjPerformance=maxpasses, 329,662 tests, including the view tests for 234 messages.GrpcCloseClientConnectionTest, see perf: generate straight-line byte array writers with direct nested calls #995); the rerun passed.Performance: AMD EPYC 9275F,
ViewBench, 3 forks,-prof gcBlockItemBenchread-every-field) needs part 2's oneof and repeated-field support.Generated code size (full HAPI build,
-PpbjPerformance=max)With options off, generated main code is unchanged (the test proto flags
MixedFastInnerwithpbj.views).Stacked on #1003.
Fixes #1004
🤖 Generated with Claude Code