From 4a2e65ed9651bd859e86d60a2172499819e6173a Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sat, 25 Jun 2022 14:19:39 -0400 Subject: [PATCH 1/6] perf(document): avoid creating unnecessary empty objects when creating a state machine Re: #11541 --- lib/document.js | 36 +++++++++---------- lib/helpers/document/cleanModifiedSubpaths.js | 6 ++-- lib/helpers/document/compile.js | 13 +++---- lib/statemachine.js | 29 ++++++++++----- 4 files changed, 49 insertions(+), 35 deletions(-) diff --git a/lib/document.js b/lib/document.js index bef20b9f560..0bd00098148 100644 --- a/lib/document.js +++ b/lib/document.js @@ -1588,7 +1588,7 @@ Document.prototype.$__shouldModify = function(pathToMark, path, options, constru return true; } - if (val === void 0 && path in this.$__.activePaths.states.default) { + if (val === void 0 && path in this.$__.activePaths.getStatePaths('default')) { // we're just unsetting the default value which was never saved return false; } @@ -1608,7 +1608,7 @@ Document.prototype.$__shouldModify = function(pathToMark, path, options, constru if (!constructing && val !== null && val !== undefined && - path in this.$__.activePaths.states.default && + path in this.$__.activePaths.getStatePaths('default') && deepEqual(val, schema.getDefault(this, constructing))) { // a path with a default was $unset on the server // and the user is setting it to the same value again @@ -1906,7 +1906,7 @@ Document.prototype.$ignore = function(path) { */ Document.prototype.directModifiedPaths = function() { - return Object.keys(this.$__.activePaths.states.modify); + return Object.keys(this.$__.activePaths.getStatePaths('modify')); }; /** @@ -1984,7 +1984,7 @@ function _isEmpty(v) { Document.prototype.modifiedPaths = function(options) { options = options || {}; - const directModifiedPaths = Object.keys(this.$__.activePaths.states.modify); + const directModifiedPaths = Object.keys(this.$__.activePaths.getStatePaths('modify')); const result = new Set(); let i = 0; @@ -2063,7 +2063,7 @@ Document.prototype[documentModifiedPaths] = Document.prototype.modifiedPaths; Document.prototype.isModified = function(paths, modifiedPaths) { if (paths) { - const directModifiedPaths = Object.keys(this.$__.activePaths.states.modify); + const directModifiedPaths = Object.keys(this.$__.activePaths.getStatePaths('modify')); if (directModifiedPaths.length === 0) { return false; } @@ -2113,7 +2113,7 @@ Document.prototype.$isDefault = function(path) { } if (typeof path === 'string' && path.indexOf(' ') === -1) { - return this.$__.activePaths.states.default.hasOwnProperty(path); + return this.$__.activePaths.getStatePaths('default').hasOwnProperty(path); } let paths = path; @@ -2121,7 +2121,7 @@ Document.prototype.$isDefault = function(path) { paths = paths.split(' '); } - return paths.some(path => this.$__.activePaths.states.default.hasOwnProperty(path)); + return paths.some(path => this.$__.activePaths.getStatePaths('default').hasOwnProperty(path)); }; /** @@ -2174,7 +2174,7 @@ Document.prototype.isDirectModified = function(path) { } if (typeof path === 'string' && path.indexOf(' ') === -1) { - return this.$__.activePaths.states.modify.hasOwnProperty(path); + return this.$__.activePaths.getStatePaths('modify').hasOwnProperty(path); } let paths = path; @@ -2182,7 +2182,7 @@ Document.prototype.isDirectModified = function(path) { paths = paths.split(' '); } - return paths.some(path => this.$__.activePaths.states.modify.hasOwnProperty(path)); + return paths.some(path => this.$__.activePaths.getStatePaths('modify').hasOwnProperty(path)); }; /** @@ -2199,7 +2199,7 @@ Document.prototype.isInit = function(path) { } if (typeof path === 'string' && path.indexOf(' ') === -1) { - return this.$__.activePaths.states.init.hasOwnProperty(path); + return this.$__.activePaths.getStatePaths('init').hasOwnProperty(path); } let paths = path; @@ -2207,7 +2207,7 @@ Document.prototype.isInit = function(path) { paths = paths.split(' '); } - return paths.some(path => this.$__.activePaths.states.init.hasOwnProperty(path)); + return paths.some(path => this.$__.activePaths.getStatePaths('init').hasOwnProperty(path)); }; /** @@ -2435,7 +2435,7 @@ Document.prototype.$validate = Document.prototype.validate; */ function _evaluateRequiredFunctions(doc) { - const requiredFields = Object.keys(doc.$__.activePaths.states.require); + const requiredFields = Object.keys(doc.$__.activePaths.getStatePaths('require')); let i = 0; const len = requiredFields.length; for (i = 0; i < len; ++i) { @@ -2463,7 +2463,7 @@ function _getPathsToValidate(doc) { _evaluateRequiredFunctions(doc); // only validate required fields when necessary - let paths = new Set(Object.keys(doc.$__.activePaths.states.require).filter(function(path) { + let paths = new Set(Object.keys(doc.$__.activePaths.getStatePaths('require')).filter(function(path) { if (!doc.$__isSelected(path) && !doc.$isModified(path)) { return false; } @@ -2473,9 +2473,9 @@ function _getPathsToValidate(doc) { return true; })); - Object.keys(doc.$__.activePaths.states.init).forEach(addToPaths); - Object.keys(doc.$__.activePaths.states.modify).forEach(addToPaths); - Object.keys(doc.$__.activePaths.states.default).forEach(addToPaths); + Object.keys(doc.$__.activePaths.getStatePaths('init')).forEach(addToPaths); + Object.keys(doc.$__.activePaths.getStatePaths('modify')).forEach(addToPaths); + Object.keys(doc.$__.activePaths.getStatePaths('default')).forEach(addToPaths); function addToPaths(p) { paths.add(p); } const subdocs = doc.$getAllSubdocs(); @@ -3197,8 +3197,8 @@ Document.prototype.$__reset = function reset() { this.$__.backup = {}; this.$__.backup.activePaths = { - modify: Object.assign({}, this.$__.activePaths.states.modify), - default: Object.assign({}, this.$__.activePaths.states.default) + modify: Object.assign({}, this.$__.activePaths.getStatePaths('modify')), + default: Object.assign({}, this.$__.activePaths.getStatePaths('default')) }; this.$__.backup.validationError = this.$__.validationError; this.$__.backup.errors = this.$errors; diff --git a/lib/helpers/document/cleanModifiedSubpaths.js b/lib/helpers/document/cleanModifiedSubpaths.js index 29bd18c3ac9..43c225e4fd2 100644 --- a/lib/helpers/document/cleanModifiedSubpaths.js +++ b/lib/helpers/document/cleanModifiedSubpaths.js @@ -13,7 +13,7 @@ module.exports = function cleanModifiedSubpaths(doc, path, options) { return deleted; } - for (const modifiedPath of Object.keys(doc.$__.activePaths.states.modify)) { + for (const modifiedPath of Object.keys(doc.$__.activePaths.getStatePaths('modify'))) { if (skipDocArrays) { const schemaType = doc.$__schema.path(modifiedPath); if (schemaType && schemaType.$isMongooseDocumentArray) { @@ -21,13 +21,13 @@ module.exports = function cleanModifiedSubpaths(doc, path, options) { } } if (modifiedPath.startsWith(path + '.')) { - delete doc.$__.activePaths.states.modify[modifiedPath]; + doc.$__.activePaths.clearPath(modifiedPath); ++deleted; if (doc.$isSubdocument) { const owner = doc.ownerDocument(); const fullPath = doc.$__fullPath(modifiedPath); - delete owner.$__.activePaths.states.modify[fullPath]; + owner.$__.activePaths.clearPath(fullPath); } } } diff --git a/lib/helpers/document/compile.js b/lib/helpers/document/compile.js index 80a0de0b041..71597582535 100644 --- a/lib/helpers/document/compile.js +++ b/lib/helpers/document/compile.js @@ -17,6 +17,13 @@ const isPOJO = utils.isPOJO; exports.compile = compile; exports.defineKey = defineKey; +const _isEmptyOptions = Object.freeze({ + minimize: true, + virtuals: false, + getters: false, + transform: false +}); + /*! * Compiles schemas. */ @@ -130,12 +137,6 @@ function defineKey({ prop, subprops, prototype, prefix, options }) { value: true }); - const _isEmptyOptions = Object.freeze({ - minimize: true, - virtuals: false, - getters: false, - transform: false - }); Object.defineProperty(nested, '$isEmpty', { enumerable: false, configurable: true, diff --git a/lib/statemachine.js b/lib/statemachine.js index 43da62812e2..9eec8c15bf2 100644 --- a/lib/statemachine.js +++ b/lib/statemachine.js @@ -39,14 +39,6 @@ StateMachine.ctor = function() { this.paths = {}; this.states = {}; this.stateNames = states; - - let i = states.length, - state; - - while (i--) { - state = states[i]; - this.states[state] = {}; - } }; ctor.prototype = new StateMachine(); @@ -76,6 +68,7 @@ StateMachine.prototype._changeState = function _changeState(path, nextState) { if (prevBucket) delete prevBucket[path]; this.paths[path] = nextState; + this.states[nextState] = this.states[nextState] || {}; this.states[nextState][path] = true; }; @@ -84,6 +77,9 @@ StateMachine.prototype._changeState = function _changeState(path, nextState) { */ StateMachine.prototype.clear = function clear(state) { + if (this.states[state] == null) { + return; + } const keys = Object.keys(this.states[state]); let i = keys.length; let path; @@ -108,6 +104,17 @@ StateMachine.prototype.clearPath = function clearPath(path) { delete this.states[state][path]; }; +/*! + * Gets the paths for the given state, or empty object `{}` if none. + */ + +StateMachine.prototype.getStatePaths = function getStatePaths(state) { + if (this.states[state] != null) { + return this.states[state]; + } + return {}; +}; + /*! * Checks to see if at least one path is in the states passed in via `arguments` * e.g., this.some('required', 'inited') @@ -120,6 +127,9 @@ StateMachine.prototype.some = function some() { const _this = this; const what = arguments.length ? arguments : this.stateNames; return Array.prototype.some.call(what, function(state) { + if (_this.states[state] == null) { + return false; + } return Object.keys(_this.states[state]).length; }); }; @@ -143,6 +153,9 @@ StateMachine.prototype._iter = function _iter(iterMethod) { const _this = this; const paths = states.reduce(function(paths, state) { + if (_this.states[state] == null) { + return paths; + } return paths.concat(Object.keys(_this.states[state])); }, []); From 1ccbed63697e930daf3853539a6af17d39fd9e47 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sat, 25 Jun 2022 14:27:25 -0400 Subject: [PATCH 2/6] fix: missed a couple of spots in #11988 --- lib/connection.js | 3 +++ lib/plugins/trackTransaction.js | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/connection.js b/lib/connection.js index 310482ce06b..aba3b26772e 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -493,6 +493,9 @@ Connection.prototype.transaction = function transaction(fn, options) { for (const path of state.modifiedPaths) { doc.$__.activePaths.paths[path] = 'modify'; + if (doc.$__.activePaths.states.modify == null) { + doc.$__.activePaths.states.modify = {}; + } doc.$__.activePaths.states.modify[path] = true; } diff --git a/lib/plugins/trackTransaction.js b/lib/plugins/trackTransaction.js index 11444d16d45..0c99bbacff6 100644 --- a/lib/plugins/trackTransaction.js +++ b/lib/plugins/trackTransaction.js @@ -23,14 +23,14 @@ module.exports = function trackTransaction(schema) { initialState.versionKey = this.get(this.$__schema.options.versionKey); } - initialState.modifiedPaths = new Set(Object.keys(this.$__.activePaths.states.modify)); + initialState.modifiedPaths = new Set(Object.keys(this.$__.activePaths.getStatePaths('modify'))); initialState.atomics = _getAtomics(this); session[sessionNewDocuments].set(this, initialState); } else { const state = session[sessionNewDocuments].get(this); - for (const path of Object.keys(this.$__.activePaths.states.modify)) { + for (const path of Object.keys(this.$__.activePaths.getStatePaths('modify'))) { state.modifiedPaths.add(path); } state.atomics = _getAtomics(this, state.atomics); From 989525ce65c03f240c88f46662fe0abf5bfcd174 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sat, 25 Jun 2022 21:18:40 -0400 Subject: [PATCH 3/6] perf(document+statemachine): remove some unnecessary duplicated fields Re: #11541 --- lib/document.js | 13 +++++-------- lib/statemachine.js | 3 ++- test/docs/lean.test.js | 6 +++--- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/lib/document.js b/lib/document.js index 0bd00098148..b74b04988a6 100644 --- a/lib/document.js +++ b/lib/document.js @@ -120,9 +120,6 @@ function Document(obj, fields, skipId, options) { fields = undefined; } else { this.$__.strictMode = schema.options.strict; - if (fields != null) { - this.$__.selected = fields; - } } const requiredPaths = schema.requiredPaths(true); @@ -134,9 +131,9 @@ function Document(obj, fields, skipId, options) { // determine if this doc is a result of a query with // excluded fields - if (utils.isPOJO(fields)) { + if (utils.isPOJO(fields) && Object.keys(fields).length > 0) { exclude = isExclusive(fields); - this.$__.fields = fields; + this.$__.selected = fields; this.$__.exclude = exclude; } @@ -745,10 +742,10 @@ Document.prototype.$__init = function(doc, opts) { this.$emit('init', this); this.constructor.emit('init', this); - const hasIncludedChildren = this.$__.exclude === false && this.$__.fields ? - $__hasIncludedChildren(this.$__.fields) : + const hasIncludedChildren = this.$__.exclude === false && this.$__.selected ? + $__hasIncludedChildren(this.$__.selected) : null; - $__applyDefaults(this, this.$__.fields, this.$__.exclude, hasIncludedChildren, false, this.$__.skipDefaults); + $__applyDefaults(this, this.$__.selected, this.$__.exclude, hasIncludedChildren, false, this.$__.skipDefaults); return this; }; diff --git a/lib/statemachine.js b/lib/statemachine.js index 9eec8c15bf2..a524c999939 100644 --- a/lib/statemachine.js +++ b/lib/statemachine.js @@ -38,11 +38,12 @@ StateMachine.ctor = function() { StateMachine.apply(this, arguments); this.paths = {}; this.states = {}; - this.stateNames = states; }; ctor.prototype = new StateMachine(); + ctor.prototype.stateNames = states; + states.forEach(function(state) { // Changes the `path`'s state to `state`. ctor.prototype[state] = function(path) { diff --git a/test/docs/lean.test.js b/test/docs/lean.test.js index 79e6bd34b51..49cd0c45aad 100644 --- a/test/docs/lean.test.js +++ b/test/docs/lean.test.js @@ -32,15 +32,15 @@ describe('Lean Tutorial', function() { // To enable the `lean` option for a query, use the `lean()` function. const leanDoc = await MyModel.findOne().lean(); - v8Serialize(normalDoc).length; // approximately 300 - v8Serialize(leanDoc).length; // 32, more than 10x smaller! + v8Serialize(normalDoc).length; // approximately 180 + v8Serialize(leanDoc).length; // 32, about 5x smaller! // In case you were wondering, the JSON form of a Mongoose doc is the same // as the POJO. This additional memory only affects how much memory your // Node.js process uses, not how much data is sent over the network. JSON.stringify(normalDoc).length === JSON.stringify(leanDoc).length; // true // acquit:ignore:start - assert.ok(v8Serialize(normalDoc).length >= 300 && v8Serialize(normalDoc).length <= 800, v8Serialize(normalDoc).length); + assert.ok(v8Serialize(normalDoc).length >= 150 && v8Serialize(normalDoc).length <= 200, v8Serialize(normalDoc).length); assert.equal(v8Serialize(leanDoc).length, 32); assert.equal(JSON.stringify(normalDoc).length, JSON.stringify(leanDoc).length); // acquit:ignore:end From 9e05bafe2f221fdf78b3e591b96c75ffddc04e5a Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sat, 25 Jun 2022 21:49:03 -0400 Subject: [PATCH 4/6] perf(document+internal): put default value for `$isNew` and `strictMode` on prototype to avoid unnecessary memory usage Re: #11541 --- lib/document.js | 19 ++++++++++++++++--- lib/internal.js | 3 ++- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/lib/document.js b/lib/document.js index b74b04988a6..d30e789a099 100644 --- a/lib/document.js +++ b/lib/document.js @@ -93,7 +93,11 @@ function Document(obj, fields, skipId, options) { } this.$__ = new InternalCache(); - this.$isNew = 'isNew' in options ? options.isNew : true; + + // Avoid setting `isNew` to `true`, because it is `true` by default + if (options.isNew != null && options.isNew !== true) { + this.$isNew = options.isNew; + } if (options.priorDoc != null) { this.$__.priorDoc = options.priorDoc; @@ -116,9 +120,11 @@ function Document(obj, fields, skipId, options) { const schema = this.$__schema; if (typeof fields === 'boolean' || fields === 'throw') { - this.$__.strictMode = fields; + if (fields !== true) { + this.$__.strictMode = fields; + } fields = undefined; - } else { + } else if (schema.options.strict !== true) { this.$__.strictMode = schema.options.strict; } @@ -207,6 +213,13 @@ Object.defineProperty(Document.prototype, 'errors', { this.$errors = value; } }); + +/*! + * ignore + */ + +Document.prototype.$isNew = true; + /*! * Document exposes the NodeJS event emitter API, so you can use * `on`, `once`, etc. diff --git a/lib/internal.js b/lib/internal.js index a31714825f9..d7fe08d5b87 100644 --- a/lib/internal.js +++ b/lib/internal.js @@ -13,8 +13,9 @@ function InternalCache() { this.activePaths = new ActiveRoster(); } +InternalCache.prototype.strictMode = true; + InternalCache.prototype.fullPath = undefined; -InternalCache.prototype.strictMode = undefined; InternalCache.prototype.selected = undefined; InternalCache.prototype.shardval = undefined; InternalCache.prototype.saveError = undefined; From 3a1c82190053600be3a4bd9424bd2d4bcb285d60 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sat, 25 Jun 2022 22:23:17 -0400 Subject: [PATCH 5/6] perf(schema): correctly pull DocumentArrayPath default `_id` value when creating document array with schema re: #11541 --- lib/schema/documentarray.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/schema/documentarray.js b/lib/schema/documentarray.js index 4fd05fbe6aa..e6575f14d20 100644 --- a/lib/schema/documentarray.js +++ b/lib/schema/documentarray.js @@ -36,6 +36,13 @@ let Subdocument; */ function DocumentArrayPath(key, schema, options, schemaOptions) { + const schemaTypeIdOption = DocumentArrayPath.defaultOptions && + DocumentArrayPath.defaultOptions._id; + if (schemaTypeIdOption != null) { + schemaOptions = schemaOptions || {}; + schemaOptions._id = schemaTypeIdOption; + } + if (schemaOptions != null && schemaOptions._id != null) { schema = handleIdOption(schema, schemaOptions); } else if (options != null && options._id != null) { From fa29f3a5ec76672b8332b961c8a74372fc6e41f0 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sun, 17 Jul 2022 20:26:42 -0400 Subject: [PATCH 6/6] perf(connection): move unnecessary check out of for loop Re: #11988 --- lib/connection.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/connection.js b/lib/connection.js index 206b2ca2c7b..797d4f50aef 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -491,11 +491,11 @@ Connection.prototype.transaction = function transaction(fn, options) { doc.set(doc.schema.options.versionKey, state.versionKey); } + if (state.modifiedPaths.length > 0 && doc.$__.activePaths.states.modify == null) { + doc.$__.activePaths.states.modify = {}; + } for (const path of state.modifiedPaths) { doc.$__.activePaths.paths[path] = 'modify'; - if (doc.$__.activePaths.states.modify == null) { - doc.$__.activePaths.states.modify = {}; - } doc.$__.activePaths.states.modify[path] = true; }