From f56841381c14a2bf50f1321462074750a3a45a7e Mon Sep 17 00:00:00 2001 From: Samuel Degueldre <sad@odoo.com> Date: Tue, 19 Sep 2023 09:57:56 +0200 Subject: [PATCH] [FIX] web: fix performance issues with large x2m fields Previously, when a view contained an x2m field that had a few thousand records, the view would take ages to load. This was caused by a few things in the basic model where we would linearly scan lists of records in a tight loop, and doing so for every single other record, causing quadratic increase runtime with regards to the number of records. For example, creating a new batch picking in stock with 4000 existing pickings would take up to ~20 seconds to load the view. This commit creates a few objects to use as lookup tables in such places, which speeds up the loading of the view to less than a second. It also makes the context of the DataPoint into a property that's initialized lazily on first read, as creating a context and the corresponding evalContext can be expensive, which becomes a problem when creating thousands of DataPoints whose context will never be read. opw-3465073 closes odoo/odoo#135860 Signed-off-by: Aaron Bohy (aab) <aab@odoo.com> --- .../src/legacy/js/views/basic/basic_model.js | 25 ++++++++++++------- .../src/views/basic_relational_model.js | 23 ++++++++++++++++- 2 files changed, 38 insertions(+), 10 deletions(-) diff --git a/addons/web/static/src/legacy/js/views/basic/basic_model.js b/addons/web/static/src/legacy/js/views/basic/basic_model.js index bc1c6e7d04c8..0dac799b6d60 100644 --- a/addons/web/static/src/legacy/js/views/basic/basic_model.js +++ b/addons/web/static/src/legacy/js/views/basic/basic_model.js @@ -3425,10 +3425,12 @@ var BasicModel = AbstractModel.extend({ // least one id was updated var didChange = false; var changes, command, relRecord; + const addedById = Object.fromEntries(relRecordAdded.map(r => [r.res_id, r])); + const updatedById = Object.fromEntries(relRecordUpdated.map(r => [r.res_id, r])); for (var i = 0; i < list.res_ids.length; i++) { if (_.contains(keptIds, list.res_ids[i])) { // this is an id that already existed - relRecord = _.findWhere(relRecordUpdated, {res_id: list.res_ids[i]}); + relRecord = updatedById[list.res_ids[i]]; changes = relRecord ? this._generateChanges(relRecord, options) : {}; if (!_.isEmpty(changes)) { command = x2ManyCommands.update(relRecord.res_id, changes); @@ -3439,7 +3441,7 @@ var BasicModel = AbstractModel.extend({ commands[fieldName].push(command); } else if (_.contains(addedIds, list.res_ids[i])) { // this is a new id (maybe existing in DB, but new in JS) - relRecord = _.findWhere(relRecordAdded, {res_id: list.res_ids[i]}); + relRecord = addedById[list.res_ids[i]]; if (!relRecord) { commands[fieldName].push(x2ManyCommands.link_to(list.res_ids[i])); continue; @@ -4797,9 +4799,16 @@ var BasicModel = AbstractModel.extend({ })); } return def.then(function (records) { + const context = list.getContext(); + const changeMap = Object.fromEntries( + (list._changes || []) + .filter((c) => c.operation === "ADD") + .map((c) => [c.resID, c]) + ); + const recordsById = Object.fromEntries(records.map(r => [r.id, r])); _.each(resIDs, function (id) { var dataPoint; - var data = _.findWhere(records, {id: id}); + var data = recordsById[id]; if (id in list._cache) { dataPoint = self.localData[list._cache[id]]; if (data) { @@ -4808,7 +4817,7 @@ var BasicModel = AbstractModel.extend({ } } else { dataPoint = self._makeDataPoint({ - context: list.getContext(), + context, data: data, fieldsInfo: list.fieldsInfo, fields: list.fields, @@ -4822,11 +4831,9 @@ var BasicModel = AbstractModel.extend({ list._cache[id] = dataPoint.id; } // set the dataPoint id in potential 'ADD' operation adding the current record - _.each(list._changes, function (change) { - if (change.operation === 'ADD' && !change.id && change.resID === id) { - change.id = dataPoint.id; - } - }); + if (changeMap[id] && !changeMap[id].id) { + changeMap[id].id = dataPoint.id; + } }); return list; }); diff --git a/addons/web/static/src/views/basic_relational_model.js b/addons/web/static/src/views/basic_relational_model.js index ae373bcc38da..3fc3a0ff516b 100644 --- a/addons/web/static/src/views/basic_relational_model.js +++ b/addons/web/static/src/views/basic_relational_model.js @@ -41,7 +41,28 @@ class DataPoint { } else if (params.handle) { this.__bm_handle__ = params.handle; info = this.model.__bm__.get(this.__bm_handle__); - this.context = this.model.__bm__.localData[this.__bm_handle__].getContext(); + // Create a lazy property that sets itself on first read, because creating the context + // is very expensive, and in some cases we are creating a lot of DataPoints whose + // context will never be read. + Object.defineProperty(this, "context", { + get() { + const context = this.model.__bm__.localData[this.__bm_handle__].getContext(); + Object.defineProperty(this, "context", { + value: context, + configurable: true, + writable: true, + }); + return context; + }, + set(value) { + Object.defineProperty(this, "context", { + value, + configurable: true, + writable: true, + }); + }, + configurable: true, + }); } else { throw new Error("Datapoint needs load params or handle"); } -- GitLab