Skip to content

perf(NODE-7660): serialize bulk write documents a single time#4999

Open
tadjik1 wants to merge 11 commits into
mainfrom
perf_bulk_single_pass_serialization
Open

perf(NODE-7660): serialize bulk write documents a single time#4999
tadjik1 wants to merge 11 commits into
mainfrom
perf_bulk_single_pass_serialization

Conversation

@tadjik1

@tadjik1 tadjik1 commented Jul 8, 2026

Copy link
Copy Markdown
Member

Description

Summary of Changes

collection.insertMany()/bulkWrite() processed every document twice - once via BSON.calculateObjectSize (for the max-size check and byte-based batch splitting) and again when the command was built. This change serializes each operation once in addToOperationsList, stores the buffer on the Batch, and reuses it as an OP_MSG document sequence (payload type 1) in the insert/update/delete command builders - reusing the mechanism added for MongoClient.bulkWrite in NODE-6325 (#4201).

The array (payload type 0) path is retained under auto-encryption and for non-bulk callers. Command monitoring's extractCommand now reconstructs documents/updates/deletes sequences back into arrays. Behavior is preserved, including the default ignoreUndefined: false semantics.

Notes for Reviewers
  • calculateObjectSize is no longer called anywhere in driver source (only re-exported as public API).
  • Single-document CRUD and MongoClient.bulkWrite are unaffected - they never took the redundant pass.

Whooping 60% boost in large doc bulkwrite!

image image

Release Highlight

Bulk writes serialize each document only once

insertMany and bulkWrite previously processed each document twice - once to measure its size for batch splitting (a full recursive walk via calculateObjectSize) and again to serialize it into the command sent to the server. Documents are now serialized a single time and the resulting bytes are reused for both, decreasing the BSON-encoding CPU spent on bulk writes and reducing event-loop blocking during large batches. The improvement is most noticeable with high document counts and documents that have many fields.

Double check the following

  • Lint is passing (npm run check:lint)
  • Self-review completed using the steps outlined here
  • PR title follows the correct format: type(NODE-xxxx)[!]: description
    • Example: feat(NODE-1234)!: rewriting everything in coffeescript
  • Changes are covered by tests
  • New TODOs have a related JIRA ticket

@tadjik1
tadjik1 force-pushed the perf_bulk_single_pass_serialization branch from 9938e75 to 343bc2b Compare July 8, 2026 13:22
insertMany/bulkWrite serialized each document twice: once via
BSON.calculateObjectSize for the max-size check and byte-based batch
splitting, then again when the command was serialized. Now each op is
serialized once in addToOperationsList (using the bulk op's resolved BSON
options), the buffer is stored on the Batch and reused as an OP_MSG
payload type-1 document sequence in Insert/Update/Delete buildCommandDocument.
@tadjik1
tadjik1 force-pushed the perf_bulk_single_pass_serialization branch from 343bc2b to e4f98ee Compare July 8, 2026 14:27
@tadjik1 tadjik1 changed the title perf: single-pass BSON serialization for collection bulk writes perf(NODE-7660): single-pass BSON serialization for collection bulk writes Jul 8, 2026
@tadjik1 tadjik1 changed the title perf(NODE-7660): single-pass BSON serialization for collection bulk writes perf(NODE-7660): serialize bulk write documents a single time Jul 8, 2026
Reassign each batch's serializedOperations to an empty array once the batch
has been executed so retained serialized bytes stay bounded to roughly one
in-flight batch rather than the entire bulk write payload. The operation keeps
its own reference for retries, so this only drops the bulk operation's copy.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 optimizes legacy bulk write paths (insertMany() / initialize*BulkOp() / bulkWrite()) by serializing each operation document exactly once, reusing the pre-serialized BSON bytes to build OP_MSG payload type 1 document sequences (instead of re-serializing during command construction). It also ensures command monitoring continues to report array-shaped documents/updates/deletes fields even when the wire message uses document sequences.

Changes:

  • Add a helper to build DocumentSequence objects from pre-serialized BSON and update insert/update/delete command builders to use it when buffers are available.
  • Serialize bulk operation documents once during batch building, store buffers on Batch, and reuse them when constructing write commands (except under auto-encryption).
  • Update command monitoring extraction + tests to reconstruct document sequences into arrays and validate ignoreUndefined default behavior remains unchanged.

Reviewed changes

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

Show a summary per file
File Description
test/unit/operations/write_ops.test.ts Adds unit coverage that update/delete operations use DocumentSequence when buffers are provided.
test/unit/operations/insert.test.ts Adds unit coverage that insert uses DocumentSequence when buffers are provided and arrays otherwise.
test/unit/cmap/command_monitoring_events.test.js Adds unit coverage ensuring command monitoring reconstructs document sequences into arrays.
test/unit/assorted/collations.test.js Adapts a mock-server test to handle updates arriving as a document sequence.
test/integration/crud/bulk.test.ts Adds integration coverage for ignoreUndefined default behavior, monitoring shape, and buffer release semantics.
src/operations/update.ts Allows update command building to use a DocumentSequence when pre-serialized statements are provided.
src/operations/insert.ts Allows insert command building to use a DocumentSequence when pre-serialized documents are provided.
src/operations/delete.ts Allows delete command building to use a DocumentSequence when pre-serialized statements are provided.
src/cmap/commands.ts Adds makeDocumentSequence() to build document sequences without re-serializing.
src/cmap/command_monitoring_events.ts Converts any top-level DocumentSequence fields back into arrays for monitoring events.
src/bulk/unordered.ts Serializes each operation once during batch construction and stores the buffer for reuse.
src/bulk/ordered.ts Serializes each operation once during batch construction and stores the buffer for reuse.
src/bulk/common.ts Stores per-operation serialized buffers on Batch, passes them into operations when allowed, and clears buffers after execution.

Comment thread src/cmap/commands.ts
Comment thread test/unit/cmap/command_monitoring_events.test.js Outdated
Comment thread src/bulk/common.ts Outdated
Comment thread src/bulk/ordered.ts
Comment thread src/bulk/unordered.ts
tadjik1 added 3 commits July 8, 2026 18:08
…ncryption

Under auto-encryption the command is sent as a BSON array (not a document
sequence), so pre-serialized buffers are never reused. Measure the document
size with calculateObjectSize instead of a full serialize and do not retain
buffers on the batch, restoring the pre-document-sequence behavior for the
encrypted path.
@tadjik1
tadjik1 marked this pull request as ready for review July 9, 2026 09:16
@tadjik1
tadjik1 requested a review from a team as a code owner July 9, 2026 09:16
@tadjik1
tadjik1 requested a review from nbbeeken July 9, 2026 09:31
@dariakp
dariakp marked this pull request as draft July 9, 2026 13:59
nbbeeken
nbbeeken previously approved these changes Jul 9, 2026

@nbbeeken nbbeeken left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM nice to see it come together! just to clarify something though: the language in the notes says something about serializing twice but calculateObjectSize doesn't serialize but it does recursively run over the whole object which is probably the source of the bad perf esp for very large insertMany inputs.

There are probably perf improvements that can be made to calc object size.. but that's a diff story..

@tadjik1

tadjik1 commented Jul 10, 2026

Copy link
Copy Markdown
Member Author

@nbbeeken yep, thanks! Changed the wording to "processed" and "walked", calcSize is not serializing indeed, only traverse everything.

@tadjik1
tadjik1 marked this pull request as ready for review July 14, 2026 15:49

@PavelSafronov PavelSafronov left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think this introduces a breaking change where we end up ignoring BulkWriteOptions passed to execute.

Comment thread src/bulk/common.ts
Comment thread src/bulk/ordered.ts
const bson = this.s.bsonOptions;
buffer = BSON.serialize(document, {
checkKeys: this.s.checkKeys,
ignoreUndefined: bson.ignoreUndefined,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We are serializing documents here according to current bson.ignoreUndefined, but it's possible for the user to pass different options to execute, so this would be a behavior change.

Example:

const bulk = collection.initializeOrderedBulkOp();
bulk.insert({ _id: 1, a: undefined });
await bulk.execute({ ignoreUndefined: true });

We're now going to serialize on insert and use ignoreUndefined: false, so we'll end up ignoring the options on execute.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

That's awesome, thanks! I wasn't aware that user can supply ignoreUndefined in so so many places.
Addressed in latest commit.

@PavelSafronov PavelSafronov left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looks good! Approving and moving to team review.

@PavelSafronov PavelSafronov self-assigned this Jul 16, 2026
@PavelSafronov PavelSafronov added the Team Review Needs review from team label Jul 16, 2026
@tadjik1
tadjik1 requested a review from a team July 17, 2026 08:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Team Review Needs review from team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants