Skip to content

Fix #1622: size base64 encoding buffer from binary length hint - #1624

Merged
cowtowncoder merged 4 commits into
FasterXML:2.xfrom
seonwooj0810:fix/1622-writebinary-buffer-size-hint
Jun 26, 2026
Merged

Fix #1622: size base64 encoding buffer from binary length hint#1624
cowtowncoder merged 4 commits into
FasterXML:2.xfrom
seonwooj0810:fix/1622-writebinary-buffer-size-hint

Conversation

@seonwooj0810

Copy link
Copy Markdown
Contributor

Fixes #1622

Root cause

writeBinary(Base64Variant, InputStream, int dataLength) always allocated the read/encoding buffer at the small default size (2000 bytes) via _ioContext.allocBase64Buffer(), ignoring the supplied dataLength. For large binary content this forces many InputStream reads, and for sources like a protobuf InputStream (where a read(byte[]) smaller than the serialized size triggers an extra throwaway in-memory copy) it adds avoidable allocation.

Change

  • When dataLength > 0, size the buffer from that hint: allocBase64Buffer(min(dataLength, MAX_BASE64_ENCODE_BUFFER_LENGTH)). The cap (64kB) bounds retention of ThreadLocal-recycled buffers, addressing the memory-retention concern raised in the issue.
  • Applied to both UTF8JsonGenerator and WriterBasedJsonGenerator (identical pattern).
  • Bumped the default base64 codec buffer in BufferRecycler from 2000 → 16000 bytes, which also helps the unknown-length path.

This follows your guidance in the issue ("passes size hint, capped at say 64kb … based on dataLength if > 0" and "increasing default from 2kb to … 16kB"). One PR against 2.x as suggested, for roll-forward to 3.x.

Tests

Added BinaryWriteBufferSize1622Test: a recording InputStream confirms the generator now requests reads sized to the (capped) hint — 50,000 for a 50k payload and exactly 64kB for a 200k payload — for both byte- and char-backed generators, while the produced base64 still round-trips back to the original bytes. Without the fix the buffer stays at the default (≤16k), so these assertions fail.

Verification done:

  • (1) No in-flight PR: checked issue cross-references and open PR list — none.
  • (3) Code-focused: touches .java generator/recycler code plus a test.
  • (4) Bug pattern confirmed present on 2.x (allocBase64Buffer() with no size hint).
  • Build/test: ./mvnw test -Dtest=BinaryWriteBufferSize1622Test,Base64GenerationTest,Base64BinaryParsingTest and the util/io packages all pass (incl. BufferRecycler tests).

AI-assistance disclosure: implemented with the help of Claude Code (consistent with prior w/ Claude code credits in this repo).

When writing binary content from an InputStream of known length via
`writeBinary(Base64Variant, InputStream, int)`, the read/encoding buffer
was always the small default (2000 bytes), forcing many InputStream reads
for large content (and, for sources like protobuf streams, an extra
throwaway in-memory copy when the supplied buffer is smaller than the
serialized size).

Now, when the length is known (> 0), the buffer is sized from that hint,
capped at 64kB to bound retention of ThreadLocal-recycled buffers. Applied
to both UTF8JsonGenerator and WriterBasedJsonGenerator. The default base64
codec buffer is also bumped from 2000 to 16000 bytes to help the
unknown-length path. Per maintainer guidance on the issue.

Verification: added BinaryWriteBufferSize1622Test asserting the read buffer
is sized to the (capped) hint and that output still round-trips; existing
base64 and buffer-recycler tests pass.
// reads when encoding binary content of unknown/large length.

private final static int[] BYTE_BUFFER_LENGTHS = new int[] { 8000, 8000, 2000, 2000 };
private final static int[] BYTE_BUFFER_LENGTHS = new int[] { 8000, 8000, 2000, 16000 };

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I don't know the answer here but is there a reason not to keep this ordered? 8000 > 2000 so maybe the 16000 should go first.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good question. This array isn't sorted by value — it's positional: each slot is addressed by the named index constants just above it, and byteBufferLength(int ix) returns BYTE_BUFFER_LENGTHS[ix]. So the entries are in index order:

  • [0] BYTE_READ_IO_BUFFER = 8000
  • [1] BYTE_WRITE_ENCODING_BUFFER = 8000
  • [2] BYTE_WRITE_CONCAT_BUFFER = 2000
  • [3] BYTE_BASE64_CODEC_BUFFER = 16000 ← the slot this PR bumps

The 16000 lands at the end only because the base64 codec buffer happens to be index 3; reordering the array would silently remap every buffer's length to the wrong purpose. Happy to add a brief inline comment naming each slot if you think that'd make the positional intent clearer.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Yeah these must not be ordered by size, they are indexed by position as @seonwooj0810 pointed out.

However, I realized something: this will also affect parser-side, if changed; Base64-decoding buffer (in addition to generator-size encoding buffer).

And in fact, not quite sure default really needs changing: if and when actual size is indicated, we'll be using that anyway. I think I'll change default to 4000 as compromise.

@cowtowncoder
cowtowncoder merged commit dff0fb5 into FasterXML:2.x Jun 26, 2026
8 checks passed
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