Possible memory leak on AutoFill and DateTime
Possible memory leak on AutoFill and DateTime
I have a SPA application that relies heavily on creating and destroying DataTables and Editors. It's been a while since I noticed that, over time, memory footprint increases, along with detached nodes. After trying to fix it several times I finally gave up, until today I gave Copilot access to my page and it found the following:
_Opening 'View Items' creates DateTime and AutoFill plugin listeners that are not fully unbound when the DataTable is destroyed. The leaked handler sources identify the plugins:
DataTables DateTime handlers:
function(t,e){e.oLanguage.datetime&&(g.extend(!0,o.c.i18n,e.oLanguage.datetime),o._optionsTitle())}
DataTables AutoFill handler:
function(){0<f("div.dt-autofill-handle").length&&void 0!==e.dom.attachedTo&&e._attach(e.dom.attachedTo)}
_
After this analysis, it came up with the following code, which to my surprise solved the increasing memory footprint:
const $ = window.jQuery;
const DataTable = $?.fn?.dataTable;
function normalizeNamespace(namespace) {
if (!namespace) {
return '';
}
return namespace.startsWith('.') ? namespace : `.${namespace}`;
}
function getEventEntries(target, eventName) {
const events = $._data?.(target, 'events') || {};
return Array.isArray(events[eventName]) ? events[eventName].slice() : [];
}
function getAddedEntries(beforeEntries, afterEntries) {
const beforeSet = new Set(beforeEntries);
return afterEntries.filter((entry) => !beforeSet.has(entry));
}
function appendNamespace(entry, namespace) {
if (!entry || !namespace) {
return;
}
const namespaceToken = normalizeNamespace(namespace).slice(1);
if (!namespaceToken) {
return;
}
const namespaceParts = (entry.namespace || '').split('.').filter(Boolean);
if (!namespaceParts.includes(namespaceToken)) {
namespaceParts.push(namespaceToken);
namespaceParts.sort();
entry.namespace = namespaceParts.join('.');
}
}
function patchDateTime() {
const DateTime = DataTable?.DateTime || window.DateTime;
if (!DateTime?.prototype?._constructor || !DateTime?.prototype?.destroy) {
return;
}
const originalConstructor = DateTime.prototype._constructor;
const originalDestroy = DateTime.prototype.destroy;
DateTime.prototype._constructor = function (...args) {
const beforeEntries = getEventEntries(document, 'i18n');
const result = originalConstructor.apply(this, args);
const namespace = normalizeNamespace(this?.s?.namespace);
if (!namespace) {
return result;
}
const addedEntries = getAddedEntries(beforeEntries, getEventEntries(document, 'i18n'));
addedEntries.forEach((entry) => appendNamespace(entry, namespace));
return result;
};
DateTime.prototype.destroy = function (...args) {
const namespace = normalizeNamespace(this?.s?.namespace);
if (namespace) {
$(document).off(namespace);
}
return originalDestroy.apply(this, args);
};
}
function patchAutoFill() {
const AutoFill = DataTable?.AutoFill;
if (!AutoFill?.prototype?._constructor || !AutoFill?.prototype?.enable || !AutoFill?.prototype?.disable) {
return;
}
const originalConstructor = AutoFill.prototype._constructor;
const originalEnable = AutoFill.prototype.enable;
const originalDisable = AutoFill.prototype.disable;
AutoFill.prototype._constructor = function (...args) {
const result = originalConstructor.apply(this, args);
const namespace = normalizeNamespace(this?.s?.namespace);
if (namespace && this?.s?.dt) {
this.s.dt.on(`destroy${namespace}.cleanup`, () => {
$(window).off(namespace);
});
}
return result;
};
AutoFill.prototype.enable = function (...args) {
const beforeResizeEntries = getEventEntries(window, 'resize');
const beforeOrientationEntries = getEventEntries(window, 'orientationchange');
const result = originalEnable.apply(this, args);
const namespace = normalizeNamespace(this?.s?.namespace);
if (!namespace) {
return result;
}
const addedResizeEntries = getAddedEntries(beforeResizeEntries, getEventEntries(window, 'resize'));
const addedOrientationEntries = getAddedEntries(beforeOrientationEntries, getEventEntries(window, 'orientationchange'));
addedResizeEntries.forEach((entry) => appendNamespace(entry, namespace));
addedOrientationEntries.forEach((entry) => appendNamespace(entry, namespace));
return result;
};
AutoFill.prototype.disable = function (...args) {
const result = originalDisable.apply(this, args);
const namespace = normalizeNamespace(this?.s?.namespace);
if (namespace) {
$(window).off(namespace);
}
return result;
};
}
if ($ && DataTable && !window.__dataTablesCleanupPatchInstalled) {
window.__dataTablesCleanupPatchInstalled = true;
patchDateTime();
patchAutoFill();
}
Decided to leave this here in case you want to investigate or anyone else faces the same issue.
Regards,
Replies
Thank you for posting this. You are quite right, there are a couple of memory leaks there! Fixes committed here:
The fixes are on the
masterbranches which is for the upcoming DataTables 3 release.Impressive code the LLM has created there to patch in a fix
Allan