Нахождение утечек памяти в ExtJS

Утечки памяти — они же memory leaks, приводят к замедлению работы приложения через определенный промежуток времени. Зачастую нахождение таких ошибок является трудоемкой задачей, в данной статье мы покажем вам некоторые типовые ошибки которые приводят к утечкам памяти, а так же затронем моменты профилирования и нахождения memory leaks.

Типовые ошибки встречающиеся в ExtJS

Пример 1 :

Ext.define('Foo.bar.CustomButton', {
    extend: 'Ext.button.Button',
    onDestroy: function () {
        // процедура разрушения объекта
    }
});

Не вызывается callParent() для вызова процедуры очистки базового класса в методе onDestroy(). Не забывайте вызывать родителя.

Пример 2 :

Ext.fly(someElement).on('click', doSomething);
someElement.parentNode.innerHTML = '';

Мы подписываемся на событие click элемента, но элементы перезаписываются путем изменения innerHTML, и этот обработчик останется в памяти навсегда. Необходимо сохранять ссылки на такие объекты и отписываться от события если оно не нужно.

Пример 3 :

Ext.define('MyClass', {
    constructor: function() {
        this.foo = new SomeLargeObject();
    },
    destroy: function() {
        this.foo.destroy();
    }
});

this.o = new MyClass();
o.destroy();

Создается экземпляр класса, который использует много памяти. Класс уничтожается, но остается ссылка на существующий объект. Необходимо в методе destroy() обнулять ссылку с помощью this.foo = null; , а так же не забыть про this.o = null; после вызова destroy()

Пример 4 :

function runAsync(val) {
    var o = new SomeLargeObject();
    var x = 42;

    // ...

    return function() {
        return x;  // переменная o находится в замыкании, но не нужна 
    }
}

var f = runAsync(1);

Эта ситуация более тонкая, но очень похожая на приведенную в «Пример 3«. Функция содержит замыкание на большой объект, который не может быть собран сборщиком мусора, пока замыкание все ещё ссылается на него. Такое часто возникает из-за того, что большой объект присутствует во внешнем пространстве и не нужен внутренней функции. Такие вещи легко пропустить, но они могут негативно влиять на использование памяти. В качестве решения, ExtJS предлагает использование Ext.Function.bind() или стандартную функцию bind, для создания безопасных замыкания для функции. Пример использования

function fn (x) {
    return x;
}

function runAsync(val) {
    var o = new SomeLargeObject();
    var x = 42;

    // ...

    return Ext.Function.bind(fn, null, [x]); // в замыкании нет переменной o
}

var f = runAsync(1);

Пример 5 :

{
    xtype: 'treepanel',
    listeners: {
        itemclick: function(view, record, item, index, e) {
            new Ext.menu.Menu({
                items: [record.get('name')]
            }).showAt(e.getXY());
        }
    }
}

Создание некоторых объектов может иметь побочные (side) эффекты (например, создание DOM элементов). Если они создаются без разрушения, они останутся в памяти навсегда. Необходимо запоминать такие объекты и вызывать destroy() если в них более нет надобности.

Внутренние коллекции ExtJS (Internal Caches)

ExtJS содержит в себе внутренние коллекции (кеши) в которых часто можно найти причину утечек памяти.

Ext.ComponentManager.all   // Ext.Component cache as map
Ext.cache                  // Ext.Element cache as map
Ext.StoreManager.items     // Ext.data.Store cache as array
Ext.fx.Manager.targets.map // Ext.fx.target.Element cache as map
Ext.dd.DragDropManager.ids // DD Object cache as map - also "leaks" by design;
                           // it retains empty objects mapped out to IDs that
                           // used to be legit components, so minimal memory
                           // impact, but those IDs are never wiped

Можно написать процедуру которая будет проходиться по указанным коллекциям и сравнивать их snapshots.

// Например
// * var s1 = getCacheSnapshot()
// * ... Делаем что то в приложении
// * var s2 = getCacheSnapshot()
// * compareCacheSnapshots(s1, s2);
function getCacheSnapshot() {
    return {
        cmp: Ext.Object.getKeys(Ext.ComponentManager.all),
        el: Ext.Object.getKeys(Ext.cache),
        st: Ext.Array.pluck(Ext.StoreManager.items, 'storeId'),
        fx: Ext.Object.getKeys(Ext.fx.Manager ? Ext.fx.Manager.targets.map : []),
        dd: Ext.Object.getKeys(Ext.dd ? Ext.dd.DragDropManager.ids : [])
    };
}
 
// Логирование и разница между snapshots
function compareCacheSnapshots(s1, s2) {
    function diffArrays(name, a1, a2) {
        var added = Ext.Array.difference(a2, a1);
        var removed = Ext.Array.difference(a1, a2);
        if (added.length || removed.length) {
            console.log(' ---- ', name, 'Added:', added, 'Removed:', removed);
        }
    }
    for (var key in s1) {
        var a1 = s1[key];
        var a2 = s2[key];
        console.log(key, a1.length === a2.length ? a1.length : a1.length + '->' + a2.length);
        diffArrays(key, a1, a2);
    }
}

Так же существует флаг Ext.enableGarbageCollector который отвечает за периодическую очистку коллекции Ext.Elements, если его установить в false то приложение будет не будет удалять непривязанные элементы из коллекции (это исключит утечки памяти в обработчиках событий их DOM элементов)

Нахождение утечек с помощью профилирования

В поисках утечек памяти я использовал стандартные инструменты Chrome Dev Tools. Это полезный инструмент, который дает вам много информации, но иногда бывает трудно понять, что на самом деле означает эта информация. Так как много информации связанно не с утечкой памяти, а с работой приложения, обнаружение утечки может быть очень сложной задачей.

После запуска приложения и получения snapshot’a вы можете посмотреть собранные данные, которые, скорее всего, будут не очень полезны с первого взгляда. Например, в приложении ExtJS существует множество классов, которые загружаются, элементы кэшируются и т.д., и не являются утечкой памяти. Чтобы сделать потенциальную утечку памяти более заметной в этом «фоновом шуме», важно, чтобы вы запускали свой тест кейс много раз. Если вы считаете, что в определенной части вашего приложения может находиться утечка памяти, тогда выполнение простого цикла должно дать вам хорошую информацию для обработки.

// Получить snapshot до
 
for (var i = 0; i< 100; i++){
   openSomeDialog();
   closeSomeDialog();
} 
 
// Получить snapshot после и рассмотреть его

Если потребление памяти значительно увеличивается — то утечка определенно есть.

Тестовый пример для нахождения утечки памяти

Рассмотрим тестовый пример который содержит несколько утечек памяти — каких догадайтесь сами 🙂 . Если воспроизводите кейс на живом приложении — настоятельно рекомендую остановить приложение.

Ext.define('UserList', {
    extend : 'Ext.view.View',
    config : {
        itemTpl    : '{name}',
        fullscreen : true
    },
    constructor : function (config) {
        var myStore = new Ext.data.Store({
           fields : ['id', 'name'],
           data   : [
               { name : 'User 1' },
               { name : 'User 2' },
               { name : 'User 3' }
           ]
        });
        this.callParent(arguments);
        this.setStore(myStore);
        this.mystore = myStore;
   }
});

Тест кейс для исследования этого класса будет выглядеть следующим образом

for (var i = 0; i< 100; i++){
   new UserList().destroy();
}

Снимаем snapshot до запуска тест-кейса , и после него. Объект создан и уничтожен 100 раз, тесты показывают что после этого память увеличилась на 3 МБ, что говорит нам о том что утечка есть.

В данном случае мы забыли уничтожить ссылку mystore , уничтожим её

Ext.define('UserList', {
    extend : 'Ext.view.View',
    config : {
        itemTpl    : '{name}',
        fullscreen : true
    },
    constructor : function (config) {
         var myStore = new Ext.data.Store({
            fields : ['id', 'name'],
            data   : [
                { name : 'User 1' },
                { name : 'User 2' },
                { name : 'User 3' }
            ]
        });
        this.callParent(arguments);
        this.setStore(myStore);
        this.mystore = myStore;
    },
 
    destroy : function() {
        this.getStore().destroy();
        this.callParent(arguments);
        this.mystore = null;
    }
});

Дополнительные материалы


Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *