Skip to content

Commit 34ed360

Browse files
authored
Merge pull request Automattic#14910 from Automattic/vkarpov15/Automatticgh-14897
fix(document): avoid massive perf degradation when saving new doc with 10 level deep subdocs
2 parents 8522f39 + d273278 commit 34ed360

File tree

3 files changed

+93
-64
lines changed

3 files changed

+93
-64
lines changed
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
'use strict';
2+
3+
const mongoose = require('../');
4+
5+
run().catch(err => {
6+
console.error(err);
7+
process.exit(-1);
8+
});
9+
10+
async function run() {
11+
await mongoose.connect('mongodb://127.0.0.1:27017/mongoose_benchmark');
12+
13+
const levels = 12;
14+
15+
let schema = new mongoose.Schema({ test: { type: String, required: true } });
16+
let doc = { test: 'gh-14897' };
17+
for (let i = 0; i < levels; ++i) {
18+
schema = new mongoose.Schema({ level: Number, subdocs: [schema] });
19+
doc = { level: (levels - i), subdocs: [{ ...doc }, { ...doc }] };
20+
}
21+
const Test = mongoose.model('Test', schema);
22+
23+
if (!process.env.MONGOOSE_BENCHMARK_SKIP_SETUP) {
24+
await Test.deleteMany({});
25+
}
26+
27+
const insertStart = Date.now();
28+
await Test.create(doc);
29+
const insertEnd = Date.now();
30+
31+
const results = {
32+
'create() time ms': +(insertEnd - insertStart).toFixed(2)
33+
};
34+
35+
console.log(JSON.stringify(results, null, ' '));
36+
process.exit(0);
37+
}

lib/document.js

Lines changed: 35 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -2689,7 +2689,7 @@ function _evaluateRequiredFunctions(doc) {
26892689
* ignore
26902690
*/
26912691

2692-
function _getPathsToValidate(doc, pathsToValidate, pathsToSkip) {
2692+
function _getPathsToValidate(doc, pathsToValidate, pathsToSkip, isNestedValidate) {
26932693
const doValidateOptions = {};
26942694

26952695
_evaluateRequiredFunctions(doc);
@@ -2709,35 +2709,40 @@ function _getPathsToValidate(doc, pathsToValidate, pathsToSkip) {
27092709
Object.keys(doc.$__.activePaths.getStatePaths('default')).forEach(addToPaths);
27102710
function addToPaths(p) { paths.add(p); }
27112711

2712-
const subdocs = doc.$getAllSubdocs();
2713-
const modifiedPaths = doc.modifiedPaths();
2714-
for (const subdoc of subdocs) {
2715-
if (subdoc.$basePath) {
2716-
const fullPathToSubdoc = subdoc.$isSingleNested ? subdoc.$__pathRelativeToParent() : subdoc.$__fullPathWithIndexes();
2717-
2718-
// Remove child paths for now, because we'll be validating the whole
2719-
// subdoc.
2720-
// The following is a faster take on looping through every path in `paths`
2721-
// and checking if the path starts with `fullPathToSubdoc` re: gh-13191
2722-
for (const modifiedPath of subdoc.modifiedPaths()) {
2723-
paths.delete(fullPathToSubdoc + '.' + modifiedPath);
2724-
}
2712+
if (!isNestedValidate) {
2713+
// If we're validating a subdocument, all this logic will run anyway on the top-level document, so skip for subdocuments
2714+
const subdocs = doc.$getAllSubdocs();
2715+
const modifiedPaths = doc.modifiedPaths();
2716+
for (const subdoc of subdocs) {
2717+
if (subdoc.$basePath) {
2718+
const fullPathToSubdoc = subdoc.$isSingleNested ? subdoc.$__pathRelativeToParent() : subdoc.$__fullPathWithIndexes();
2719+
2720+
// Remove child paths for now, because we'll be validating the whole
2721+
// subdoc.
2722+
// The following is a faster take on looping through every path in `paths`
2723+
// and checking if the path starts with `fullPathToSubdoc` re: gh-13191
2724+
for (const modifiedPath of subdoc.modifiedPaths()) {
2725+
paths.delete(fullPathToSubdoc + '.' + modifiedPath);
2726+
}
27252727

2726-
if (doc.$isModified(fullPathToSubdoc, null, modifiedPaths) &&
2727-
!doc.isDirectModified(fullPathToSubdoc) &&
2728-
!doc.$isDefault(fullPathToSubdoc)) {
2729-
paths.add(fullPathToSubdoc);
2728+
if (doc.$isModified(fullPathToSubdoc, null, modifiedPaths) &&
2729+
// Avoid using isDirectModified() here because that does additional checks on whether the parent path
2730+
// is direct modified, which can cause performance issues re: gh-14897
2731+
!doc.$__.activePaths.getStatePaths('modify').hasOwnProperty(fullPathToSubdoc) &&
2732+
!doc.$isDefault(fullPathToSubdoc)) {
2733+
paths.add(fullPathToSubdoc);
27302734

2731-
if (doc.$__.pathsToScopes == null) {
2732-
doc.$__.pathsToScopes = {};
2733-
}
2734-
doc.$__.pathsToScopes[fullPathToSubdoc] = subdoc.$isDocumentArrayElement ?
2735-
subdoc.__parentArray :
2736-
subdoc.$parent();
2735+
if (doc.$__.pathsToScopes == null) {
2736+
doc.$__.pathsToScopes = {};
2737+
}
2738+
doc.$__.pathsToScopes[fullPathToSubdoc] = subdoc.$isDocumentArrayElement ?
2739+
subdoc.__parentArray :
2740+
subdoc.$parent();
27372741

2738-
doValidateOptions[fullPathToSubdoc] = { skipSchemaValidators: true };
2739-
if (subdoc.$isDocumentArrayElement && subdoc.__index != null) {
2740-
doValidateOptions[fullPathToSubdoc].index = subdoc.__index;
2742+
doValidateOptions[fullPathToSubdoc] = { skipSchemaValidators: true };
2743+
if (subdoc.$isDocumentArrayElement && subdoc.__index != null) {
2744+
doValidateOptions[fullPathToSubdoc].index = subdoc.__index;
2745+
}
27412746
}
27422747
}
27432748
}
@@ -2972,7 +2977,7 @@ Document.prototype.$__validate = function(pathsToValidate, options, callback) {
29722977
paths = [...paths];
29732978
doValidateOptionsByPath = {};
29742979
} else {
2975-
const pathDetails = _getPathsToValidate(this, pathsToValidate, pathsToSkip);
2980+
const pathDetails = _getPathsToValidate(this, pathsToValidate, pathsToSkip, options && options._nestedValidate);
29762981
paths = shouldValidateModifiedOnly ?
29772982
pathDetails[0].filter((path) => this.$isModified(path)) :
29782983
pathDetails[0];
@@ -3059,7 +3064,8 @@ Document.prototype.$__validate = function(pathsToValidate, options, callback) {
30593064
const doValidateOptions = {
30603065
...doValidateOptionsByPath[path],
30613066
path: path,
3062-
validateAllPaths
3067+
validateAllPaths,
3068+
_nestedValidate: true
30633069
};
30643070

30653071
schemaType.doValidate(val, function(err) {
@@ -3478,44 +3484,9 @@ Document.prototype.$__reset = function reset() {
34783484
// Skip for subdocuments
34793485
const subdocs = !this.$isSubdocument ? this.$getAllSubdocs() : null;
34803486
if (subdocs && subdocs.length > 0) {
3481-
const resetArrays = new Set();
34823487
for (const subdoc of subdocs) {
3483-
const fullPathWithIndexes = subdoc.$__fullPathWithIndexes();
34843488
subdoc.$__reset();
3485-
if (this.isModified(fullPathWithIndexes) || isParentInit(fullPathWithIndexes)) {
3486-
if (subdoc.$isDocumentArrayElement) {
3487-
resetArrays.add(subdoc.parentArray());
3488-
} else {
3489-
const parent = subdoc.$parent();
3490-
if (parent === this) {
3491-
this.$__.activePaths.clearPath(subdoc.$basePath);
3492-
} else if (parent != null && parent.$isSubdocument) {
3493-
// If map path underneath subdocument, may end up with a case where
3494-
// map path is modified but parent still needs to be reset. See gh-10295
3495-
parent.$__reset();
3496-
}
3497-
}
3498-
}
3499-
}
3500-
3501-
for (const array of resetArrays) {
3502-
this.$__.activePaths.clearPath(array.$path());
3503-
array[arrayAtomicsBackupSymbol] = array[arrayAtomicsSymbol];
3504-
array[arrayAtomicsSymbol] = {};
3505-
}
3506-
}
3507-
3508-
function isParentInit(path) {
3509-
path = path.indexOf('.') === -1 ? [path] : path.split('.');
3510-
let cur = '';
3511-
for (let i = 0; i < path.length; ++i) {
3512-
cur += (cur.length ? '.' : '') + path[i];
3513-
if (_this.$__.activePaths[cur] === 'init') {
3514-
return true;
3515-
}
35163489
}
3517-
3518-
return false;
35193490
}
35203491

35213492
// clear atomics

test/document.test.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13905,6 +13905,27 @@ describe('document', function() {
1390513905
const objectWithGetters = result.toObject({ getters: true, virtuals: false });
1390613906
assert.strictEqual(objectWithGetters.level1.level2.level3.property, 'TESTVALUE');
1390713907
});
13908+
13909+
it('handles inserting and saving large document with 10-level deep subdocs (gh-14897)', async function() {
13910+
const levels = 10;
13911+
13912+
let schema = new Schema({ test: { type: String, required: true } });
13913+
let doc = { test: 'gh-14897' };
13914+
for (let i = 0; i < levels; ++i) {
13915+
schema = new Schema({ level: Number, subdocs: [schema] });
13916+
doc = { level: (levels - i), subdocs: [{ ...doc }, { ...doc }] };
13917+
}
13918+
13919+
const Test = db.model('Test', schema);
13920+
const savedDoc = await Test.create(doc);
13921+
13922+
let cur = savedDoc;
13923+
for (let i = 0; i < levels - 1; ++i) {
13924+
cur = cur.subdocs[0];
13925+
}
13926+
cur.subdocs[0] = { test: 'updated' };
13927+
await savedDoc.save();
13928+
});
1390813929
});
1390913930

1391013931
describe('Check if instance function that is supplied in schema option is available', function() {

0 commit comments

Comments
 (0)