lizining1231 opened a new pull request, #3718:
URL: https://github.com/apache/dubbo-go/pull/3718

   ### Description
   Fixes #3717
   Triple unary RPCs serialize every payload through `codec.Marshal`, which 
calls `proto.Marshal`. Because `proto.Marshal` always starts from `nil`, each 
message pays a structural per-request allocation tax: a fresh `[]byte` of ~L 
bytes is allocated and zeroed (`memclr`) from scratch, then 
`bytes.NewBuffer(raw)` wraps it with a ~40B escaping allocation before the 
bytes are written to the transport. 
   
   1. **Per-message output slice (optimization point A)**: `proto.Marshal` 
builds the encoded payload from a `nil` starting point, so the slice can never 
reuse the pooled capacity that `bufferPool` already holds. Each message 
allocates ~L bytes and memclr-zeroes them (~53µs at 1MiB), even though the pool 
hands out a reusable `*bytes.Buffer` on the same call path.
   2. **Fresh wrapper + transport copy (optimization point B)**: the freshly 
allocated slice is wrapped by `bytes.NewBuffer(raw)` (a ~40B escaping 
allocation) and then copied wholesale into the transport writer, while the 
pooled buffer underneath is returned without its underlying array ever being 
reused.
   3. **Server-side generic responses allocate twice**: non-IDL server 
responses are wrapped in a `TripleResponseWrapper` by 
`tripleServerCodecSession`, serializing the payload and the wrapper as two 
separate allocations.
   
   The fix mirrors the approach upstream connect-go already ships 
(`marshalAppend`): marshal into the caller-provided pooled buffer instead of 
into a fresh slice. It is purely additive, codecs without the extension are 
untouched and keep their exact previous behavior.
   
   ### Changes
   
   **1. Optional `marshalAppender` codec extension (non-breaking)**
   
   `protocol/triple/triple_protocol/codec.go`:
   - Add optional interface `marshalAppender { MarshalAppend(dst []byte, 
message any) ([]byte, error) }`; implementing codecs serialize into a 
caller-provided buffer. The `Codec` interface itself is unchanged, so 
hessian2/json/msgpack and third-party codecs compile and behave exactly as 
before.
   - `protoBinaryCodec.MarshalAppend` delegates to 
`proto.MarshalOptions{}.MarshalAppend`, which drives the same protobuf encoder, 
so output is byte-identical to `Marshal`, and the cap-gated pure-append path 
means zero allocation when the buffer's spare capacity suffices.
   - `tripleServerCodecSession.MarshalAppend` extends the per-request server 
codec: the IDL leg forwards to the delegate's appender, and the non-IDL leg 
marshals the inner payload with its own codec but appends the outer 
`TripleResponseWrapper` into the caller-provided buffer instead of allocating a 
fresh slice, so server responses reach the fast path too.
   
   `protocol/triple/triple_protocol/buffer_pool.go`:
   - Add shared helper `marshalToPool(pool, appender, message)`: borrow a 
`*bytes.Buffer` from `bufferPool`, call `MarshalAppend` into its spare 
capacity; when the appender had to grow the slice, swap the larger array back 
into the pooled buffer so the capacity is recycled instead of dropped; on 
failure the buffer is returned and a `CodeInternal` error surfaces. Both 
marshalers now share one borrow/marshal/adopt dance.
   
   **2. Fast path in both unary marshalers + a shared compression tail**
   
   `protocol/triple/triple_protocol/envelope.go` (`envelopeWriter.Marshal`, 
gRPC/Triple envelope wire):
   - Probe the codec for `marshalAppender`; on hit run the new 
`marshalAndWrite` (marshalToPool → `Write(&envelope{Data: buffer})`), 
preserving the backup-codec fallback semantics of the slow path. On miss the 
existing `marshalWithFallback` slow path is kept unchanged.
   
   `protocol/triple/triple_protocol/protocol_triple.go` 
(`tripleUnaryMarshaler.Marshal`, Triple unary body wire):
   - Same probe: capable codecs take the new `marshalAndWrite` fast path; 
codecs without the extension keep the old `codec.Marshal` slow path unchanged.
   - The compression tail previously inlined at the end of `Marshal` is 
extracted into one shared `compressAndWrite`, called by both the fast and slow 
paths, so the compression threshold, pooled gzip, `sendMaxBytes` enforcement, 
compression header and final write can never drift between the two.
   
   ```mermaid
   flowchart TD
       classDef base fill:#f6f8fa,stroke:#8b949e
       classDef added fill:#fff3b0,stroke:#e3a008,stroke-width:2px
   
       C["Client sends request<br/>conn.Send → marshaler<br/>codec = configured 
codec (proto → protoBinaryCodec)"]:::base
       S["Server sends response<br/>conn.Send → marshaler<br/>codec = wrapped 
as tripleServerCodecSession (supports generic calls)"]:::base
   
       M["tripleUnaryMarshaler.Marshal (L504)<br/>Shared by both sides"]:::base
       P["Capability probe: m.codec.(marshalAppender)"]:::added
   
       F["Optimized path<br/>marshalAndWrite → marshalToPool<br/>→ 
MarshalAppender with buffer pooling → zero allocation<br/>(Implementers: 
protoBinaryCodec / server session wrapper)"]:::added
       SL["Legacy path<br/>codec.Marshal → always allocates"]:::base
   
       T["compressAndWrite (L540)<br/>Compression / size limit / write header / 
write body<br/>Common tail path shared by fast & slow (extracted in this 
change)"]:::added
       O["HTTP body"]:::base
   
       C --> M
       S --> M
       M --> P
       P -->|hit| F
       P -->|miss| SL
       F --> T
       SL --> T
       T --> O
   ```
   
   ### Test
   
   `marshal_perf_regression_test.go`
   
   | Test | Description |
   |---|---|
   | `TestMarshalPerfWireParity` | Core invariant: for the same message and 
compression setting the MarshalAppend fast path emits byte-for-byte the same 
wire output as the slow `codec.Marshal` path, on both the triple body wire and 
the envelope wire |
   | `TestMarshalPerfPoolInvariants` | `bufferPool.Get` returns an empty 
buffer, `Put` resets length; `nil` message writes nothing; an empty proto 
yields a zero-length (5-byte prefix only) envelope; the `compressMinBytes` 
boundary anchored on the real encoded length never panics and both paths agree 
on the compression flag |
   | `TestMarshalPerfBackupCodecFallback` | A failing codec implementing both 
`Codec` and `marshalAppender` forces both paths into the backup-codec fallback; 
output is byte-identical to the healthy reference, with no double fallback, 
`CodeInternal` on final error, and no panic when the backup is nil |
   | `TestMarshalPerfCompressionAndMaxBytes` | Both paths enforce the 
compression threshold and `sendMaxBytes` identically: over-limit messages 
return `CodeResourceExhausted`, compressed messages set the `Content-Encoding` 
header on both |
   | `TestMarshalPerfLargeBufferDropped` | Buffers grown beyond the 8MiB 
recycle cap are dropped, not reused, and a large message leaves no residue for 
the next small message |
   | `TestMarshalPerfConcurrentSend` | Concurrent sends share the same 
marshaler and `bufferPool` with no data races (run under `-race`) |
   | `TestMarshalPerfTypeGuard` | Scope guard: codecs whose `MarshalAppend` is 
not byte-identical to `Marshal` (hessian2/json/msgpack/backup wrapper) must not 
implement `marshalAppender`; only `protoBinaryCodec` and 
`tripleServerCodecSession` do |
   | `TestMarshalPerfErrorGuard` | A non-proto message never panics on either 
path and surfaces as `CodeInternal`, matching slow-path behavior |
   | `TestMarshalPerfServerSessionFastPath` | `tripleServerCodecSession` 
reaches the MarshalAppend branch end to end for both IDL and non-IDL responses 
with byte-identical output |
   
   `marshal_perf_bench_test.go`
   
   | Benchmark | Description |
   |---|---|
   | `BenchmarkUnaryMarshalerFastPath` | `tripleUnaryMarshaler.Marshal` with 
`protoBinaryCodec` (MarshalAppend fast path), payloads 128B / 1KiB / 16KiB / 
1MiB |
   | `BenchmarkUnaryMarshalerSlowPath` | Control: same marshaler with 
`noAppenderCodec` (no `marshalAppender`, old `codec.Marshal` path), same 
payloads |
   | `BenchmarkUnaryMarshalerFastPathCompressed` / `SlowPathCompressed` | 
Fast/slow A/B on the gzip-compressed branch (second pooled buffer, compression 
header, compressed-size limit check) |
   | `BenchmarkEnvelopeWriterFastPath` / `SlowPath` | Same A/B for 
`envelopeWriter.Marshal` (gRPC/Triple envelope wire) |
   | `BenchmarkEnvelopeWriterFastPathCompressed` / `SlowPathCompressed` | Same 
A/B on the compressed envelope branch |
   
   ### Validation
   #### pprof CPU Profile diff
   
   <img width="1915" height="958" alt="image" 
src="https://github.com/user-attachments/assets/da23768b-0a92-4a41-93ab-0634ac295604";
 />
   
   #### Marshal Fast-Path Benchmark Results
   
   **go test bench -bench=BenchmarkMarshal**
   
   **allocs/op reduced from 3 to 1**
   
   | Message | Before  | After |
   | ------- | ------------------ | ----------------- |
   | 128B    | 3                  | 1                 |
   | 1KiB    | 3                  | 1                 |
   | 16KiB   | 3                  | 1                 |
   | 1MiB    | 3                  | 1                 |
   
   Both wire slow paths incurred 3 allocations per op, coming from proto 
marshal + gzip compression buffer + envelope/header overhead. After 
optimization, only 1 allocation remains, from the gzip compression pool's 
initial buffer, which is then reused by the pool.
   
   **B/op reduced by approximately −99.8%**
   
   | Message | Before | After  | Reduction |
   | ------- | ------------- | ------------ | --------- |
   | 128B    | 237           | 6            | -97.47%   |
   | 1KiB    | 1252          | 7            | -99.44%   |
   | 16KiB   | 18712         | 23           | -99.88%   |
   | 1MiB    | 1069533       | 3482         | -99.67%   |
   
   Before optimization, B/op ≈ message size, with full payload copy/allocation 
per request. After optimization, only fixed overhead remains, and it no longer 
grows with payload size.
   
   Reproduce:
   
   ```bash
   # pprof
   go test -run '^$' -bench '^BenchmarkUnaryMarshalerSlowPath$' 
-benchtime=30000x \
     -cpuprofile cpu_slow.out ./protocol/triple/triple_protocol/
   go test -run '^$' -bench '^BenchmarkUnaryMarshalerFastPath$' 
-benchtime=30000x \
     -cpuprofile cpu_fast.out ./protocol/triple/triple_protocol/
   go tool pprof -top -diff_base cpu_slow.out cpu_fast.out
   # go test bench
   go test -run '^$' -bench 
'^BenchmarkEnvelopeWriter(FastPath|SlowPath)Compressed$' \
     -benchmem -count=3 ./protocol/triple/triple_protocol/
   
   ```
   ### Checklist
   - [x] I confirm the target branch is `develop`
   - [x] I have run `make fmt` to format my code
   - [x] I have run `make test` to run local tests
   - [x] I have added tests that prove my fix is effective or that my feature 
works
   


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


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to