[FIX] mail: fix performance of JS models update cycle

- wrap all JS model operations in update cycle
- properly delay execution of compute during `_updateAfter`

This allows to delay the execution of computes as long as possible, which
significantly improves performance, especially during init_messaging with plenty
of chat windows (due to compute of "visual").

PR: #55261
Part of task-2308312

X-original-commit: 79f85523ebef6b7ac4c198eec8b1e55e243f49d1
This commit is contained in:
Sébastien Theys
2020-07-31 14:43:15 +00:00
parent 64f7186843
commit 7a1bfa793a
+128 -99
View File
@@ -119,42 +119,44 @@ class ModelManager {
* @returns {mail.model} newly created record
*/
create(Model, data = {}) {
const record = new Model({ valid: true });
Object.defineProperty(record, 'env', { get: () => Model.env });
record.localId = record._createRecordLocalId(data);
// Contains field values of record.
record.__values = {};
// Contains revNumber of record for checking record update in useStore.
record.__state = 0;
return this._updateCycle(() => {
const record = new Model({ valid: true });
Object.defineProperty(record, 'env', { get: () => Model.env });
record.localId = record._createRecordLocalId(data);
// 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.
const proxifiedRecord = this._makeProxifiedRecord(record);
this._records[record.localId] = proxifiedRecord;
proxifiedRecord.init();
this._makeDefaults(proxifiedRecord);
// Make proxified record, so that access to field redirects
// to field getter.
const proxifiedRecord = this._makeProxifiedRecord(record);
this._records[record.localId] = proxifiedRecord;
proxifiedRecord.init();
this._makeDefaults(proxifiedRecord);
const data2 = Object.assign({}, data);
for (const field of Object.values(Model.fields)) {
if (field.fieldType !== 'relation') {
continue;
const data2 = Object.assign({}, data);
for (const field of Object.values(Model.fields)) {
if (field.fieldType !== 'relation') {
continue;
}
if (!field.autocreate) {
continue;
}
data2[field.fieldName] = [['create']];
}
if (!field.autocreate) {
continue;
for (const field of Object.values(Model.fields)) {
if (field.compute || field.related) {
// new record should always invoke computed fields.
this.registerToComputeField(record, field);
}
}
data2[field.fieldName] = [['create']];
}
for (const field of Object.values(Model.fields)) {
if (field.compute || field.related) {
// new record should always invoke computed fields.
this.registerToComputeField(record, field);
}
}
this.update(proxifiedRecord, data2);
this.update(proxifiedRecord, data2);
return proxifiedRecord;
return proxifiedRecord;
});
}
/**
@@ -165,46 +167,50 @@ class ModelManager {
* @param {mail.model} record
*/
delete(record) {
const Model = record.constructor;
if (!this.get(Model, record)) {
// Record has already been deleted.
// (e.g. unlinking one of its reverse relation was causal)
return;
}
const data = {};
const recordRelations = Object.values(Model.fields)
.filter(field => field.fieldType === 'relation');
for (const relation of recordRelations) {
if (relation.isCausal) {
switch (relation.relationType) {
case 'one2one':
case 'many2one':
if (record[relation.fieldName]) {
record[relation.fieldName].delete();
}
break;
case 'one2many':
case 'many2many':
for (const relatedRecord of record[relation.fieldName]) {
relatedRecord.delete();
}
break;
}
this._updateCycle(() => {
const Model = record.constructor;
if (!this.get(Model, record)) {
// Record has already been deleted.
// (e.g. unlinking one of its reverse relation was causal)
return;
}
data[relation.fieldName] = [['unlink-all']];
}
record.update(data);
delete this._records[record.localId];
delete this.env.store.state[record.localId];
const data = {};
const recordRelations = Object.values(Model.fields)
.filter(field => field.fieldType === 'relation');
for (const relation of recordRelations) {
if (relation.isCausal) {
switch (relation.relationType) {
case 'one2one':
case 'many2one':
if (record[relation.fieldName]) {
record[relation.fieldName].delete();
}
break;
case 'one2many':
case 'many2many':
for (const relatedRecord of record[relation.fieldName]) {
relatedRecord.delete();
}
break;
}
}
data[relation.fieldName] = [['unlink-all']];
}
record.update(data);
delete this._records[record.localId];
delete this.env.store.state[record.localId];
});
}
/**
* Delete all records.
*/
deleteAll() {
for (const record of Object.values(this._records)) {
record.delete();
}
this._updateCycle(() => {
for (const record of Object.values(this._records)) {
record.delete();
}
});
}
/**
@@ -257,13 +263,15 @@ class ModelManager {
* @returns {mail.model} created or updated record.
*/
insert(Model, data) {
let record = Model.find(Model._findFunctionFromData(data));
if (!record) {
record = Model.create(data);
} else {
record.update(data);
}
return record;
return this._updateCycle(() => {
let record = Model.find(Model._findFunctionFromData(data));
if (!record) {
record = Model.create(data);
} else {
record.update(data);
}
return record;
});
}
/**
@@ -276,36 +284,9 @@ class ModelManager {
* @param {Object} data
*/
update(record, data) {
if (!this._isInUpdateCycle) {
this._isInUpdateCycle = true;
this._updateCycle(() => {
this._updateDirect(record, data);
while (
this._toComputeFields.size > 0 ||
this._toUpdateAfters.length > 0
) {
if (this._toComputeFields.size > 0) {
this._updateComputes();
} else {
this._isHandlingToUpdateAfters = true;
// process one update after
const [recordToUpdate, previous] = this._toUpdateAfters.pop();
const RecordToUpdateModel = recordToUpdate.constructor;
if (this.get(RecordToUpdateModel, recordToUpdate)) {
recordToUpdate._updateAfter(previous);
}
this._isHandlingToUpdateAfters = false;
}
}
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) {
this._updateComputes();
}
}
});
}
/**
@@ -917,6 +898,54 @@ class ModelManager {
}
}
/**
* Executes the provided function as part of a single update cycle. This
* allows the execution of computed fields to happen only once, at the end
* of the last pending update cycle.
* It makes sense to call this function when the provided function is
* expected to create/update/delete records, which in turn would lead to
* potentially triggering computes.
*
* @private
* @param {function} func synchronous function expected to trigger computes
* @returns {any} the result of the provided function
*/
_updateCycle(func) {
let res;
if (!this._isInUpdateCycle) {
this._isInUpdateCycle = true;
res = func();
this._updateComputes();
while (this._toUpdateAfters.length > 0) {
this._isHandlingToUpdateAfters = true;
// process one update after
const [recordToUpdate, previous] = this._toUpdateAfters.pop();
const RecordToUpdateModel = recordToUpdate.constructor;
if (this.get(RecordToUpdateModel, recordToUpdate)) {
recordToUpdate._updateAfter(previous);
}
this._isHandlingToUpdateAfters = false;
}
this._isInUpdateCycle = false;
// trigger at most one useStore call per update cycle
this.env.store.state.messagingRevNumber++;
} else {
const wasHandlingToUpdateAfters = this._isHandlingToUpdateAfters;
this._isHandlingToUpdateAfters = false;
res = func();
if (wasHandlingToUpdateAfters) {
// Special case for computes triggered during an _updateAfter:
// execute them at the end of the current cycle, instead of at
// the end of the last pending cycle. This is because
// _updateAfter is expected to work with the "final" state just
// like business code, not with a temporary non-computed state.
this._updateComputes();
this._isHandlingToUpdateAfters = true;
}
}
return res;
}
/**
* Process a direct update on given record with provided data.
*