Repository navigation
Encode fixed-width builtin arrays in bulk - #2122
Merged
kevinherron merged 1 commit intoOct 12, 2026
Merged
Conversation
|
OpcUaBinaryEncoder wrote arrays of Boolean, the integer types, Float and Double one element at a time. Each element was a separate ByteBuf.writeXxxLE call, and each call checked bounds, the reference count and the capacity, and grew the buffer when it filled. A Read that returns a 921,600-element Int32 array made that call 921,600 times on the channel's event loop. In a profile of such Reads, element writes and buffer growth were most of the event loop's time, and one connection's responses could not be encoded faster than that thread. encodeBuiltinTypeArray now reserves the array's space once and converts its elements a block at a time into an 8 KiB little-endian heap buffer, which it copies into the target buffer. Primitive arrays such as int[] use the bulk put of the matching NIO view, byte[] is copied directly, and boxed arrays are converted element by element into the block. Copying a block works for any ByteBuf, including a CompositeByteBuf or one whose nioBuffer is a copy. The encoded bytes are unchanged. A null element of a boxed array is still written as zero, and Float and Double keep their raw bits, including NaN payloads. OpcUaBinaryEncoderArrayTest compares the bulk output with the per-element scalar encoders for every fixed-width type, in primitive and boxed form, over arrays that span several blocks.
kevinherron
force-pushed
the
perf/bulk-builtin-array-encoding
branch
from
October 12, 2026 18:15
18175a2 to
6ce9dfb
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.
Part 5 of 6 (#2118 to #2123) of a stack of server performance changes, found by profiling the Milo server in the o6-automation/opcua-benchmarks suite.
Problem
OpcUaBinaryEncoderwrote arrays of Boolean, the integer types, Float and Double one element at a time. Each element was a separateByteBuf.writeXxxLEcall, and each call checked bounds, the reference count and the capacity, and grew the buffer when it filled. A Read that returns a 921,600-element Int32 array (one HD video frame of values) made that call 921,600 times on the channel's event loop. In a profile of such Reads, element writes and buffer growth were most of the event loop's time, and one connection's responses could not be encoded faster than that thread.Change
encodeBuiltinTypeArraynow reserves the array's space once, then converts its elements a block at a time into an 8 KiB little-endian heap buffer, which it copies into the target buffer withwriteBytes:int[]use the bulkputof the matching NIO view;byte[]is copied directly.Integer[]are converted element by element into the block.ByteBuf. The encoder deliberately does not write throughByteBuf.nioBuffer, which Netty documents as either sharing or copying the buffer's content.The encoded bytes are unchanged. A null element of a boxed array is still written as zero, and Float and Double keep their raw bits, including NaN payloads. Variant arrays, Variant Matrices and
encodeMatrixuse the new path; the typedencodeXxxArraymethods used for structure fields still write element by element.One behavior differs at the edges: because the whole array is reserved up front, encoding into a
ByteBufwith a smallmaxCapacityfails as soon as the array's space is reserved, at an earlier writer index, with the sameIndexOutOfBoundsException. The transport encodes into an unrestricted pooled buffer and applies message and chunk limits afterwards, so those limits are unaffected.Results
Same benchmark setup as the rest of the stack, default configuration: the benchmark server stores the array as
Integer[], and no JVM or Netty options are set.Single-client HD Reads are bimodal on this host: each run lands near 165 or near 230 to 250 Reads/s. With this PR, half the runs land in the upper group (166 to 248, against 161 to 167 before), so its median understates the change. The 10-client runs do not overlap: 494 to 523 with this PR, against 434 to 468 before.
With the default configuration the gain is limited by what this PR does not touch. The rest of a large Read is buffer allocation in Netty (a 3.7 MB response is larger than a 4 MiB pool chunk once it grows, so it is a fresh direct allocation that the JDK zeroes) and the boxed
Integer[]itself.Tests
New
OpcUaBinaryEncoderArrayTest(stack-core) compares the bulk output with the per-element scalar encoders (encodeInt32,encodeFloat, and so on), byte for byte:These tests pass on the old encoder too, by design: they establish that the wire format did not change.
Verification
OpcUaBinaryEncoderArrayTest,OpcUaBinaryEncoderTest,VariantSerializationTest,DataValueSerializationTest: 134 tests pass. The full stack-core suite passes.Integer.MAX_VALUEelements; the loop now advances by the elements actually written. It also checked heap, direct, pooled, composite and swapped-endian buffers for byte equivalence.