Before Creating the Enhancement Request
Summary
Optimize MessageDecoder.encodeMessages(List<Message>) so that a batch is encoded directly into one exactly sized output buffer instead of allocating a full encoded byte[] for every message and then copying all of those arrays into a second final buffer.
The proposed implementation preserves the existing public API and produces byte-for-byte identical wire data.
Motivation
encodeMessages is used in the producer batch path, including automatic message batching. Its current implementation performs the following work for a batch of N messages:
- call
encodeMessage and allocate one complete encoded array for each message;
- retain all
N arrays in a list;
- allocate the final aggregate array;
- copy every encoded array into the aggregate array.
That creates avoidable allocation pressure and memory bandwidth on a producer hot path. A repeated JMH 1.36 benchmark measured both throughput and normalized allocation. Its baseline() method reconstructs the previous aggregation algorithm in the same benchmark binary: encode each message fully, retain the arrays, then copy them into the final output.
Benchmark controls:
- one thread pinned to CPU 5;
- 3 forks, 5 warm-up iterations and 7 measurement iterations per fork;
- each iteration 500 ms;
-Xms512m -Xmx512m;
- JMH GC profiler;
- OpenJDK 11.0.31 on Ubuntu 22.04.4 / Intel Xeon Gold 6133.
Each throughput score is the mean plus or minus its JMH 99.9% confidence-interval half-width.
| Scenario |
Reconstructed baseline ops/s |
Proposed ops/s |
Throughput |
Baseline B/op |
Proposed B/op |
Allocation |
| 10 messages x 128 B |
340,743 +/- 6,348 |
381,842 +/- 9,236 |
+12.06% |
6,760 |
4,552 |
-32.66% |
| 100 messages x 128 B |
32,247 +/- 1,099 |
36,246 +/- 242 |
+12.40% |
67,152 |
45,192 |
-32.70% |
| 10 messages x 1 KiB |
179,223 +/- 3,518 |
240,718 +/- 10,326 |
+34.31% |
24,680 |
13,512 |
-45.25% |
| 100 messages x 1 KiB |
16,954 +/- 594 |
22,411 +/- 594 |
+32.19% |
246,352 |
134,792 |
-45.28% |
The reconstructed baseline and proposed 99.9% throughput confidence intervals do not overlap in any of the four scenarios.
The representative 100-message x 1 KiB scenario was also repeated on OpenJDK 8u492:
- throughput: 14,472.346 +/- 329.526 to 18,954.609 +/- 113.257 ops/s (+30.97%);
- allocation: 288,568 to 173,832 B/op (-39.76%);
- the throughput 99.9% confidence intervals are disjoint.
To independently verify the reconstructed baseline, I also ran the actual production encodeMessages implementation from separate baseline and optimized checkouts in A-B-A order for the 100-message x 1 KiB scenario:
| Production checkout |
Throughput ops/s |
99.9% CI |
Allocation B/op |
| Baseline A1 |
17,352.526 +/- 400.418 |
[16,952.108, 17,752.943] |
246,352.044 |
| Optimized B |
22,600.219 +/- 880.650 |
[21,719.569, 23,480.870] |
134,792.035 |
| Baseline A2 |
16,876.707 +/- 671.661 |
[16,205.046, 17,548.368] |
246,352.045 |
The optimized production checkout is 30.24% faster than A1 and 33.91% faster than A2, while allocating 45.28% less. Its 99.9% confidence interval does not overlap either baseline run.
Describe the Solution You'd Like
Use two passes without constructing temporary full-message encodings:
- For every message, retain the body reference, serialize its properties exactly once, cache those property bytes, and calculate its encoded size.
- Allocate one
ByteBuffer for the exact aggregate size.
- Write each message's total size, magic code, CRC placeholder, flag, body and properties directly into that shared buffer.
The single-message encodeMessage method can use the same internal size and field-writing helpers, keeping the wire layout in one implementation.
Add a compatibility test which asserts that batch encoding is byte-for-byte equal to concatenating the existing single-message encoding. The test should include empty and large bodies, Unicode and multiple properties, edge flag values, and an empty batch. Add a fixed golden-vector test as an independent oracle for the wire field layout, because the single-message and batch paths share the refactored helper.
Describe Alternatives You've Considered
- Keep the current per-message arrays and final copy: simplest, but retains the allocation and copy cost shown above.
- Use a dynamically growing output buffer: avoids the size pass but introduces capacity growth, over-allocation, and potentially more copies.
- Calculate sizes first but serialize properties again while writing: avoids full-message arrays but duplicates property serialization work. Caching the serialized properties keeps the two-pass design deterministic and avoids that extra CPU cost.
Additional Context
This is a focused follow-up to #2931 and the unmerged PR #2932, which had a similar copy-reduction motivation in 2021 but bundled unrelated broker, store and topic changes and later became stale. The proposed production change is limited to MessageDecoder, adds wire-compatibility coverage, serializes properties once, and includes multi-fork benchmark evidence.
Active issue #10442 and PR #10444 optimize property encoding/decoding allocation in MessageDecoder, but do not change encodeMessages or eliminate its per-message full buffers and final copies. PR #10588 concerns propagation of batch message properties. Both are conceptually separate from this optimization.
The full common module test suite passes with 245 tests and no failures, errors or skips. Three downstream store batch suites pass 8/8 tests, and the producer accumulator suite passes 3/3 tests on this performance branch.
Benchmark source, methodology, checksums, and raw per-fork/per-iteration results: https://gist.github.com/ai-yang/c3e3f65351054263dec6f7b8d67ad3cd
Before Creating the Enhancement Request
Summary
Optimize
MessageDecoder.encodeMessages(List<Message>)so that a batch is encoded directly into one exactly sized output buffer instead of allocating a full encodedbyte[]for every message and then copying all of those arrays into a second final buffer.The proposed implementation preserves the existing public API and produces byte-for-byte identical wire data.
Motivation
encodeMessagesis used in the producer batch path, including automatic message batching. Its current implementation performs the following work for a batch ofNmessages:encodeMessageand allocate one complete encoded array for each message;Narrays in a list;That creates avoidable allocation pressure and memory bandwidth on a producer hot path. A repeated JMH 1.36 benchmark measured both throughput and normalized allocation. Its
baseline()method reconstructs the previous aggregation algorithm in the same benchmark binary: encode each message fully, retain the arrays, then copy them into the final output.Benchmark controls:
-Xms512m -Xmx512m;Each throughput score is the mean plus or minus its JMH 99.9% confidence-interval half-width.
The reconstructed baseline and proposed 99.9% throughput confidence intervals do not overlap in any of the four scenarios.
The representative 100-message x 1 KiB scenario was also repeated on OpenJDK 8u492:
To independently verify the reconstructed baseline, I also ran the actual production
encodeMessagesimplementation from separate baseline and optimized checkouts in A-B-A order for the 100-message x 1 KiB scenario:The optimized production checkout is 30.24% faster than A1 and 33.91% faster than A2, while allocating 45.28% less. Its 99.9% confidence interval does not overlap either baseline run.
Describe the Solution You'd Like
Use two passes without constructing temporary full-message encodings:
ByteBufferfor the exact aggregate size.The single-message
encodeMessagemethod can use the same internal size and field-writing helpers, keeping the wire layout in one implementation.Add a compatibility test which asserts that batch encoding is byte-for-byte equal to concatenating the existing single-message encoding. The test should include empty and large bodies, Unicode and multiple properties, edge flag values, and an empty batch. Add a fixed golden-vector test as an independent oracle for the wire field layout, because the single-message and batch paths share the refactored helper.
Describe Alternatives You've Considered
Additional Context
This is a focused follow-up to #2931 and the unmerged PR #2932, which had a similar copy-reduction motivation in 2021 but bundled unrelated broker, store and topic changes and later became stale. The proposed production change is limited to
MessageDecoder, adds wire-compatibility coverage, serializes properties once, and includes multi-fork benchmark evidence.Active issue #10442 and PR #10444 optimize property encoding/decoding allocation in
MessageDecoder, but do not changeencodeMessagesor eliminate its per-message full buffers and final copies. PR #10588 concerns propagation of batch message properties. Both are conceptually separate from this optimization.The full
commonmodule test suite passes with 245 tests and no failures, errors or skips. Three downstream store batch suites pass 8/8 tests, and the producer accumulator suite passes 3/3 tests on this performance branch.Benchmark source, methodology, checksums, and raw per-fork/per-iteration results: https://gist.github.com/ai-yang/c3e3f65351054263dec6f7b8d67ad3cd