Skip to content

Conversation

@BorisDog
Copy link
Contributor

This PR introduces ReadOnlyMemoryBsonReader, an alternative to BinaryBsonReader. ReadOnlyMemoryBsonReader provides a more lightweight BSON deserialization from a continuous single chunk buffers.
Preliminary testing shows an improvement of ~50% in our deserialization benchmarks.

@BorisDog BorisDog requested a review from a team as a code owner November 21, 2025 23:29
@BorisDog BorisDog added the improvement Optimizations or refactoring (no new features or fixes). label Nov 21, 2025
Copilot finished reviewing on behalf of BorisDog November 21, 2025 23:30
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces ReadOnlyMemoryBsonReader, a new, optimized BSON reader implementation designed for continuous single-chunk buffers. The reader provides approximately 50% improvement in deserialization benchmarks compared to BinaryBsonReader.

Key changes:

  • New ReadOnlyMemoryBsonReader class that directly works with ReadOnlyMemory<byte> instead of streams
  • New ReadOnlyMemoryBuffer implementation of IByteBuffer for read-only memory
  • Updated existing BSON deserialization code to use BsonBinaryReaderUtils.CreateBinaryReader() which automatically selects the optimized reader when possible
  • Enhanced name decoders (Utf8NameDecoder, TrieNameDecoder) to support span-based decoding
  • Various refactorings to reduce nesting and improve code readability

Reviewed changes

Copilot reviewed 30 out of 30 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
ReadOnlyMemoryBsonReader.cs New optimized BSON reader implementation for contiguous memory
ReadOnlyMemoryBuffer.cs New read-only byte buffer wrapper for ReadOnlyMemory<byte>
ReadOnlyMemoryReaderSettings.cs Settings class for the new reader
BsonBinaryReaderUtils.cs Factory utilities for creating appropriate reader implementations
Utf8Helper.cs, Utf8NameDecoder.cs, TrieNameDecoder.cs Span-based decoding support
RawBsonDocument.cs, RawBsonArray.cs, LazyBsonDocument.cs, LazyBsonArray.cs Refactored to use factory method for reader creation
ObjectId.cs Refactored to use ReadOnlySpan<byte> for parsing
BsonBinaryReader.cs Moved common utilities to BsonBinaryReaderUtils
Various operation files Simplified using statements and refactored to use factory method
Comments suppressed due to low confidence (1)

src/MongoDB.Bson/IO/ReadOnlyMemoryBsonReader.cs:1

  • Double forward slashes in XML documentation comment. Should be /// <param name="requiredBsonType">The required BSON type.</param> with three slashes, not four.
/* Copyright 2010-present MongoDB Inc.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@BorisDog BorisDog requested review from papafe and rstam and removed request for ajcvickers November 21, 2025 23:45
@codeowners-service-app
Copy link

codeowners-service-app bot commented Nov 21, 2025

Assigned mongoKart for team dbx-csharp-dotnet because rstam is out of office.
Assigned jordan-smith721 for team dbx-csharp-dotnet because papafe is out of office.

get => _position;
set
{
if (value < 0 || value > _memory.Length)
Copy link
Member

Choose a reason for hiding this comment

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

Ensure.IsBetween?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not available (yet?) in Bson lib.

// insert the element name into the error message
var periodIndex = ex.Message.IndexOf('.');
var dottedElementName = GenerateDottedElementName();
var dottedElementName = BsonBinaryReaderUtils.GenerateDottedElementName(_context, _contextStack.ToArray(), () => _bsonStream.ReadCString(Utf8Encodings.Lenient));
Copy link
Member

Choose a reason for hiding this comment

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

I'm pretty sure we have a context allocation here which could be avoided by passing _bsonStream as a parameter for GenerateDottedElementName. If the method is more generic, we should add TState parameter then.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's fine I think, it's not on a hot path.
For example _contextStack.ToArray() is also not ideal, but ok on an exception path.

/// <summary>
/// Provides utility methods for working with <see cref="IBsonReader"/> instances in binary BSON format.
/// </summary>
public static class BsonBinaryReaderUtils
Copy link
Member

Choose a reason for hiding this comment

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

Can it be internal?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Unfortunately not, used in Driver.

var dataSize = ReadSize();
var totalSize = dataSize + 1; // data + subtype
var span = _memory.Span.Slice(_position, totalSize);
_position += totalSize;
Copy link
Member

Choose a reason for hiding this comment

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

Can we encapsulate memory slicing and position update in the helper methods?

private byte ReadByte() => _memory.Span[_position++];

and

private ReadOnlySpan<byte> ReadBytes(int size)
{
  var span = _memory.Span.Slice(_position, size);
  _position += size;
  return span;
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I am hesitant about this for a couple of reasons:

  1. We won't be able to encapsulate all accesses to _memory and _position in ReadBytes, for example it does help with methods like ReadCStringFromMemory, ReadSliceFromMemory
  2. I couldn't get it inlined easily

{
VerifyBsonType(BsonType.Array);

var slice = ReadSliceFromMemory();
Copy link
Member

Choose a reason for hiding this comment

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

As far as I understood ReadSliceFromMemory does not copy bytes to a new buffer. I do not think it's safe to return reference to the original buffer. As RawBsonArray could outlive the original buffer and could still reference it even after it will be returned to pool or even reused.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is a good point, looking deeper into that.

var bytes = new byte[] { 1, 2 };
var subject = CreateSubject(bytes);

var e = Record.Exception(() => subject.Clear(0, 0));
Copy link
Member

Choose a reason for hiding this comment

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

Can reuse ValidateWritableException here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks, done.

private void ValidateWritableException(Action action)
{
var e = Record.Exception(action);
e.Should().BeOfType<InvalidOperationException>();
Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Aligning with ByteArrayBuffer here. We might reevaluate this in 4.0.

{
var subject = CreateSubject(2);

Action action = () => subject.GetByte(position);
Copy link
Member

Choose a reason for hiding this comment

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

We are using Record.Exception usually.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Refactored

var subject = CreateSubject(2);
var destination = new byte[3];

Action action = () => subject.GetBytes(position, destination, 0, count);
Copy link
Member

Choose a reason for hiding this comment

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

Record.Exception here and more below.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Refactored


Action action = () => subject.GetBytes(0, null, 0, 2);

action.ShouldThrow<ArgumentOutOfRangeException>();
Copy link
Member

Choose a reason for hiding this comment

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

May be ArgumentNullException?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Just propagating Span ctor exception for simplicity.

@BorisDog BorisDog requested a review from sanych-sun November 27, 2025 20:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

improvement Optimizations or refactoring (no new features or fixes).

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants