Repository navigation
test: synthetic HAPI block workload, benchmarks and reference codec - #1028
Open
jasperpotts wants to merge 3 commits into
Open
jasperpotts wants to merge 3 commits into
jasperpotts wants to merge 3 commits into
Conversation
…odec A deterministic block of 20,000 transactions with the structure and value sizes of an external block-stream workload, built only from HAPI messages (`SyntheticBlock`), with its shape asserted in `SyntheticBlockTest`. Model objects, the reverse writer, the generated writers and a benchmark-only hand-written `ReferenceCodec` all produce it from the same flat values, byte for byte; read-every-field visitors over objects (`ScanObjects`), views (`ScanViews`) and the reference codec return the same sums. `SyntheticBlockBench` measures encode, produce, decode, read-every-field and transaction decode, per transaction. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Jasper Potts <1466205+jasperpotts@users.noreply.github.com>
This was referenced Oct 31, 2026
The reference now uses the techniques of the external hand-written codec (fast-codegen plan section 5): back-to-front writing into a buffer it owns with single-store varints, no scratch copies or sizing pass; in-order reading with expected-tag checks first and the tag switch as fallback, and PBJ's XOR-chain varint with the one-byte case inlined. referenceProduce writes each item without copying it to offset 0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Jasper Potts <1466205+jasperpotts@users.noreply.github.com>
This was referenced Oct 31, 2026
Signed-off-by: Jasper Potts <1466205+jasperpotts@users.noreply.github.com>
jasperpotts
force-pushed
the
1018-flyweight-writers
branch
from
November 2, 2026 20:33
39b6231 to
b42070f
Compare
jasperpotts
force-pushed
the
1027-synthetic-block-bench
branch
from
November 2, 2026 20:33
a0a4673 to
2fe51fe
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.
Closes #1027. Stacked on #1023 (
1018-flyweight-writers).Summary
A synthetic HAPI block with the structure and value sizes of the external block-stream workload, so its results can be reproduced and decided in this repo. Plus benchmarks, read-every-field visitors and a hand-written reference codec, the in-repo target. All of it is test and benchmark code (
src/testMax,src/jmhMax); nothing ships.Performance (AMD EPYC 9275F): this PR adds the workload and changes no library code. On it, the whole stack against 0.16.4 (final run, ns per transaction): block decode 3,002 → 1,353 ns (2.2×), block encode 3,389 → 1,410 ns (2.4×), transaction decode 1,202 → 148 ns (8.1×); writers encode with 0 B at 1.5× the reference codec, and views read every field with 0 B at about 3×. Full table: #1023 (comment)
SyntheticBlock: a deterministic generator (fixed seed) of 20,000 transactions, using only HAPI messages.build), the generated writers (write) and the reference codec all read in the same order.transactions(): 20,000SignedTransactions with one signature pair each (80% ECDSA secp256k1, 20% ed25519), crypto-transfer bodies, and 12% atomic batches of 2-20 inner transactions.ScanObjects/ScanViews: read every field of the subset's ~25 message types, set or not, through objects or through views, and return the same sum. Numbers, booleans, enum ordinals and oneof field numbers are added; bytes contribute their length and first and last byte.ReferenceCodec: a benchmark-only, hand-written, zero-allocation codec for the subset, as fast as I could make it, so it is a real target.byte[], with an in-order fast path per message and a switch for anything else, and a branch-light varint reader.SyntheticBlockTest,ReferenceCodecTest:writeReverse, item writers, the whole-block writer and the reference encoder write the same bytes for every item and for the whole block.SyntheticBlockBench: per transaction, with-prof gc; listed in the table below.The shape, and why bytes are higher than the external 680 B
The extra bytes come from the same fields and values: HAPI wraps every id in a message.
AccountID,TokenIDandNftIDcost about 3 more bytes each than a bare varint, and a nestedTimestampwith varint nanos costs about 4.5 more than afixed64. A transaction's state changes carry about 36 ids and 4.5 timestamps, about 127 bytes. The test asserts 680 + 127 and 316 + 127.EPYC results
AMD EPYC 9275F, JDK 25.0.4, pinned to one CCD (
taskset -c 0-2,24-26) of an idle machine,-Xms16g -Xmn12g. 3 forks × 5 iterations of 2 s,-prof gc. Every value is per transaction (block operations ÷ 20,000). PBJ is this PR's head with every option on (-PpbjPerformance=max, writers on).encodeFresh: fresh objects,write(byte[])encodeFreshToBytesencodeReverseFreshproduceObjectsToBytes: values → objects →toBytesproduceObjectsReverse: values → objects →writeReverseproduceWriters: values → item writersproduceWritersBlockHashed:Block.Writer, SHA-384 of each itemreferenceProducedecode:Block.PROTOBUF.parse(Bytes)referenceDecodescanObjects: parse, then read every fieldscanViews: read every field through viewsreferenceScantxDecode:SignedTransaction+TransactionBodytxScanObjectstxScanViewsreferenceTxDecodereferenceTxScan* The
encode…Freshallocation includes building the objects in@Setup(Level.Invocation), which isn't timed.Validation against the external ratios (±20%)
decode÷referenceDecode)writeReversescanViews÷referenceScan)Three of the four validate. With the reference encoder now writing back to front, writers against it validate; against the earlier forward encoder they were 1.28×. The views ratio now misses. The reference reader got faster, and HAPI has more small nested messages per byte than the external format, because every id and timestamp is a message: that per-message cost is exactly what views pay. I didn't change the composition to make it fit, so read the view ratios here as an upper bound on the external gap.
What the numbers already say
scanViewsalso allocates 22 B per transaction, all from the boxedLongthat a wrapper view getter (SingletonUpdateChange.View.entityNumberValue()) returns.bodyBytesis copied to a scratch array before the body's view reads it.writeReverse, and 1.79× the reference encoder. Decode is 1.46×, with more than twice the reference's allocation.Running it
🤖 Generated with Claude Code