Skip to content

decorateUpdateWithVersionKey is slow for large updates #15672

@orgads

Description

@orgads

Prerequisites

  • I have written a descriptive issue title
  • I have searched existing issues to ensure the performance issue has not already been reported

Last performant version

N/A

Slowed down in version

N/A

Node.js version

22.20.0

🦥 Performance issue

While profiling an application that executes many upsert updates with very large objects (~4M), we've noticed that modifiedPaths consumes noticeable amount of CPU.

The stack trace is:

modifiedPaths (common.js:101)
modifiedPaths (modifiedPaths.js:24)
decorateUpdateWithVersionKey (decorateUpdateWithVersionKey.js:15)
_update (model.js:4011)
updateOne (model.js:3944)

It looks like the entire object is translated, while the only thing we care about is whether it contains a version key. This could most probably be improved by using lookup instead of translating the while object.

Steps to Reproduce

import mongoose from 'mongoose';

await mongoose.connect('mongodb://127.0.0.1:27017/test');
// Create a 4M object with many sub-objects and then update with upsert
const TestSchema = new mongoose.Schema({
  name: String,
  data: mongoose.Schema.Types.Mixed
});
const TestModel = mongoose.model('Test', TestSchema);

const bigObject = {
  data: {}
};

// Create nested structure with 4 levels and target ~4MB
const createNestedLevel = (obj, currentLevel, maxLevel, keysPerLevel) => {
  if (currentLevel >= maxLevel) {
    // At the deepest level, add string values
    for (let i = 0; i < 4; i++) {  // Reduced to 4 leaves per branch
      obj[`leaf${i}`] = `value_${currentLevel}_${i}_`.padEnd(100, 'x'); // ~100 chars per leaf
    }
    return;
  }

  for (let i = 0; i < keysPerLevel; i++) {
    obj[`level${currentLevel}_${i}`] = {};
    createNestedLevel(obj[`level${currentLevel}_${i}`], currentLevel + 1, maxLevel, keysPerLevel);
  }
};

// Calculate structure: 10^4 = 10,000 final branches * 4 leaves ≈ 40,000 leaf nodes * ~100 chars ≈ 4MB
createNestedLevel(bigObject.data, 0, 4, 10);

for (let iteration = 0; iteration < 50; iteration++) {
  const name = `bigObject${iteration}`;

  console.time(`Upsert ${name} - Iteration ${iteration}`);
  await TestModel.updateOne(
    { name },
    { $set: { name: name, data: bigObject.data } },
    { upsert: true }
  );
  console.timeEnd(`Upsert ${name} - Iteration ${iteration}`);
}
await mongoose.disconnect();
Image

Expected Behavior

Lookup should be faster.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions