Skip to content

Provide an opt-in zero-copy response body view - #2322

Open
pavel-ptashyts wants to merge 2 commits into
AsyncHttpClient:mainfrom
maygemdev:perf/zero-copy-response-body-view
Open

Provide an opt-in zero-copy response body view#2322
pavel-ptashyts wants to merge 2 commits into
AsyncHttpClient:mainfrom
maygemdev:perf/zero-copy-response-body-view

Conversation

@pavel-ptashyts

Copy link
Copy Markdown
Contributor

Summary

  • add Response#getResponseBodyAsBytesView() as a compatible default method
    with an explicit possibly-shared, read-only contract
  • return the existing byte array for a single eager Netty body part
  • use the body part accessor for lazy/direct/sliced buffers, avoiding a second
    aggregate copy without exposing ByteBuf storage
  • preserve defensive-copy behavior for getResponseBodyAsBytes(),
    getResponseBodyAsByteBuffer(), and getResponseBodyAsStream()
  • reuse the new accessor for string decoding so the fast path from Decode a lone response body part in place #2303 stays
    centralized

Closes #2321.

Compatibility and safety

The default implementation delegates to getResponseBodyAsBytes(), so existing
third-party Response implementations keep their current behavior. Netty uses
the optimized path only for exactly one body part. Empty and multipart bodies
continue through the existing aggregation path.

Tests cover eager identity and repeated access, defensive-copy isolation,
heap and direct lazy slices, reader/writer indices, reference counts, empty
bodies, multipart ordering and split UTF-8 characters, stream isolation, and
default-method delegation.

Verification

  • mvnw.cmd -B -ntp -pl client -Dtest=NettyAsyncResponseTest test
  • mvnw.cmd clean verify
    • Amazon Corretto 11.0.32.1
    • 1,489 tests; 0 failures; 0 errors; 26 skipped
    • Error Prone, NullAway, Javadocs, GPG signing, and Revapi passed

Allocation benchmark

JMH 1.37, Corretto 11.0.32.1, one fork, three 500 ms warmups, five
500 ms measurements, gc profiler. Values are allocated bytes per operation,
rounded to whole bytes; zero means below the profiler's resolution.

Body shape getResponseBodyAsBytes() getResponseBodyAsBytesView()
Empty 16 B/op 16 B/op
1 eager part, 512 B 576 B/op 0 B/op
1 eager part, 4 KiB 4,160 B/op 0 B/op
1 eager part, 16 KiB 16,448 B/op 0 B/op
1 eager part, 128 KiB 131,136 B/op 0 B/op
1 lazy direct part, 4 KiB 8,272 B/op 4,112 B/op
1 lazy direct part, 16 KiB 32,848 B/op 16,400 B/op
2 eager parts, 4 KiB each 8,256 B/op 8,208 B/op
8 eager parts, 4 KiB each 32,832 B/op 32,832 B/op

The eager single-part view measured about 2.3 ns/op regardless of body size,
versus 322 ns/op for 4 KiB and 11.0 us/op for 128 KiB defensive copies. Lazy
single-part allocation is halved as expected. Multipart bodies retain the
payload-sized aggregation allocation.

Codex on behalf of Pavel Ptashyts

pavel-ptashyts and others added 2 commits August 31, 2026 12:28
Byte-array consumers currently pay for an aggregate copy even when a
response has a single body part. Add an explicit read-only view accessor
so callers can opt into sharing while the existing accessor retains its
defensive-copy contract.

Reuse the view for string decoding and cover eager, lazy, multipart, and
third-party Response implementations.

Refs AsyncHttpClient#2321

Codex on behalf of Pavel Ptashyts

Co-Authored-By: OpenAI Codex <noreply@openai.com>
Mockito 4 cannot invoke interface default methods through
CALLS_REAL_METHODS on JDK 21 and newer. Invoke the Response default
method explicitly so delegation remains covered on every supported JDK.

Refs AsyncHttpClient#2321

Codex on behalf of Pavel Ptashyts

Co-Authored-By: OpenAI Codex <noreply@openai.com>
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.

Provide an opt-in zero-copy response body byte-array view

1 participant