diff options
| author | Sébastien Theys <seb@odoo.com> | 2020-07-30 16:14:32 +0000 |
|---|---|---|
| committer | Sébastien Theys <seb@odoo.com> | 2020-07-31 09:43:01 +0000 |
| commit | 78bef3e08c04a45bdf9122e595160e42bf7af8e1 (patch) | |
| tree | e606c67646a48907979f9927438bc9691b2e6cd3 | |
| parent | 89bf21ba797d72ff624fbf88e97a40628f72e0d8 (diff) | |
[FIX] mail: improve performance of JS write, especially for x2m
- store `x2many` values as `Set`. This is conceptually more correct because
duplicates were already not allowed, and using a `Set` allows for much faster
operations such as checking the presence of a record in the relation, which is
done at every write, for every linked record.
- `stringify` in `write` was slow and unnecessary because we can always assume
that new non-primitive should be provided to `write`
- remove field values from store, in favor of just storing a counter, which is
enough to trigger renders. This allows to get rid of one Proxy level around
data, which was slower, but which also prevented from using `Set`.
- write in store exactly once per update cycle, to trigger one `useStore` call
on each component. Records keep `__state` but as a local counter, to let
`useStore` know which records have been updated (easier to compare a counter
than comparing each and every field value).
Results
=======
The worse case test (using a lot of x2m) that was used to benchmark this PR
`form_renderer_tests.js` of `mail_enterprise` went from taking around 4s before
this commit, to around 2.5s after this commit.
And the worse `_setRelationLinkMany2Many` previously took around 250ms, and it
now takes around 50ms.
The whole `mail` QUnit suite took around 55s before, now around 42s.
Part of task-2308312
closes odoo/odoo#55248
X-original-commit: 4db0a10d85534ef896f6bcbf13bca4983a6e2003
Signed-off-by: Alexandre Kühn (aku) <aku@odoo.com>
| -rw-r--r-- | addons/mail/static/src/env/test_env.js | 4 | ||||
| -rw-r--r-- | addons/mail/static/src/js/main.js | 4 | ||||
| -rw-r--r-- | addons/mail/static/src/model/model_field.js | 47 | ||||
| -rw-r--r-- | addons/mail/static/src/model/model_manager.js | 15 | ||||
| -rw-r--r-- | addons/mail/static/tests/systray/systray_activity_menu_tests.js | 3 |
5 files changed, 36 insertions, 37 deletions
diff --git a/addons/mail/static/src/env/test_env.js b/addons/mail/static/src/env/test_env.js index 75e921c27c7..b7bf22010cb 100644 --- a/addons/mail/static/src/env/test_env.js +++ b/addons/mail/static/src/env/test_env.js @@ -34,7 +34,9 @@ function addMessagingToEnv(providedEnv = {}) { */ const store = new Store({ env, - state: {}, + state: { + messagingRevNumber: 0, + }, }); /** diff --git a/addons/mail/static/src/js/main.js b/addons/mail/static/src/js/main.js index 95c8c4153b1..6b275ab9851 100644 --- a/addons/mail/static/src/js/main.js +++ b/addons/mail/static/src/js/main.js @@ -13,7 +13,9 @@ const { EventBus } = owl.core; */ const store = new Store({ env, - state: {}, + state: { + messagingRevNumber: 0, + }, }); /** diff --git a/addons/mail/static/src/model/model_field.js b/addons/mail/static/src/model/model_field.js index 589eee5c782..1d31d42caf3 100644 --- a/addons/mail/static/src/model/model_field.js +++ b/addons/mail/static/src/model/model_field.js @@ -276,13 +276,11 @@ class ModelField { if (['one2one', 'many2one'].includes(this.relationType)) { return OtherModel.get(this.read(record)); } - return this.read(record) - .map(localId => OtherModel.get(localId)) - /** - * FIXME: Stored relation may still contain - * outdated records. - */ - .filter(record => !!record); + const res = []; + for (const localId of this.read(record)) { + res.push(OtherModel.get(localId)); + } + return res; } throw new Error(`cannot get field with unsupported type ${this.fieldType}.`); } @@ -295,7 +293,7 @@ class ModelField { * @returns {any} */ read(record) { - return record.__state[this.fieldName]; + return record.__values[this.fieldName]; } /** @@ -357,18 +355,15 @@ class ModelField { */ write(record, newVal, { registerDependents = true } = {}) { if (this.read(record) === newVal) { + // value unchanged, don't need to compute dependent fields return; } - const prevStringified = JSON.stringify(this.read(record)); - record.__state[this.fieldName] = newVal; - const newStringified = JSON.stringify(this.read(record)); if (this._containsRecords(newVal)) { - throw new Error("Forbidden write operation with records in the __state!!"); - } - if (newStringified === prevStringified) { - // value unchanged, don't need to compute dependent fields - return; + throw new Error("Forbidden write operation with records!!"); } + record.__values[this.fieldName] = newVal; + record.__state++; + if (!registerDependents) { return; } @@ -656,7 +651,7 @@ class ModelField { const prevLocalIds = this.read(record); // other record already linked, avoid linking twice - if (prevLocalIds.includes(newLocalId)) { + if (prevLocalIds.has(newLocalId)) { continue; } @@ -668,7 +663,7 @@ class ModelField { } // link other record to current record - this.write(record, prevLocalIds.concat([newLocalId])); + this.write(record, new Set([...prevLocalIds, newLocalId])); // link current record to other record newOtherRecord.update({ @@ -741,7 +736,7 @@ class ModelField { const prevLocalIds = this.read(record); // other record already linked, avoid linking twice - if (prevLocalIds.includes(newLocalId)) { + if (prevLocalIds.has(newLocalId)) { continue; } @@ -753,7 +748,7 @@ class ModelField { } // link other record to current record - this.write(record, prevLocalIds.concat([newLocalId])); + this.write(record, new Set([...prevLocalIds, newLocalId])); // link current record to other record newOtherRecord.update({ @@ -861,14 +856,12 @@ class ModelField { const prevLocalIds = this.read(record); // other record already unlinked, avoid useless processing - if (!prevLocalIds.includes(otherLocalId)) { + if (!prevLocalIds.delete(otherLocalId)) { continue; } // unlink other record from current record - this.write(record, prevLocalIds.filter( - localId => localId !== otherLocalId - )); + this.write(record, new Set([...prevLocalIds])); const otherRecord = OtherModel.get(otherLocalId); // other record may be deleted due to causality, avoid useless @@ -931,14 +924,12 @@ class ModelField { const prevLocalIds = this.read(record); // other record already unlinked, avoid useless processing - if (!prevLocalIds.includes(otherLocalId)) { + if (!prevLocalIds.delete(otherLocalId)) { continue; } // unlink other record from current record - this.write(record, prevLocalIds.filter( - localId => localId !== otherLocalId - )); + this.write(record, new Set([...prevLocalIds])); const otherRecord = OtherModel.get(otherLocalId); // other record may be deleted due to causality, avoid useless diff --git a/addons/mail/static/src/model/model_manager.js b/addons/mail/static/src/model/model_manager.js index 37ce2d2808c..5ef0d5c19ae 100644 --- a/addons/mail/static/src/model/model_manager.js +++ b/addons/mail/static/src/model/model_manager.js @@ -122,11 +122,10 @@ class ModelManager { const record = new Model({ valid: true }); Object.defineProperty(record, 'env', { get: () => Model.env }); record.localId = record._createRecordLocalId(data); - - // Make state, which contain field values of record that have to - // be observed in store. - this.env.store.state[record.localId] = {}; - record.__state = this.env.store.state[record.localId]; + // Contains field values of record. + record.__values = {}; + // Contains revNumber of record for checking record update in useStore. + record.__state = 0; // Make proxified record, so that access to field redirects // to field getter. @@ -299,6 +298,8 @@ class ModelManager { } this._toComputeFields.clear(); this._isInUpdateCycle = false; + // trigger at most one useStore call per update cycle + this.env.store.state.messagingRevNumber++; } else { this._updateDirect(record, data); if (this._isHandlingToUpdateAfters) { @@ -689,8 +690,8 @@ class ModelManager { } if (field.fieldType === 'relation') { if (['one2many', 'many2many'].includes(field.relationType)) { - // Ensure X2many relations are arrays by defaults. - field.write(record, [], { registerDependents: false }); + // Ensure X2many relations are Set by defaults. + field.write(record, new Set(), { registerDependents: false }); } else { field.write(record, undefined, { registerDependents: false }); } diff --git a/addons/mail/static/tests/systray/systray_activity_menu_tests.js b/addons/mail/static/tests/systray/systray_activity_menu_tests.js index 0258b35652f..651ea8eddfc 100644 --- a/addons/mail/static/tests/systray/systray_activity_menu_tests.js +++ b/addons/mail/static/tests/systray/systray_activity_menu_tests.js @@ -228,6 +228,9 @@ QUnit.test('activity menu widget: close on messaging menu click', async function const { widget } = await start({ hasMessagingMenu: true, async mockRPC(route, args) { + if (args.method === 'message_fetch') { + return []; + } if (args.method === 'systray_get_activities') { return []; } |
