Skip to content

Commit 862d1a5

Browse files
authored
Merge pull request #15055 from Automattic/vkarpov15/gh-15029
perf: cache results from getAllSubdocs() on saveOptions, only loop through known subdoc properties
2 parents 768d460 + 8f774f0 commit 862d1a5

File tree

6 files changed

+115
-49
lines changed

6 files changed

+115
-49
lines changed

Diff for: benchmarks/saveSimple.js

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
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+
const FooSchema = new mongoose.Schema({
13+
prop1: String,
14+
prop2: String,
15+
prop3: String,
16+
prop4: String,
17+
prop5: String,
18+
prop6: String,
19+
prop7: String,
20+
prop8: String,
21+
prop9: String,
22+
prop10: String
23+
});
24+
const FooModel = mongoose.model('Foo', FooSchema);
25+
26+
if (!process.env.MONGOOSE_BENCHMARK_SKIP_SETUP) {
27+
await FooModel.deleteMany({});
28+
}
29+
30+
const numIterations = 500;
31+
const saveStart = Date.now();
32+
for (let i = 0; i < numIterations; ++i) {
33+
for (let j = 0; j < 10; ++j) {
34+
const doc = new FooModel({
35+
prop1: `test ${i}`,
36+
prop2: `test ${i}`,
37+
prop3: `test ${i}`,
38+
prop4: `test ${i}`,
39+
prop5: `test ${i}`,
40+
prop6: `test ${i}`,
41+
prop7: `test ${i}`,
42+
prop8: `test ${i}`,
43+
prop9: `test ${i}`,
44+
prop10: `test ${i}`
45+
});
46+
await doc.save();
47+
}
48+
}
49+
const saveEnd = Date.now();
50+
51+
const results = {
52+
'Average save time ms': +((saveEnd - saveStart) / numIterations).toFixed(2)
53+
};
54+
55+
console.log(JSON.stringify(results, null, ' '));
56+
process.exit(0);
57+
}

Diff for: lib/document.js

+37-43
Original file line numberDiff line numberDiff line change
@@ -2711,7 +2711,7 @@ function _getPathsToValidate(doc, pathsToValidate, pathsToSkip, isNestedValidate
27112711

27122712
if (!isNestedValidate) {
27132713
// 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();
2714+
const subdocs = doc.$getAllSubdocs({ useCache: true });
27152715
const modifiedPaths = doc.modifiedPaths();
27162716
for (const subdoc of subdocs) {
27172717
if (subdoc.$basePath) {
@@ -3482,7 +3482,7 @@ Document.prototype.$__reset = function reset() {
34823482
let _this = this;
34833483

34843484
// Skip for subdocuments
3485-
const subdocs = !this.$isSubdocument ? this.$getAllSubdocs() : null;
3485+
const subdocs = !this.$isSubdocument ? this.$getAllSubdocs({ useCache: true }) : null;
34863486
if (subdocs && subdocs.length > 0) {
34873487
for (const subdoc of subdocs) {
34883488
subdoc.$__reset();
@@ -3672,64 +3672,58 @@ Document.prototype.$__getArrayPathsToValidate = function() {
36723672
/**
36733673
* Get all subdocs (by bfs)
36743674
*
3675+
* @param {Object} [options] options. Currently for internal use.
36753676
* @return {Array}
36763677
* @api public
36773678
* @method $getAllSubdocs
36783679
* @memberOf Document
36793680
* @instance
36803681
*/
36813682

3682-
Document.prototype.$getAllSubdocs = function() {
3683+
Document.prototype.$getAllSubdocs = function(options) {
3684+
if (options?.useCache && this.$__.saveOptions?.__subdocs) {
3685+
return this.$__.saveOptions.__subdocs;
3686+
}
3687+
36833688
DocumentArray || (DocumentArray = require('./types/documentArray'));
36843689
Embedded = Embedded || require('./types/arraySubdocument');
36853690

3686-
function docReducer(doc, seed, path) {
3687-
let val = doc;
3688-
let isNested = false;
3689-
if (path) {
3690-
if (doc instanceof Document && doc[documentSchemaSymbol].paths[path]) {
3691-
val = doc._doc[path];
3692-
} else if (doc instanceof Document && doc[documentSchemaSymbol].nested[path]) {
3693-
val = doc._doc[path];
3694-
isNested = true;
3695-
} else {
3696-
val = doc[path];
3691+
const subDocs = [];
3692+
function getSubdocs(doc) {
3693+
const newSubdocs = [];
3694+
for (const { path } of doc.$__schema.childSchemas) {
3695+
const val = doc.$__getValue(path);
3696+
if (val == null) {
3697+
continue;
36973698
}
3698-
}
3699-
if (val instanceof Embedded) {
3700-
seed.push(val);
3701-
} else if (val instanceof Map) {
3702-
seed = Array.from(val.keys()).reduce(function(seed, path) {
3703-
return docReducer(val.get(path), seed, null);
3704-
}, seed);
3705-
} else if (val && !Array.isArray(val) && val.$isSingleNested) {
3706-
seed = Object.keys(val._doc).reduce(function(seed, path) {
3707-
return docReducer(val, seed, path);
3708-
}, seed);
3709-
seed.push(val);
3710-
} else if (val && utils.isMongooseDocumentArray(val)) {
3711-
val.forEach(function _docReduce(doc) {
3712-
if (!doc || !doc._doc) {
3713-
return;
3699+
if (val.$__) {
3700+
newSubdocs.push(val);
3701+
}
3702+
if (Array.isArray(val)) {
3703+
for (const el of val) {
3704+
if (el != null && el.$__) {
3705+
newSubdocs.push(el);
3706+
}
37143707
}
3715-
seed = Object.keys(doc._doc).reduce(function(seed, path) {
3716-
return docReducer(doc._doc, seed, path);
3717-
}, seed);
3718-
if (doc instanceof Embedded) {
3719-
seed.push(doc);
3708+
}
3709+
if (val instanceof Map) {
3710+
for (const el of val.values()) {
3711+
if (el != null && el.$__) {
3712+
newSubdocs.push(el);
3713+
}
37203714
}
3721-
});
3722-
} else if (isNested && val != null) {
3723-
for (const path of Object.keys(val)) {
3724-
docReducer(val, seed, path);
37253715
}
37263716
}
3727-
return seed;
3717+
for (const subdoc of newSubdocs) {
3718+
getSubdocs(subdoc);
3719+
}
3720+
subDocs.push(...newSubdocs);
37283721
}
37293722

3730-
const subDocs = [];
3731-
for (const path of Object.keys(this._doc)) {
3732-
docReducer(this, subDocs, path);
3723+
getSubdocs(this);
3724+
3725+
if (this.$__.saveOptions) {
3726+
this.$__.saveOptions.__subdocs = subDocs;
37333727
}
37343728

37353729
return subDocs;

Diff for: lib/model.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -3146,7 +3146,7 @@ function _setIsNew(doc, val) {
31463146
doc.$emit('isNew', val);
31473147
doc.constructor.emit('isNew', val);
31483148

3149-
const subdocs = doc.$getAllSubdocs();
3149+
const subdocs = doc.$getAllSubdocs({ useCache: true });
31503150
for (const subdoc of subdocs) {
31513151
subdoc.$isNew = val;
31523152
subdoc.$emit('isNew', val);

Diff for: lib/options/saveOptions.js

+2
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,6 @@ class SaveOptions {
1111
}
1212
}
1313

14+
SaveOptions.prototype.__subdocs = null;
15+
1416
module.exports = SaveOptions;

Diff for: lib/plugins/saveSubdocs.js

+4-2
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ module.exports = function saveSubdocs(schema) {
1515
}
1616

1717
const _this = this;
18-
const subdocs = this.$getAllSubdocs();
18+
const subdocs = this.$getAllSubdocs({ useCache: true });
1919

2020
if (!subdocs.length) {
2121
next();
@@ -27,6 +27,8 @@ module.exports = function saveSubdocs(schema) {
2727
cb(err);
2828
});
2929
}, function(error) {
30+
// Bust subdocs cache because subdoc pre hooks can add new subdocuments
31+
_this.$__.saveOptions.__subdocs = null;
3032
if (error) {
3133
return _this.$__schema.s.hooks.execPost('save:error', _this, [_this], { error: error }, function(error) {
3234
next(error);
@@ -64,7 +66,7 @@ module.exports = function saveSubdocs(schema) {
6466
}
6567

6668
const _this = this;
67-
const subdocs = this.$getAllSubdocs();
69+
const subdocs = this.$getAllSubdocs({ useCache: true });
6870

6971
if (!subdocs.length) {
7072
return;

Diff for: lib/schema.js

+14-3
Original file line numberDiff line numberDiff line change
@@ -1126,6 +1126,13 @@ Schema.prototype.path = function(path, obj) {
11261126

11271127
this.paths[mapPath] = schemaType.$__schemaType;
11281128
this.mapPaths.push(this.paths[mapPath]);
1129+
if (schemaType.$__schemaType.$isSingleNested) {
1130+
this.childSchemas.push({
1131+
schema: schemaType.$__schemaType.schema,
1132+
model: schemaType.$__schemaType.caster,
1133+
path: path
1134+
});
1135+
}
11291136
}
11301137

11311138
if (schemaType.$isSingleNested) {
@@ -1154,7 +1161,8 @@ Schema.prototype.path = function(path, obj) {
11541161
schemaType.caster.base = this.base;
11551162
this.childSchemas.push({
11561163
schema: schemaType.schema,
1157-
model: schemaType.caster
1164+
model: schemaType.caster,
1165+
path: path
11581166
});
11591167
} else if (schemaType.$isMongooseDocumentArray) {
11601168
Object.defineProperty(schemaType.schema, 'base', {
@@ -1167,7 +1175,8 @@ Schema.prototype.path = function(path, obj) {
11671175
schemaType.casterConstructor.base = this.base;
11681176
this.childSchemas.push({
11691177
schema: schemaType.schema,
1170-
model: schemaType.casterConstructor
1178+
model: schemaType.casterConstructor,
1179+
path: path
11711180
});
11721181
}
11731182

@@ -1235,7 +1244,9 @@ function gatherChildSchemas(schema) {
12351244
for (const path of Object.keys(schema.paths)) {
12361245
const schematype = schema.paths[path];
12371246
if (schematype.$isMongooseDocumentArray || schematype.$isSingleNested) {
1238-
childSchemas.push({ schema: schematype.schema, model: schematype.caster });
1247+
childSchemas.push({ schema: schematype.schema, model: schematype.caster, path: path });
1248+
} else if (schematype.$isSchemaMap && schematype.$__schemaType.$isSingleNested) {
1249+
childSchemas.push({ schema: schematype.$__schemaType.schema, model: schematype.$__schemaType.caster, path: path });
12391250
}
12401251
}
12411252

0 commit comments

Comments
 (0)