Possible memory leak on AutoFill and DateTime

Possible memory leak on AutoFill and DateTime

hserveirahserveira Posts: 16Questions: 4Answers: 0

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

  • allanallan Posts: 65,740Questions: 1Answers: 10,934 Site admin

    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 master branches which is for the upcoming DataTables 3 release.

    Impressive code the LLM has created there to patch in a fix :)

    Allan

Sign In or Register to comment.