-
Notifications
You must be signed in to change notification settings - Fork 1.3k
CSHARP-5348: Avoid allocations for Bson*Context #1791
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
base: main
Are you sure you want to change the base?
Conversation
_bsonStream = (stream as BsonStream) ?? new BsonStreamAdapter(stream); | ||
|
||
_context = null; | ||
_context = new(ContextType.TopLevel, 0); |
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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()); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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);
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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);
There was a problem hiding this comment.
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(); | ||
|
There was a problem hiding this comment.
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();
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PopContext();
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PopContext(); // JavaScriptWithScope
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PopContext();
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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));
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PopContext();
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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));
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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));
There was a problem hiding this comment.
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;
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
There was a problem hiding this 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(); |
Copilot
AI
Oct 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing space after '=' operator.
_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); |
Copilot
AI
Oct 17, 2025
There was a problem hiding this comment.
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.
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); |
Copilot
AI
Oct 17, 2025
There was a problem hiding this comment.
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.
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()); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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(); | ||
|
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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()); |
There was a problem hiding this comment.
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 */ } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Make Rider happier.
There was a problem hiding this 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); |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unused using.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch, done.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
Convert
BsonBinaryWriterContext
andBsonBinaryReaderContext
to structs, to reduces allocations on a hot path.