Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

heliang666s
Copy link
Member

Optimize Triple POJO Serialization with Zero-Copy

What is the purpose of this PR?

This PR introduces a significant zero-copy optimization for POJO serialization within the Dubbo Triple protocol's wrapper mode. It targets UNARY RPC calls to dramatically reduce memory allocation and improve performance.

Why is this change necessary?

Previously, serializing multiple POJO parameters involved creating an intermediate byte[] for each parameter. This led to:

  • High memory allocation and churn.
  • Increased GC pressure, impacting overall application performance.
  • Performance bottlenecks due to memory copy operations.

How did you implement it?

The core of this optimization is a dual-phase streaming mechanism for UNARY RPC calls:

  1. Phase 1: Size Calculation (Allocation-Free)
    A lightweight CountingOutputStream calculates the serialized size of each parameter without allocating memory.

  2. Phase 2: Direct Serialization
    The parameters are serialized directly to the network output stream, prefixed with their calculated length.

To maintain backward compatibility, this new format is identified by appending a -streaming suffix to the serialization type (e.g., hessian4-streaming). The server detects this suffix and uses a corresponding streaming decoder.

The optimization automatically falls back to the traditional method for streaming RPCs (SERVER_STREAM, BI_STREAM) and non-capable serializations, ensuring no breaking changes.

What are the key benefits?

  • Reduced Memory Allocation: Eliminates intermediate byte[] buffers for parameters.
  • Lower GC Pressure: Significantly fewer objects for the garbage collector to manage.
  • Improved Performance: Faster serialization/deserialization by avoiding memory copies.
  • Full Backward Compatibility: The new mechanism is interoperable with older clients/servers and gracefully degrades when not applicable.

@heliang666s heliang666s requested review from EarthChen and oxsean August 6, 2025 18:16
@codecov-commenter
Copy link

codecov-commenter commented Aug 6, 2025

Codecov Report

❌ Patch coverage is 43.61446% with 234 lines in your changes missing coverage. Please review.
✅ Project coverage is 58.91%. Comparing base (a7b641f) to head (1462ecf).

Files with missing lines Patch % Lines
...bbo/rpc/protocol/tri/ReflectionPackableMethod.java 66.66% 41 Missing and 5 partials ⚠️
...e/dubbo/rpc/protocol/tri/ZeroCopyDirectUnpack.java 0.00% 40 Missing ⚠️
...che/dubbo/rpc/protocol/tri/ZeroCopyDirectPack.java 0.00% 31 Missing ⚠️
...ubbo/rpc/protocol/tri/WrapperStreamingDecoder.java 50.00% 12 Missing and 8 partials ⚠️
...c/main/java/org/apache/dubbo/rpc/model/UnPack.java 0.00% 16 Missing ⚠️
...e/dubbo/rpc/protocol/tri/CountingOutputStream.java 31.81% 12 Missing and 3 partials ⚠️
...ubbo/rpc/protocol/tri/WrapperStreamingEncoder.java 37.50% 12 Missing and 3 partials ⚠️
...on/serialize/fastjson2/FastJson2Serialization.java 6.25% 15 Missing ⚠️
...g/apache/dubbo/rpc/protocol/tri/PbArrayPacker.java 0.00% 11 Missing ⚠️
...erialize/DefaultSerializationExceptionWrapper.java 25.00% 8 Missing and 1 partial ⚠️
... and 5 more
Additional details and impacted files
@@             Coverage Diff              @@
##                3.3   #15616      +/-   ##
============================================
- Coverage     61.02%   58.91%   -2.11%     
+ Complexity    11685       12   -11673     
============================================
  Files          1923     1932       +9     
  Lines         87081    87464     +383     
  Branches      13115    13162      +47     
============================================
- Hits          53141    51532    -1609     
- Misses        28488    30344    +1856     
- Partials       5452     5588     +136     
Flag Coverage Δ
integration-tests-java21 ?
integration-tests-java8 ?
samples-tests-java21 ?
samples-tests-java8 ?
unit-tests-java11 58.90% <43.47%> (-0.10%) ⬇️
unit-tests-java17 58.66% <43.47%> (-0.11%) ⬇️
unit-tests-java21 58.65% <43.47%> (-0.11%) ⬇️
unit-tests-java8 58.90% <43.47%> (-0.11%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@EarthChen EarthChen requested a review from Copilot August 12, 2025 06:05
Copy link

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR implements zero-copy optimization for POJO serialization in the Dubbo Triple protocol, specifically targeting UNARY RPC calls. The optimization eliminates intermediate byte array allocations during serialization to reduce memory usage and improve performance.

Key changes include:

  • Introduction of ZeroCopyCapable interface for serialization implementations that support direct streaming
  • Dual-phase serialization approach using CountingOutputStream for size calculation followed by direct stream writing
  • Streaming format detection with -streaming suffix for backward compatibility
  • Enhanced Pack/UnPack interfaces to support OutputStream-based operations

Reviewed Changes

Copilot reviewed 18 out of 18 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
ZeroCopyCapable.java New interface extending Serialization to support zero-copy operations
Hessian2Serialization.java Implements ZeroCopyCapable with direct Hessian2Output streaming
FastJson2Serialization.java Implements ZeroCopyCapable using FastJSON2's direct streaming APIs
DefaultSerializationExceptionWrapper.java Wrapper implementation supporting ZeroCopyCapable delegation
StreamingLengthEncoder.java Utility for dual-phase length-prefixed serialization
CountingOutputStream.java Memory-efficient byte counting stream for size calculation
ReflectionPackableMethod.java Enhanced to detect zero-copy capability and choose appropriate packing strategy
Pack.java / UnPack.java / PackableMethod.java Extended interfaces to support OutputStream-based operations
Comments suppressed due to low confidence (1)

dubbo-serialization/dubbo-serialization-fastjson2/src/main/java/org/apache/dubbo/common/serialize/fastjson2/FastJson2Serialization.java:123

  • The comment mentions 'Remove ErrorOnNoneSerializable' but this feature is still being used in the code. Either remove the feature or update the comment to reflect the actual behavior.
                // Remove ErrorOnNoneSerializable to allow POJOs in zero-copy mode

try {
hessianOutput.close();
} catch (Exception e) {
// Ignore close errors, stream ownership belongs to caller
Copy link

Copilot AI Aug 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The catch block silently ignores close exceptions with a generic comment. This could hide important errors during stream cleanup. Consider logging the exception or at least checking if it's a critical error type.

Suggested change
// Ignore close errors, stream ownership belongs to caller
logger.warn("Exception occurred while closing Hessian2Output stream.", e);

Copilot uses AI. Check for mistakes.

Comment on lines +57 to +60
int length = ((bais.read() & 0xFF) << 24)
| ((bais.read() & 0xFF) << 16)
| ((bais.read() & 0xFF) << 8)
| (bais.read() & 0xFF);
Copy link

Copilot AI Aug 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The length validation checks for negative values but doesn't handle the case where available data is less than 4 bytes for the length prefix. If any of the bais.read() calls return -1, the bitwise operations will produce unexpected results.

Suggested change
int length = ((bais.read() & 0xFF) << 24)
| ((bais.read() & 0xFF) << 16)
| ((bais.read() & 0xFF) << 8)
| (bais.read() & 0xFF);
int length = readIntFromStream(bais);

Copilot uses AI. Check for mistakes.

int b3 = inputStream.read();
int b4 = inputStream.read();

if (b1 < 0 || b2 < 0 || b3 < 0 || b4 < 0) {
Copy link

Copilot AI Aug 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This validation correctly handles end-of-stream detection, but the same issue exists in ZeroCopyDirectUnpack where this validation is missing.

Copilot uses AI. Check for mistakes.

throw new IllegalStateException("Can not reach here");
}

boolean singleArgument = method.getRpcType() != MethodDescriptor.RpcType.UNARY;
Copy link

Copilot AI Aug 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic appears inverted. For UNARY RPC calls, singleArgument should typically be false (multiple arguments possible), but this sets it to true for non-UNARY types. This contradicts the PR description which targets UNARY calls.

Suggested change
boolean singleArgument = method.getRpcType() != MethodDescriptor.RpcType.UNARY;
boolean singleArgument = method.getRpcType() == MethodDescriptor.RpcType.UNARY;

Copilot uses AI. Check for mistakes.

* This is based on RPC type compatibility and serialization capability.
*/
private boolean shouldUseStreamingMode() {
if (rpcType != MethodDescriptor.RpcType.UNARY) {
Copy link

Copilot AI Aug 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The streaming mode logic correctly restricts to UNARY calls, but this conflicts with the earlier singleArgument assignment logic which seems inverted.

Suggested change
if (rpcType != MethodDescriptor.RpcType.UNARY) {
if (rpcType == MethodDescriptor.RpcType.UNARY) {

Copilot uses AI. Check for mistakes.

Thread.currentThread().getContextClassLoader()));

try {
if (isStreamingMode) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like the same code is in both the if and else branches. Why?

boolean isStreamingMode = isStreamingMode(url);

// Use Hessian2Output directly for zero-copy serialization
com.alibaba.com.caucho.hessian.io.Hessian2Output hessianOutput =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is there not org.apache.dubbo.common.serialize.hessian2.Hessian2ObjectOutput?

boolean serializationSupportsZeroCopy = isZeroCopyCapable(serializeName, url);

// Zero-copy should only be used when protocol allows it AND serialization supports it
// If protocol needs wrapper OR serialization doesn't support zero-copy, use wrapper mode
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not use and?

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.

4 participants