Skip to content

Conversation

BorisDog
Copy link
Contributor

@BorisDog BorisDog commented Oct 13, 2025

Convert BsonBinaryWriterContext and BsonBinaryReaderContext to structs, to reduces allocations on a hot path.

@BorisDog BorisDog requested a review from rstam October 13, 2025 22:26
@BorisDog BorisDog requested a review from a team as a code owner October 13, 2025 22:26
_bsonStream = (stream as BsonStream) ?? new BsonStreamAdapter(stream);

_context = null;
_context = new(ContextType.TopLevel, 0);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Use TopLevel instead of null

public class BsonBinaryReaderTests
{
[Fact]
public void Bookmarks_should_work()
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added bookmarks test for better coverage of BsonBinaryReader.

GuidRepresentation.Unspecified)] GuidRepresentation guidRepresentation)
[InlineData(BsonBinarySubType.UuidStandard)]
[InlineData(BsonBinarySubType.UuidLegacy)]
public void ReadBinaryData_should_read_correct_subtype(BsonBinarySubType subType)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Unrelated cleanup.
GuidRepresentation wasn't used in ReadBinaryData_subtype_3_should_use_GuidRepresentation_from_settings and ReadBinaryData_subtype_4_should_use_GuidRepresentation_Standard tests. Both tests combined into single ReadBinaryData_should_read_correct_subtype test.

#pragma warning restore CA2213 // Disposable never disposed
private readonly BsonStream _bsonStream;
private BsonBinaryReaderContext _context;
private readonly Lazy<Stack<BsonBinaryReaderContext>> _contexts = new(() => new());
Copy link
Contributor

Choose a reason for hiding this comment

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

We've traded allocations of contexts for whatever allocations the stack class does.

Consider using a non-default initial capacity, probably around 4 (maybe 8), otherwise the first few calls to Push will result in memory allocations.

Copy link
Contributor

Choose a reason for hiding this comment

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

Lazy requires an allocation also. Maybe just go ahead and allocate the Stack?

Actually maybe multiple allocations. I think the factory lambda is an allocation as well.

Copy link
Contributor

Choose a reason for hiding this comment

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

private readonly Stack<BsonBinaryReaderContext> _contextStack = new(4);

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's a good suggestion, done.

return new BsonBinaryReaderBookmark(State, CurrentBsonType, CurrentName, _context, _bsonStream.Position);
}
public override BsonReaderBookmark GetBookmark() =>
new BsonBinaryReaderBookmark(State, CurrentBsonType, CurrentName, _context, _contexts.Value.ToArray(), _bsonStream.Position);
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's encapsulate all logic about how to restore the context stack into the bookmark, so here we can just pass the _contextStack and let the bookmark call ToArray (or whatever it wants to do).

new BsonBinaryReaderBookmark(State, CurrentBsonType, CurrentName, _context, _contextStack, _bsonStream.Position);

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.


_context = _context.PopContext(_bsonStream.Position);
_context = PopContext();

Copy link
Contributor

Choose a reason for hiding this comment

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

Let's encapsulate all side effects of popping the context in the PopContext method (see below).

 PopContext();

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good idea, done.

}

_context = _context.PopContext(_bsonStream.Position);
_context = PopContext();
Copy link
Contributor

Choose a reason for hiding this comment

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

 PopContext();

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

if (_context.ContextType == ContextType.JavaScriptWithScope)
{
_context = _context.PopContext(_bsonStream.Position); // JavaScriptWithScope
_context = PopContext(); // JavaScriptWithScope
Copy link
Contributor

Choose a reason for hiding this comment

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

PopContext(); // JavaScriptWithScope

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

{
BackpatchSize(); // size of the JavaScript with scope value
_context = _context.ParentContext;
_context = _contexts.Value.Pop();
Copy link
Contributor

Choose a reason for hiding this comment

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

PopContext();

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

_context = new BsonBinaryWriterContext(_context, ContextType.JavaScriptWithScope, _bsonStream.Position);

_contexts.Value.Push(_context);
_context = new(ContextType.JavaScriptWithScope, _bsonStream.Position);
Copy link
Contributor

Choose a reason for hiding this comment

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

Single line instead of two:

PushContext(new(ContextType.JavaScriptWithScope, _bsonStream.Position));

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

{
BackpatchSize(); // size of the JavaScript with scope value
_context = _context.ParentContext;
_context = _contexts.Value.Pop();
Copy link
Contributor

Choose a reason for hiding this comment

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

 PopContext();

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

_context = new BsonBinaryWriterContext(_context, ContextType.Array, _bsonStream.Position);

_contexts.Value.Push(_context);
_context = new(ContextType.Array, _bsonStream.Position);
Copy link
Contributor

Choose a reason for hiding this comment

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

PushContext(new(ContextType.Array, _bsonStream.Position));

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

var contextType = (State == BsonWriterState.ScopeDocument) ? ContextType.ScopeDocument : ContextType.Document;
_context = new BsonBinaryWriterContext(_context, contextType, _bsonStream.Position);
_contexts.Value.Push(_context);
_context = new(contextType, _bsonStream.Position);
Copy link
Contributor

Choose a reason for hiding this comment

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

PushContext(new(contextType, _bsonStream.Position));

Copy link
Contributor

Choose a reason for hiding this comment

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

Add new helper methods:

private void PopContext()
{
    _context = _contextStack.Pop();
}
 
private void PushContext(BsonBinaryWriterContext context)
{                                                        
    _contextStack.Push(_context);                        
    _context = context;                                  
}                                                        

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

@Copilot Copilot AI review requested due to automatic review settings October 17, 2025 23:22
Copy link

@Copilot 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 converts BsonBinaryReaderContext and BsonBinaryWriterContext from reference types (classes) to value types (structs) to reduce heap allocations on hot paths during BSON serialization/deserialization.

Key changes:

  • Converting context classes to structs with primary constructors
  • Implementing stack-based context management to replace parent-child context chains
  • Refactoring bookmark restoration logic to work with value-type contexts

Reviewed Changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/MongoDB.Bson/IO/BsonBinaryWriterContext.cs Converts class to struct with primary constructor and init-only properties
src/MongoDB.Bson/IO/BsonBinaryWriter.cs Adds context stack and helper methods to manage struct-based contexts
src/MongoDB.Bson/IO/BsonBinaryReaderContext.cs Converts class to struct, removes Clone/PopContext methods, renames properties
src/MongoDB.Bson/IO/BsonBinaryReaderBookmark.cs Updates bookmark to store context array instead of cloning context chain
src/MongoDB.Bson/IO/BsonBinaryReader.cs Adds context stack, updates context management, refactors dotted name generation
tests/MongoDB.Bson.Tests/IO/BsonBinaryReaderTests.cs Adds bookmark test and simplifies binary data subtype tests

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

throw new FormatException($"Expected size to be {_context.Size}, not {actualSize}.");
}

_context =_contexts.Pop();
Copy link

Copilot AI Oct 17, 2025

Choose a reason for hiding this comment

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

Missing space after '=' operator.

Suggested change
_context =_contexts.Pop();
_context = _contexts.Pop();

Copilot uses AI. Check for mistakes.

{
var indexElementName = context.CurrentArrayIndex.ToString(NumberFormatInfo.InvariantInfo);
return GenerateDottedElementName(context.ParentContext, indexElementName + "." + elementName);
return GenerateDottedElementName(contexts, nextIndex, (context.ElementName ?? "?") + "." + elementName);
Copy link

Copilot AI Oct 17, 2025

Choose a reason for hiding this comment

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

Extra space after comma in method calls.

Suggested change
return GenerateDottedElementName(contexts, nextIndex, (context.ElementName ?? "?") + "." + elementName);
return GenerateDottedElementName(contexts, nextIndex, (context.ElementName ?? "?") + "." + elementName);

Copilot uses AI. Check for mistakes.

{
return GenerateDottedElementName(context.ParentContext, "?." + elementName);
var indexElementName = context.ArrayIndex.ToString(NumberFormatInfo.InvariantInfo);
return GenerateDottedElementName(contexts, nextIndex, indexElementName + "." + elementName);
Copy link

Copilot AI Oct 17, 2025

Choose a reason for hiding this comment

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

Extra space after comma in method calls.

Suggested change
return GenerateDottedElementName(contexts, nextIndex, indexElementName + "." + elementName);
return GenerateDottedElementName(contexts, nextIndex, indexElementName + "." + elementName);

Copilot uses AI. Check for mistakes.

#pragma warning restore CA2213 // Disposable never disposed
private readonly BsonStream _bsonStream;
private BsonBinaryReaderContext _context;
private readonly Lazy<Stack<BsonBinaryReaderContext>> _contexts = new(() => new());
Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's a good suggestion, done.

return new BsonBinaryReaderBookmark(State, CurrentBsonType, CurrentName, _context, _bsonStream.Position);
}
public override BsonReaderBookmark GetBookmark() =>
new BsonBinaryReaderBookmark(State, CurrentBsonType, CurrentName, _context, _contexts.Value.ToArray(), _bsonStream.Position);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.


_context = _context.PopContext(_bsonStream.Position);
_context = PopContext();

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good idea, done.

}

_context = _context.PopContext(_bsonStream.Position);
_context = PopContext();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

if (_context.ContextType == ContextType.JavaScriptWithScope)
{
_context = _context.PopContext(_bsonStream.Position); // JavaScriptWithScope
_context = PopContext(); // JavaScriptWithScope
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

{
BackpatchSize(); // size of the JavaScript with scope value
_context = _context.ParentContext;
_context = _contexts.Value.Pop();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.


_context = _context.ParentContext;
if (_context == null)
_context = _contexts.Value.Pop();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

BackpatchSize(); // size of document

_context = _context.ParentContext;
_context = _contexts.Value.Pop();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

#pragma warning restore CA2213 // Disposable never disposed
private readonly BsonStream _bsonStream;
private BsonBinaryWriterContext _context;
private readonly Lazy<Stack<BsonBinaryWriterContext>> _contexts = new(() => new());
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

Close();
}
catch { } // ignore exceptions
catch { /* ignore exceptions */ }
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Make Rider happier.

@BorisDog BorisDog requested a review from rstam October 17, 2025 23:23
Copy link
Contributor

@rstam rstam left a comment

Choose a reason for hiding this comment

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

A few minor comments. Would be willing to LGTM as is if you don't want to make the suggested changes.

#pragma warning restore CA2213 // Disposable never disposed
private readonly BsonStream _bsonStream;
private BsonBinaryReaderContext _context;
private readonly Stack<BsonBinaryReaderContext> _contexts = new(4);
Copy link
Contributor

Choose a reason for hiding this comment

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

You didn't like the suggestion to rename this field to _contextStack?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

#pragma warning restore CA2213 // Disposable never disposed
private readonly BsonStream _bsonStream;
private BsonBinaryWriterContext _context;
private readonly Stack<BsonBinaryWriterContext> _contexts = new(4);
Copy link
Contributor

Choose a reason for hiding this comment

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

You didn't like the suggestion to rename this field to _contextStack?

}
}

private void PopContext()
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we want these in alphabetical order?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

BsonType currentBsonType,
string currentName,
BsonBinaryReaderContext currentContext,
Stack<BsonBinaryReaderContext> contextsStack,
Copy link
Contributor

Choose a reason for hiding this comment

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

I think contextStack sounds more grammatical than contextsStack.

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.

using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
Copy link
Contributor

Choose a reason for hiding this comment

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

Unused using.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch, done.

@BorisDog BorisDog requested a review from rstam October 22, 2025 19:37
Copy link
Contributor

@rstam rstam left a comment

Choose a reason for hiding this comment

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

LGTM

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants