Skip to content

Reuse the chunk size header of full chunks in checksum trailers (6,441 → 39 allocations per 100 MiB) - #3490

Open
czj wants to merge 11 commits into
aws:version-3from
czj:perf/cache-chunk-header
Open

czj wants to merge 11 commits into
aws:version-3from
czj:perf/cache-chunk-header

Conversation

@czj

@czj czj commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

Following up on @jterapin's suggestion in #3448: AwsChunkedTrailerDigestIO still allocates one String per chunk for its hex size. Every chunk but the last has the same size, so that header is now built once in initialize, and only a shorter chunk still formats its own.

The gain is allocations only, not time: since #3448 no GC runs on this path anyway, so this mostly matters for apps that watch allocations per request or job.

Streaming 100 MiB through it with 16 KiB reads into a reused buffer, as Net::HTTP does (Ruby 4.0.9, arm64 macOS, median of 9 alternating runs):

read path Ruby allocated objects time
read(length, buffer) interpreter 6,441 → 39 20.6 → 20.8 ms
read(length, buffer) YJIT 6,437 → 35 20.6 → 20.5 ms
read(length) only interpreter 12,846 → 6,444 34.5 → 34.4 ms
read(length) only YJIT 12,842 → 6,440 34.7 → 34.3 ms

A 3,000-case differential fuzz against #3465 (random payloads, chunk sizes, read lengths, sources returning short reads, rewinds, both read paths) gives byte-identical output, and the new spec covers a source returning short reads, where chunks before the last one are not full either.

This branch sits on top of #3465 (itself on #3448), since #3465 changes the same line, so only the last two commits are new here: compare with #3465. I'll rebase it as they land. Does stacking it this way work for you, or would you rather have it based on #3448 alone?

kpumuk and others added 11 commits October 15, 2026 20:33
Reuse the input buffer, append chunk and trailer framing directly, and avoid temporary substrings when draining the encoded buffer. Cover binary payloads, partial reads, EOF, and rewind behavior.
A reader that ignores the given buffer and returns a frozen String made it the next read's buffer, raising FrozenError. Read into a local instead, consume buffer_enabled like the other options, and skip the trailer compatibility examples on JRuby, where trailer checksums are disabled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every chunk but the last one has the same size, so its hex size is built once instead of allocating a String per chunk: streaming 100 MiB with 16 KiB reads allocates 39 objects instead of 6,441.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The spec now reads 4 bytes first, then full chunks, so both the cached
header and the formatted one are exercised. `size` derives the full
chunk overhead from the same cached header the body writes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jterapin

jterapin commented Nov 9, 2026

Copy link
Copy Markdown
Contributor

Nice! I’ll take a look. This will ship separately from the earlier two PRs. I want to release those first and make sure everything goes smoothly before shipping this one.

@czj

czj commented Nov 9, 2026

Copy link
Copy Markdown
Contributor Author

That's a wise approach. Thanks for your work on this :)

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.

3 participants