[FIX] web: remove memory leak in editable list used as one2many

The confirmUpdate method in list_editable_renderer would destroy all
rows' widgets and recreate them *except* the currently modified one
(this one gets updated).

The problem was that the widgets of the current row were recreated
anyway. It created a memory leak.

This memory leak isn't such a big deal, as anything is garbage
collected as soon as the view is left anyway (so it's a small leak
during the lifetime of the x2many list)

The fix consists in keeping the reference of the widgets on the
currently modified row, and when all rows' widgets are recreated, we
delete a replace by our reference for the current row.

closes odoo/odoo#68386

Signed-off-by: Aaron Bohy (aab) <aab@odoo.com>
This commit is contained in:
Simon Genin (ges)
2021-03-26 13:27:48 +00:00
parent a52d32822d
commit 3d0b77a711
2 changed files with 58 additions and 0 deletions
@@ -230,6 +230,11 @@ ListRenderer.include({
}
});
// Keep ref of the current row's widgets
const currentRowFieldWidgets = self.allFieldWidgets[id];
// Remove it from the list just for a clean start for generating the new content (avoid later memory leak)
delete self.allFieldWidgets[id];
// re-render whole body (outside the dom)
self.defs = [];
var $newBody = self._renderBody();
@@ -237,6 +242,12 @@ ListRenderer.include({
delete self.defs;
return Promise.all(defs).then(function () {
// All the field widgets have now be recreated, but we don't want the current row's ones.
// We destroy it and get it back from our reference.
// This manipulation avoids a memory leak, where field widgets in the current row wouldn't be destroyed and were kept in memory.
self._destroyFieldWidgets(id);
self.allFieldWidgets[id] = currentRowFieldWidgets;
// update registered modifiers to edit 'mode' because the call to
// _renderBody set baseModeByRecord as 'readonly'
_.each(self.columns, function (node) {
@@ -9868,8 +9868,55 @@ QUnit.module('fields', {}, function () {
assert.hasClass(form.$('.o_data_row:nth(1) .o_field_badge'), 'bg-warning-light');
form.destroy();
});
QUnit.test('Editable list renderer confirmUpdate method does not create a memory leak by no deleted currently modified row widgets but recreating them anyway.', async function (assert) {
assert.expect(5);
let count = 0;
const MyField = AbstractField.extend({
init() {
this._super(...arguments);
count++;
},
destroy() {
this._super(...arguments);
count--;
}
});
fieldRegistry.add('myfield', MyField);
this.data.partner.records[0].p = [1, 2];
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: `
<form>
<field name="p">
<tree editable="bottom">
<field name="int_field"/>
<field name="foo" widget="myfield"/>
</tree>
</field>
</form>`,
res_id: 1,
});
assert.containsN(form, '.o_data_row', 2);
assert.strictEqual(count, 2);
await testUtils.dom.click(form.$('.o_data_row .o_data_cell:first'));
assert.strictEqual(count, 2);
await testUtils.fields.editInput(form.$('.o_selected_row .o_field_integer'), '44');
assert.strictEqual(count, 2);
form.destroy();
delete fieldRegistry.map.my_field;
assert.strictEqual(count, 0);
});
});