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

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions