Skip to content

Encode fixed-width builtin arrays in bulk - #2122

Merged
kevinherron merged 1 commit into
perf/read-access-check-loopsfrom
perf/bulk-builtin-array-encoding
Oct 12, 2026
Merged

kevinherron merged 1 commit into
perf/read-access-check-loopsfrom
perf/bulk-builtin-array-encoding

Conversation

@kevinherron

@kevinherron kevinherron commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

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

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 (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

encodeBuiltinTypeArray now 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 with writeBytes:

  • Primitive arrays such as int[] use the bulk put of the matching NIO view; byte[] is copied directly.
  • Boxed arrays such as Integer[] are converted element by element into the block.
  • Copying a block works for any ByteBuf. The encoder deliberately does not write through ByteBuf.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 encodeMatrix use the new path; the typed encodeXxxArray methods 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 ByteBuf with a small maxCapacity fails as soon as the array's space is reserved, at an earlier writer index, with the same IndexOutOfBoundsException. 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.

Workload 1.2 Before this PR With this PR Change
async, 1 client, HD array read 164 164 200 +22%
async, 10 clients, HD array read 456 459 511 +11%
async, 10 clients, batch of 100 read 57,564 129,422 129,224 0%
async, 10 clients, scalar read 540,578 624,958 639,982 +2%

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:

  • every fixed-width type, in primitive and boxed form, over 10,000-element arrays that span several blocks, including extreme values, -0.0, NaN payloads, and UInt64 values at and above 2^63;
  • arrays of length 0, 1, and one less, equal to, and one more than a block, and two blocks, for Int32 and Boolean;
  • boxed arrays with null elements;
  • a Matrix.

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.
  • Full build with tests on the top of the stack: 5,792 tests, no failures, 4 skipped.
  • Reviewed independently by GPT-6.1-Sol (Codex) in two rounds. It found that the block loop's index could overflow after the last partial block of a byte-sized array near Integer.MAX_VALUE elements; the loop now advances by the elements actually written. It also checked heap, direct, pooled, composite and swapped-endian buffers for byte equivalence.

@kevinherron
kevinherron added this pull request to stack #2124 October 12, 2026 15:28
@greptile-apps

greptile-apps Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

[Medium impact] The PR appears safe to merge; no actionable issues were found.

Summary

Fixed-width arrays now use bulk writes in OpcUaBinaryEncoder.

  • Fixed-width arrays are written to the output in blocks.

No actionable issues were found. Tests were inspected, not run.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Array or Matrix elements] --> B{Primitive byte array?}
  B -->|Yes| C[Copy directly to output]
  B -->|No| D[Reserve output space]
  D --> E[Convert next block in little-endian order]
  E --> F[Copy exact block bytes to output]
  F --> G{Elements remain?}
  G -->|Yes| E
  G -->|No| H[Finish array encoding]
  C --> H
Loading

Reviews (1) · Last reviewed commit: "Encode fixed-width builtin arrays in bul..." · Reviewed by Greptile

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
kevinherron force-pushed the perf/bulk-builtin-array-encoding branch from 18175a2 to 6ce9dfb Compare October 12, 2026 18:15
@kevinherron
kevinherron merged commit 9da7adc into integration/1.2 Oct 12, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant