Skip to content

Optimize BSON decoding #1667

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 24 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions bson/src/main/org/bson/ByteBuf.java
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,26 @@ public interface ByteBuf {
*/
byte[] array();

/**
* <p>States whether this buffer is backed by an accessible byte array.</p>
*
* <p>If this method returns {@code true} then the {@link #array()} and {@link #arrayOffset()} methods may safely be invoked.</p>
*
* @return {@code true} if, and only if, this buffer is backed by an array and is not read-only
* @since 5.5
*/
boolean hasArray();

/**
* Returns the offset of the first byte within the backing byte array of
* this buffer.
*
* @throws java.nio.ReadOnlyBufferException If this buffer is backed by an array but is read-only
* @throws UnsupportedOperationException if this buffer is not backed by an accessible array
* @since 5.5
*/
int arrayOffset();

/**
* Returns this buffer's limit.
*
Expand Down
10 changes: 10 additions & 0 deletions bson/src/main/org/bson/ByteBufNIO.java
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,16 @@ public byte[] array() {
return buf.array();
}

@Override
public boolean hasArray() {
return buf.hasArray();
}

@Override
public int arrayOffset() {
return buf.arrayOffset();
}

@Override
public int limit() {
return buf.limit();
Expand Down
54 changes: 54 additions & 0 deletions bson/src/main/org/bson/internal/PlatformUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright 2008-present MongoDB, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.bson.internal;

/**
* Utility class for platform-specific operations.
* This class is not part of the public API and may be removed or changed at any time.
*/
public final class PlatformUtil {

private PlatformUtil() {
//NOOP
}

// These architectures support unaligned memory access.
// While others might as well, it's safer to assume they don't.
private static final String[] ARCHITECTURES_ALLOWING_UNALIGNED_ACCESS = {
"x86",
"amd64",
"i386",
"x86_64",
"arm64",
"aarch64"};

public static boolean isUnalignedAccessAllowed() {
try {
String processArch = System.getProperty("os.arch");
for (String supportedArch : ARCHITECTURES_ALLOWING_UNALIGNED_ACCESS) {
if (supportedArch.equals(processArch)) {
return true;
}
}
return false;
} catch (Exception e) {
// Ignore security exception and proceed with default value
return false;
}
}
}

85 changes: 68 additions & 17 deletions bson/src/main/org/bson/io/ByteBufferBsonInput.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import java.nio.charset.StandardCharsets;

import static java.lang.String.format;
import static org.bson.internal.PlatformUtil.isUnalignedAccessAllowed;

/**
* An implementation of {@code BsonInput} that is backed by a {@code ByteBuf}.
Expand All @@ -33,6 +34,10 @@
public class ByteBufferBsonInput implements BsonInput {

private static final String[] ONE_BYTE_ASCII_STRINGS = new String[Byte.MAX_VALUE + 1];
private static final boolean UNALIGNED_ACCESS_SUPPORTED = isUnalignedAccessAllowed();
private int scratchBufferSize = 0;
private byte[] scratchBuffer;


static {
for (int b = 0; b < ONE_BYTE_ASCII_STRINGS.length; b++) {
Expand Down Expand Up @@ -127,15 +132,12 @@ public String readString() {

@Override
public String readCString() {
int mark = buffer.position();
skipCString();
int size = buffer.position() - mark;
buffer.position(mark);
int size = computeCStringLength(buffer.position());
return readString(size);
}

private String readString(final int size) {
if (size == 2) {
private String readString(final int stringSize) {
if (stringSize == 2) {
byte asciiByte = buffer.get(); // if only one byte in the string, it must be ascii.
byte nullByte = buffer.get(); // read null terminator
if (nullByte != 0) {
Expand All @@ -146,26 +148,75 @@ private String readString(final int size) {
}
return ONE_BYTE_ASCII_STRINGS[asciiByte]; // this will throw if asciiByte is negative
} else {
byte[] bytes = new byte[size - 1];
buffer.get(bytes);
byte nullByte = buffer.get();
if (nullByte != 0) {
throw new BsonSerializationException("Found a BSON string that is not null-terminated");
if (buffer.hasArray()) {
int position = buffer.position();
int arrayOffset = buffer.arrayOffset();
buffer.position(position + stringSize - 1);
byte nullByte = buffer.get();
if (nullByte != 0) {
throw new BsonSerializationException("Found a BSON string that is not null-terminated");
}
return new String(buffer.array(), arrayOffset + position, stringSize - 1, StandardCharsets.UTF_8);
}

if (stringSize <= scratchBufferSize) {
buffer.get(scratchBuffer, 0, stringSize);
if (scratchBuffer[stringSize - 1] != 0) {
throw new BsonSerializationException("BSON string not null-terminated");
}
return new String(scratchBuffer, 0, stringSize - 1, StandardCharsets.UTF_8);
} else {
scratchBufferSize = stringSize + (stringSize >> 1); //1.5 times the size
scratchBuffer = new byte[scratchBufferSize];
buffer.get(scratchBuffer, 0, stringSize);
if (scratchBuffer[stringSize - 1] != 0) {
throw new BsonSerializationException("BSON string not null-terminated");
}
return new String(scratchBuffer, 0, stringSize - 1, StandardCharsets.UTF_8);
}
return new String(bytes, StandardCharsets.UTF_8);
}
}

@Override
public void skipCString() {
ensureOpen();
boolean checkNext = true;
while (checkNext) {
if (!buffer.hasRemaining()) {
throw new BsonSerializationException("Found a BSON string that is not null-terminated");
int pos = buffer.position();
int length = computeCStringLength(pos);
buffer.position(pos + length);
}

public int computeCStringLength(final int prevPos) {
ensureOpen();
int pos = buffer.position();
int limit = buffer.limit();

if (UNALIGNED_ACCESS_SUPPORTED) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

So if we did this for all platforms, it would be slower on the ones that don't allow unaligned access? Slower than just going byte by byte? Just wondering if it's worth it to have two code paths to maintain.

I also don't see a test for when this value is false, since we don't run on any platforms that would make it so. It's a bit concerning that we don't, even though by inspection it seems obvious, at least with the code as it is, that it's correct. If we did want to add a test, we would have to add a testing backdoor to PlatformUtil to override the default behavior of examining "os.arch"

Copy link
Member Author

@vbabanin vbabanin Apr 17, 2025

Choose a reason for hiding this comment

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

There might be some performance penalty, as ByteBuffer uses Unsafe.getLongUnaligned, which reads bytes individually and composes a long on architectures that do not support unaligned access, potentially adding some overhead.

Nearly all modern cloud providers provide architectures that support unaligned access. The ones that don’t are typically limited to embedded systems or legacy hardware. Given how rare such platforms are, I’m actually in favor of removing the platform check altogether - I think the likelihood of hitting such an architecture is extremely low. @jyemin @NathanQingyangXu What do you think?

Copy link
Contributor

Choose a reason for hiding this comment

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

I am ok with this. Keeping expanding the CPU list in the long future doesn't make much sense to me.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yes, let's remove the platform check.

int chunks = (limit - pos) >>> 3;
// Process 8 bytes at a time.
for (int i = 0; i < chunks; i++) {
long word = buffer.getLong(pos);
long mask = word - 0x0101010101010101L;
mask &= ~word;
mask &= 0x8080808080808080L;
if (mask != 0) {
// first null terminator found in the Little Endian long
int offset = Long.numberOfTrailingZeros(mask) >>> 3;
// Found the null at pos + offset; reset buffer's position.
return (pos - prevPos) + offset + 1;
}
pos += 8;
Copy link
Contributor

@NathanQingyangXu NathanQingyangXu Apr 9, 2025

Choose a reason for hiding this comment

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

the above bitwise magic is cool; but it would be also great to leave some reference to SWAR (like https://en.wikipedia.org/wiki/SWAR) so future coder could know how to understand and maintain it (I assumed that if he had not known of SWAR, the above code comments won't be super helpful as well.

Copy link
Contributor

@NathanQingyangXu NathanQingyangXu Apr 9, 2025

Choose a reason for hiding this comment

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

I am not sure whether it is an idea to present some proof that the above bitwise magic does help perf significantly so the tradeoff between perf and readability is an easy decision (Or you have provided it in the description of the PR?).
For instance, two following metrics could be juxtaposed:

  1. baseline or current main branch metrics (without any optimization)
  2. metrics when only the above SWAR trick applied

Then it would be more convincing that the bitwise magic is really justified.

Copy link
Member Author

@vbabanin vbabanin Apr 17, 2025

Choose a reason for hiding this comment

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

For the findMany case with Netty transport settings (emptying the cursor), I observed a 28.42% improvement. Results: link

With Java IO (default transport), the gains are more modest:
findMany: ~3%
Flat BSON decoding: ~8.65%
Results: link

Copy link
Contributor

Choose a reason for hiding this comment

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

For the findMany case with Netty transport settings (emptying the cursor), I observed a 28.42% improvement. Results: link

With Java IO (default transport), the gains are more modest: findMany: ~3% Flat BSON decoding: ~8.65% Results: link

I think we could delay this cryptic SWAR bitwise optimization to next round of "extra-mile" perf optimization. Currently without its usage the perf improvement has been good enough. Needless to say, the bitwise code logic is hard to understand, difficult to maintain (even for the code author in the long future). Without compelling perf gain, it seems too risky to adopt such perf optmization trick. The above benchmark shows mediore metrics on default Java IO transport, so to me there is no reason to introduce this scary SWAR usage for the initial round of perf optmization.

@jyemin , how do you think?

Copy link
Collaborator

Choose a reason for hiding this comment

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

The Java IO gains are indeed modest, but I can't come up with a hypothesis for why Netty would be so much better in this case.

Since the gains from the other optimizations still seem worthwhile, perhaps we should just revert the SWAR piece and consider it in follow-on ticket and get another set of eyes on it (perhaps our Netty expert collaborator from other PRs).

Copy link
Member Author

@vbabanin vbabanin Apr 23, 2025

Choose a reason for hiding this comment

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

Unresolving this, as there is still a discussion.

difficult to maintain

From my perspective, the maintenance burden is quite minimal - the code spans just ~8 lines with bitwise complexity that might not be immediately obvious, however, i believe would not require deep study. For a ~30% improvement in the Netty case, the trade-off seems worthwhile.

a hypothesis for why Netty would be so much better in this case.

I ran a local benchmark with JIT compilation (C2 on ARM), which gave some visibility into the generated assembly for both Java’s ByteBuffer and Netty’s ByteBuf.

TLDR; The original byte-by-byte search seems to be inefficient on Netty due to virtual calls, component navigation, and additional checks. Java’s ByteBuffer, by contrast, was already quite optimized by JIT. The SWAR change improved inlining and reduced overhead in Netty, while bringing limited gains for JDK buffers as there were not much room for improvement, and additionally getLongUnalignhed was not inlined.

Before SWAR (Netty):

In its original pre-SWAR byte-by-byte loop, Netty’s ByteBuff had several sources of overhead:

  • Virtual Calls: Each getByte call involved virtual dispatch through the ByteBuf hierarchy (e.g., CompositeByteBuf, SwappedByteBuf), introducing latency from vtable lookups and branching.
  • Buffer Navigation: For buffers like CompositeByteBuf, getByte required navigating the buffer structure via findComponent, involving array lookups.
  • Heavy Bounds Checking: It seems that Netty’s checkIndex included multiple conditionals and reference count checks (e.g., ensureAccessible), which were more costly than Java’s simpler bounds checks.
  • 'Not unrolled while loop': JDKs ByteBuffer has been unrolled to 4 getByte() per iteration which reduced branching overhead, but Netty's hasn't.
Netty pre-SWAR (byte-by-byte) assembly
0x0000000111798150:   orr	w29, w12, w15           ; Combine indices for bounds check
0x0000000111798154:   tbnz	w29, #31, 0x0000000111798638 ; Check for negative index, branch to trap
0x0000000111798158:   ldr	x12, [x1, #80]          ; Load memory address
0x000000011179815c:   ldrsb	w0, [x12, w0, sxtw #0]  ; Load byte (getByte, after navigation)
0x0000000111798160:   cbz	w0, 0x000000011179833c ; Check if byte is null (0)
0x0000000111798164:   cmp	w11, w17                ; Compare pos with limit
0x0000000111798168:   b.ge	0x00000001117985cc     ; Branch if pos >= limit
0x000000011179816c:   mov	w10, w11                ; Update pos
; findComponent navigation (part of getByte)
0x00000001117981c4:   cmp	w14, w15                ; Compare index with component bounds
0x00000001117981c8:   b.eq	0x000000011179829c     ; Branch to component lookup
0x000000011179829c:   ldr	w0, [x14, #20]          ; Load component index
0x00000001117982a0:   ldr	w12, [x14, #32]         ; Load component array
0x00000001117982a4:   ldr	w2, [x14, #24]          ; Load component bounds
0x00000001117982a8:   ldr	w15, [x14, #16]         ; Load component offset
; Virtual call for getByte
0x00000001117983a8:   bl	0x0000000111228600      ; Virtual call to getByte
; - com.mongodb.internal.connection.netty.NettyByteBuf::get@5
; UncommonTrap for bounds check (checkIndex)
0x0000000111798638:   bl	0x000000011122ec80      ; Call UncommonTrapBlob (checkIndex failure)
; - io.netty.util.internal.MathUtil::isOutOfBounds@15
; - io.netty.buffer.AbstractByteBuf::checkIndex0@14

Before SWAR (JDK):

In contrast, Java’s ByteBuffer pre-SWAR byte-by-byte loop was already quite efficient, using an inlined getByte on a direct byte[] with lightweight bounds checks.

After SWAR (Netty):

  • ̶ ̶S̶w̶i̶t̶c̶h̶i̶n̶g̶ ̶t̶o̶ ̶̶g̶e̶t̶L̶o̶n̶g̶̶ ̶e̶l̶i̶m̶i̶n̶a̶t̶e̶d̶ ̶v̶i̶r̶t̶u̶a̶l̶ ̶c̶a̶l̶l̶s̶ ̶w̶h̶i̶c̶h̶ ̶l̶e̶a̶d̶ ̶t̶o̶ ̶i̶n̶l̶i̶n̶e̶d̶ ̶̶g̶e̶t̶L̶o̶n̶g̶̶ ̶d̶i̶r̶e̶c̶t̶l̶y̶ ̶t̶o̶ ̶a̶ ̶̶l̶d̶r̶̶ ̶i̶n̶s̶t̶r̶u̶c̶t̶i̶o̶n̶.̶ ̶
  • Bypassed buffer navigation for direct buffers (like PooledUnsafeDirectByteBuf), and reduced bounds checks to one per 8 bytes.

This addressed Netty’s significant overhead

After SWAR (JDK):

The SWAR version used a native getLongUnaligned call, which introduced overhead. The JVM’s C2 compiler did not inline getLongUnaligned, so the compiler generated a stub, which is evident from the produced JIT assembly:

Non-inlined getLongUnaligned ~10 cycles of overhead `0x000000011424bc34: bl 0x000000011424bf00 ; Call getLongUnaligned (stub)`
0x000000011424bf00:   isb
0x000000011424bf04:   mov	x12, #0x7b10            ; Metadata for getLongUnaligned
0x000000011424bf08:   movk	x12, #0xb, lsl #16
0x000000011424bf0c:   movk	x12, #0x8, lsl #32
0x000000011424bf10:   mov	x8, #0x17c4             ; Jump address
0x000000011424bf14:   movk	x8, #0x13d0, lsl #16
0x000000011424bf18:   movk	x8, #0x1, lsl #32
0x000000011424bf1c:   br	x8                      ; Branch to trampoline
0x000000011424bf20:   ldr	x8, 0x000000011424bf28 ; Load trampoline address
0x000000011424bf24:   br	x8                      ; Branch to trampoline
0x000000011424bf28:   b	0x0000000114b7bbe8     ; Trampoline address

Java’s efficient pre-SWAR loop leaves little room for improvement, and SWAR’s unlined native call limits gains.

Assembly for JDK's already inlined `getByte() in pre-SWAR`
; Byte-by-byte loop (org.bson.io.ByteBufferBsonInput::computeCStringLength)
0x00000001302dc760:   ldrsb	w10, [x10, #16]        ; Load byte from byte[] (getByte)
0x00000001302dc764:   cbz	w10, 0x00000001302dc900 ; Check for null

That being said, I’m in favor of keeping this optimization given the substantial performance improvement for Netty users and the low risk associated with unaligned access. However, I’m also fine with moving it to a separate ticket to unblock the rest of the optimizations. I am reverting the SWAR changes for now.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Switching to getLong eliminated virtual calls which lead to inlined getLong directly to a ldr instruction

Is getLong better because the method is not virtual, or because it's invoked 1/8 of the time? (It seems like if getByte is virtual then getLong would have to be as well.)

Copy link
Collaborator

Choose a reason for hiding this comment

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

I am reverting the SWAR changes for now.

I don't see it reverted yet, but as for me this is convincing rationale to keep it.

Copy link
Member Author

@vbabanin vbabanin Apr 23, 2025

Choose a reason for hiding this comment

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

Is getLong better because the method is not virtual, or because it's invoked 1/8 of the time? (It seems like if getByte is virtual then getLong would have to be as well.)

Ah, actually getLong also stayed virtual for SwappedByteBuff path, which is in our case. JIT produced code with inlined getLong for PooledUnsafeDirectByteBuf but virtual for SwappedByteBuff.

Java interpretation of compiled code
              if (underlying instanceof UnsafeDirectSwappedByteBuf) {
                UnsafeDirectSwappedByteBuf swappedBuf = (UnsafeDirectSwappedByteBuf) underlying;
                ByteBuf innerBuf = swappedBuf.unwrap();

      
                if (innerBuf instanceof PooledUnsafeDirectByteBuf) {
                    PooledUnsafeDirectByteBuf directBuf = (PooledUnsafeDirectByteBuf) innerBuf;
                    readerIndex = directBuf.readerIndex();
                    writerIndex = directBuf.writerIndex();
                    
                    //processPooledUnsafeDirect has INLINED getLong
                   return processPooledUnsafeDirect(directBuf, startIndex, readerIndex, writerIndex);
                } else {
                    // Fallback for other types 
                    readerIndex = innerBuf.readerIndex();
                    writerIndex = innerBuf.writerIndex();
                    return processGeneric(innerBuf, startIndex, readerIndex, writerIndex);
                }
            }
      
        //Our case
            else if (underlying instanceof SwappedByteBuf) {
                SwappedByteBuf swappedBuf = (SwappedByteBuf) underlying;
                ByteBuf innerBuf = swappedBuf.unwrap();

         
                if (innerBuf instanceof CompositeByteBuf) {
                    CompositeByteBuf compositeBuf = (CompositeByteBuf) innerBuf;
                    readerIndex = compositeBuf.readerIndex();
                    writerIndex = compositeBuf.writerIndex();
                   
                    // Virtual call inside (not inlined)
                    return processComposite(compositeBuf, startIndex, readerIndex, write
                    }
                     ........
                   }

Most of gains comes from 1/8 fewer calls reducing virtual call overhead, checks and component lookups. I also noticed, that JDK's ByteBuffer had 4 unrolled getByte operations per iteration, which was not the case for Netty.

Copy link
Member Author

Choose a reason for hiding this comment

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

i don't see it reverted yet, but as for me this is convincing rationale to keep it.

I have reverted the changes in these commits:

I will create a separate follow-up PR after merging this one to have a focused discussion.

}
checkNext = buffer.get() != 0;
}

// Process remaining bytes one-by-one.
while (pos < limit) {
if (buffer.get(pos++) == 0) {
return (pos - prevPos);
}
}

buffer.position(pos);
throw new BsonSerializationException("Found a BSON string that is not null-terminated");
}

@Override
Expand Down
71 changes: 71 additions & 0 deletions bson/src/test/unit/org/bson/internal/PlatformUtilTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* Copyright 2008-present MongoDB, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.bson.internal;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

class PlatformUtilTest {

@ParameterizedTest
@ValueSource(strings = {"arm", "arm64", "aarch64", "ppc", "ppc64", "sparc", "mips"})
@DisplayName("Should not allow unaligned access for unsupported architectures")
void shouldNotAllowUnalignedAccessForUnsupportedArchitecture(final String architecture) {
withSystemProperty("os.arch", architecture, () -> {
boolean result = PlatformUtil.isUnalignedAccessAllowed();
assertFalse(result);
});
}

@Test
@DisplayName("Should not allow unaligned access when system property is undefined")
void shouldNotAllowUnalignedAccessWhenSystemPropertyIsUndefined() {
withSystemProperty("os.arch", null, () -> {
boolean result = PlatformUtil.isUnalignedAccessAllowed();
assertFalse(result);
});
}

@ParameterizedTest
@ValueSource(strings = {"x86", "amd64", "i386", "x86_64", "arm64", "aarch64"})
@DisplayName("Should allow unaligned access for supported architectures")
void shouldAllowUnalignedAccess(final String architecture) {
withSystemProperty("os.arch", architecture, () -> {
boolean result = PlatformUtil.isUnalignedAccessAllowed();
assertTrue(result);
});
}

public static void withSystemProperty(final String name, final String value, final Runnable testCode) {
String original = System.getProperty(name);
if (value == null) {
System.clearProperty(name);
} else {
System.setProperty(name, value);
}
try {
testCode.run();
} finally {
System.setProperty(name, original);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,16 @@ public byte[] array() {
throw new UnsupportedOperationException("Not implemented yet!");
}

@Override
public boolean hasArray() {
return false;
}

@Override
public int arrayOffset() {
throw new UnsupportedOperationException("Not implemented yet!");
}

@Override
public ByteBuf limit(final int newLimit) {
if (newLimit < 0 || newLimit > capacity()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ <T extends BsonDocument> T getResponseDocument(final int messageId, final Decode
* @return a read-only buffer containing the response body
*/
public ByteBuf getBodyByteBuffer() {
return bodyByteBuffer.asReadOnly();
return bodyByteBuffer;
}

public void reset() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,16 @@ public byte[] array() {
return proxied.array();
}

@Override
public boolean hasArray() {
return proxied.hasArray();
}

@Override
public int arrayOffset() {
return proxied.arrayOffset();
}

@Override
public int limit() {
if (isWriting) {
Expand Down
Loading