Before Creating the Enhancement Request
Summary
Replace per-message HashMap allocation in the properties encode/decode hot path with a compact FlatPropertiesMap backed by a flat Object[] array. Add ThreadLocal reuse for the map, StringBuilder, and char[] buffers. Intern high-frequency property keys and values to eliminate redundant String allocation.
Scope: 8 files in common/message
Motivation
Heap dump analysis reveals:
HashMap$Node[] occupies 21.0% of live heap — every message creates multiple HashMaps for properties parsing.
String occupies 8.1% with only 21,873 distinct values out of 100,000 sampled — "true" duplicated 7,142 times, "UNIQ_KEY" 4,059 times, etc.
byte[] occupies 29.3% — partly from backing arrays of duplicated small Strings.
The string2messageProperties / messageProperties2String path is the single largest allocation hotspot on the broker send path.
Describe the Solution You'd Like
FlatPropertiesMap (new class): A compact Map<String, String> implementation using a flat Object[] (key-value pairs). Supports reset() for ThreadLocal reuse, computeEncodedLength(), and encodeTo(ByteBuffer) for zero-copy serialization.
MessageDecoder: New bytes2messageProperties(ByteBuffer) method that parses properties byte-by-byte without new String() + split(). Uses REUSABLE_PROPS_MAP ThreadLocal for map reuse and REUSABLE_SB ThreadLocal for StringBuilder reuse.
MessageConst: Add STRING_INTERN_BY_LEN — length-bucketed arrays for key interning. Known property keys (e.g., KEYS, TAGS, WAIT, UNIQ_KEY) are resolved to static constants by length + first-char matching.
MessageDecoder: Add VALUE_INTERN_BY_LEN for common value interning ("true", "false", "DefaultRegion", etc.).
MessageClientIDSetter: ThreadLocal char[] buffer for createUniqID to avoid per-call allocation.
MessageVersion: Replace values() iteration with direct if-else lookup.
Describe Alternatives You've Considered
String.intern() (JVM native): Risks polluting the JVM String table and has unpredictable GC interaction; length-bucketed array interning is deterministic and bounded.
- Keep
HashMap but pool it: Still pays the overhead of HashMap.put() internal Node allocation; FlatPropertiesMap avoids this entirely for the typical 8–12 property case.
- Protocol-level binary properties (like Pulsar): Would require wire format changes; out of scope for a backward-compatible enhancement.
Additional Context
FlatPropertiesMap is only used on the broker-internal decode→encode path within a single thread; it does not escape to user-facing APIs.
- The send-path
string2messageProperties in MessageDecoder returns HashMap (not FlatPropertiesMap) to maintain compatibility with downstream code that expects HashMap.
- JFR-measured bytes/msg reduction: -42.7% from this change alone (from 4,594 to 2,631 bytes/msg).
Before Creating the Enhancement Request
Summary
Replace per-message
HashMapallocation in the properties encode/decode hot path with a compactFlatPropertiesMapbacked by a flatObject[]array. Add ThreadLocal reuse for the map, StringBuilder, and char[] buffers. Intern high-frequency property keys and values to eliminate redundant String allocation.Scope: 8 files in
common/messageMotivation
Heap dump analysis reveals:
HashMap$Node[]occupies 21.0% of live heap — every message creates multiple HashMaps for properties parsing.Stringoccupies 8.1% with only 21,873 distinct values out of 100,000 sampled —"true"duplicated 7,142 times,"UNIQ_KEY"4,059 times, etc.byte[]occupies 29.3% — partly from backing arrays of duplicated small Strings.The
string2messageProperties/messageProperties2Stringpath is the single largest allocation hotspot on the broker send path.Describe the Solution You'd Like
FlatPropertiesMap(new class): A compactMap<String, String>implementation using a flatObject[](key-value pairs). Supportsreset()for ThreadLocal reuse,computeEncodedLength(), andencodeTo(ByteBuffer)for zero-copy serialization.MessageDecoder: Newbytes2messageProperties(ByteBuffer)method that parses properties byte-by-byte withoutnew String()+split(). UsesREUSABLE_PROPS_MAPThreadLocal for map reuse andREUSABLE_SBThreadLocal for StringBuilder reuse.MessageConst: AddSTRING_INTERN_BY_LEN— length-bucketed arrays for key interning. Known property keys (e.g.,KEYS,TAGS,WAIT,UNIQ_KEY) are resolved to static constants by length + first-char matching.MessageDecoder: AddVALUE_INTERN_BY_LENfor common value interning ("true","false","DefaultRegion", etc.).MessageClientIDSetter: ThreadLocalchar[]buffer forcreateUniqIDto avoid per-call allocation.MessageVersion: Replacevalues()iteration with directif-elselookup.Describe Alternatives You've Considered
String.intern()(JVM native): Risks polluting the JVM String table and has unpredictable GC interaction; length-bucketed array interning is deterministic and bounded.HashMapbut pool it: Still pays the overhead ofHashMap.put()internal Node allocation;FlatPropertiesMapavoids this entirely for the typical 8–12 property case.Additional Context
FlatPropertiesMapis only used on the broker-internal decode→encode path within a single thread; it does not escape to user-facing APIs.string2messagePropertiesinMessageDecoderreturnsHashMap(notFlatPropertiesMap) to maintain compatibility with downstream code that expectsHashMap.