qianye1001 opened a new issue, #11003:
URL: https://github.com/apache/rocketmq/issues/11003

   ### Before Creating the Enhancement Request
   
   - [X] I have confirmed that this should be classified as an enhancement 
rather than a bug/feature.
   
   ### Summary
   
   Replace Guava `MoreObjects.toStringHelper` with a pre-sized `StringBuilder` 
in the `toString()` of the high-frequency request header classes under 
`remoting/src/main/java/org/apache/rocketmq/remoting/protocol/header/`. The 
rendered output stays byte-for-byte identical; the change removes roughly N+3 
short-lived allocations and an O(N) linked-list traversal per call for a header 
with N fields.
   
   Scope (14 send / consume / ack / offset hot-path headers): 
`SendMessageRequestHeader`, `SendMessageRequestHeaderV2`, 
`PopMessageRequestHeader`, `PopLiteMessageRequestHeader`, 
`PullMessageRequestHeader`, `NotificationRequestHeader`, 
`AckMessageRequestHeader`, `ChangeInvisibleTimeRequestHeader`, 
`QueryConsumerOffsetRequestHeader`, `UpdateConsumerOffsetRequestHeader`, 
`GetMaxOffsetRequestHeader`, `GetMinOffsetRequestHeader`, 
`ConsumerSendMsgBackRequestHeader`, `RecallMessageRequestHeader`.
   
   Measured on the module's current build target (JDK 8 / `target 1.8`): 
**1.62x - 2.60x faster `toString()` and 32% - 48% less allocation per call**.
   
   ### Motivation
   
   These headers render themselves with `MoreObjects.toStringHelper`, e.g. 
`SendMessageRequestHeader`:
   
   ```java
   return MoreObjects.toStringHelper(this)
       .add("producerGroup", producerGroup)
       .add("topic", topic)
       ... // 13 fields in total
       .toString();
   ```
   
   Per invocation this allocates, before the resulting `String` is even built:
   
   1. one `ToStringHelper` object;
   2. one `ValueHolder` head plus **one `ValueHolder` node per `.add(...)`**, 
forming a singly linked list;
   3. `getClass().getSimpleName()`, which itself allocates (`getName()` + 
`substring`);
   4. an internal `StringBuilder(32)` that grows through repeated 
`Arrays.copyOf` whenever the rendering exceeds 32 characters;
   
   and then traverses that linked list to render. For a header with N fields 
this is about **N + 3 short-lived objects plus an O(N) traversal per call**.
   
   `toString()` on these headers sits on hot paths: it is invoked whenever a 
header is rendered for logging or diagnostics (request logging, exception 
messages, troubleshooting output). At high message throughput this becomes a 
continuous stream of short-lived garbage and a measurable amount of CPU spent 
purely on rendering, which also shows up as extra young-generation collection 
work.
   
   ### Describe the Solution You'd Like
   
   Use a **pre-sized `StringBuilder`** with direct `append` calls:
   
   - **Plain headers** - a single chained `append` sequence. The initial 
capacity is derived from the class's fixed skeleton (class name + field names + 
`=` / `, ` separators) plus an allowance for variable-length values such as 
`properties`, `subscription` and `extraInfo`. Pre-sizing matters here: a 
default-capacity builder needs 5-6 growth-and-copy steps to reach a ~350 
character rendering.
   - **Headers using `.omitNullValues()`** (`AckMessageRequestHeader`, 
`ChangeInvisibleTimeRequestHeader`, `NotificationRequestHeader`) - conditional 
appends that skip null values, preserving the exact omission semantics. 
Primitive-typed fields are always emitted, which matches today's behaviour 
since they autobox to a non-null value.
   - **Conditional entries** such as `.add("isLiteConsumer", isLiteConsumer ? 
true : null)` in `NotificationRequestHeader` are preserved by evaluating the 
expression into a local and null-checking it, so the entry is still rendered 
only when `true`.
   
   The rendered strings must remain **byte-for-byte identical**, because they 
end up in logs that people and external tooling parse.
   
   ### Describe Alternatives You've Considered
   
   **1. Plain `+` string concatenation.** More concise, and on a JDK 9+ 
bytecode target it compiles to `invokedynamic` / 
`StringConcatFactory.makeConcatWithConstants`, which beats anything 
hand-written - measured **4.53x** and **-73%** allocation on JDK 21.
   
   However this module builds with `maven.compiler.source/target = 1.8`, where 
`javac` lowers `+` to `new StringBuilder()` with the default capacity of 16. 
For headers whose rendering runs to a few hundred characters (those carrying 
`properties` / `subscription` / `extraInfo`) that means 5-6 growth-and-copy 
steps, which cancels out most of the benefit of dropping Guava. Measured for 
`SendMessageRequestHeader` at `target 1.8`:
   
   | form | alloc/call | throughput vs Guava |
   |---|---|---|
   | `+` concatenation | 3664 B (-2.6%) | 1.27x |
   | pre-sized `StringBuilder` | 1944 B (-48.3%) | **2.07x** |
   
   So plain concatenation was rejected for now: it is the worse option under 
the project's current build target. The pre-sized builder also stays good on 
newer targets (1.95x on JDK 21), so it does not become a liability. **If the 
project later raises the bytecode target, switching these to plain `+` 
concatenation would be a worthwhile follow-up.**
   
   **2. Removing or gating the `toString()` calls at the call sites.** Out of 
scope: it changes observable logging behaviour rather than making the existing 
rendering cheaper.
   
   **3. Also converting the remaining ~10 header classes that use 
`toStringHelper`.** Left out deliberately - those are admin / low-frequency 
headers, and keeping them out makes this change easier to review. They can 
follow up separately.
   
   ### Additional Context
   
   #### Measured effect
   
   Microbenchmark against the real classes, JDK 8 runtime, the module's `target 
1.8` bytecode, realistic field values (including a ~120 character 
`properties`). Allocation measured with `ThreadMXBean.getThreadAllocatedBytes`:
   
   | Header | alloc before | alloc after | reduction | throughput |
   |---|---|---|---|---|
   | `SendMessageRequestHeader` (13 fields) | 3760 B | 1944 B | **-48.3%** | 
**2.07x** |
   | `PullMessageRequestHeader` (15 fields) | 4072 B | 2328 B | -42.8% | 1.79x |
   | `PopMessageRequestHeader` (12 fields) | 2456 B | 1432 B | -41.7% | 2.08x |
   | `AckMessageRequestHeader` (6 fields, omitNullValues) | 1848 B | 1176 B | 
-36.4% | 1.80x |
   | `ChangeInvisibleTimeRequestHeader` (8 fields, omitNullValues) | 2088 B | 
1416 B | -32.2% | 1.62x |
   | `QueryConsumerOffsetRequestHeader` (4 fields) | 1104 B | 608 B | -44.9% | 
**2.60x** |
   
   #### Correctness verification
   
   A differential test loads the original and the modified compiled classes in 
two separate class loaders, applies identical field values to both instances 
and compares `toString()`. Value plans cover: all-default, all-null, 
all-non-null, **each field individually null**, **each field individually 
set**, boolean fields in both states (this is what covers the conditional 
`isLiteConsumer` entry), plus 40,000 randomized adversarial value sets per 
class (null, empty, `\r\n\t`, other control characters, 300- and 2000-character 
strings, non-ASCII, `Integer`/`Long` `MAX_VALUE`/`MIN_VALUE`).
   
   - Differential test: **560,483 cases, 0 mismatches**
   - `mvn -pl remoting test`: **174 tests, 0 failures**
   - checkstyle (`style/rmq_checkstyle.xml`, `validate` phase): **0 violations**
   
   As a check on the test itself, deleting the conditional `isLiteConsumer` 
handling makes the differential test fail immediately and prints the exact 
divergence, confirming it can detect this class of regression rather than 
passing vacuously.
   
   Verified against `develop` at `bee586bcd`.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to