Пошук витоків пам'яті JavaScript за допомогою Chrome


163

Я створив дуже простий тестовий випадок, який створює перегляд Backbone, приєднує обробник події та інстанціює визначений користувачем клас. Я вважаю, що натиснувши кнопку "Видалити" в цьому зразку, все буде очищено і не повинно бути витоків пам'яті.

Jsfiddle для коду тут: http://jsfiddle.net/4QhR2/

// scope everything to a function
function main() {

    function MyWrapper() {
        this.element = null;
    }
    MyWrapper.prototype.set = function(elem) {
        this.element = elem;
    }
    MyWrapper.prototype.get = function() {
        return this.element;
    }

    var MyView = Backbone.View.extend({
        tagName : "div",
        id : "view",
        events : {
            "click #button" : "onButton",
        },    
        initialize : function(options) {        
            // done for demo purposes only, should be using templates
            this.html_text = "<input type='text' id='textbox' /><button id='button'>Remove</button>";        
            this.listenTo(this,"all",function(){console.log("Event: "+arguments[0]);});
        },
        render : function() {        
            this.$el.html(this.html_text);

            this.wrapper = new MyWrapper();
            this.wrapper.set(this.$("#textbox"));
            this.wrapper.get().val("placeholder");

            return this;
        },
        onButton : function() {
            // assume this gets .remove() called on subviews (if they existed)
            this.trigger("cleanup");
            this.remove();
        }
    });

    var view = new MyView();
    $("#content").append(view.render().el);
}

main();

Однак мені незрозуміло, як за допомогою профіля Google Chrome переконатися, що це насправді так. Є кілька мільйонів речей, які відображаються на знімку профілів купи, і я не маю уявлення, як розшифрувати, що добре / що погано. Підручники, які я бачив на ньому до цього часу, або просто кажуть мені "використовувати профайл знімка", або дають мені дуже детальний маніфест про те, як працює весь профілер. Чи можна просто використовувати профайлер як інструмент, чи мені справді потрібно зрозуміти, як була спроектована вся справа?

EDIT: Підручники на кшталт цих:

Виправлення витоку пам’яті Gmail

Використання DevTools

Представляють деякі сильніші матеріали з того, що я бачив. Однак, крім введення концепції 3-х знімкової техніки , я вважаю, що вони пропонують дуже мало з точки зору практичних знань (для початківця, як я). Підручник "Використання DevTools" не працює через реальний приклад, тому його розпливчастий та загальний концептуальний опис речей не надто корисний. Що стосується прикладу "Gmail":

Так ви знайшли витік. А тепер що?

  • Вивчіть утримуючий шлях просочених предметів у нижній половині панелі "Профілі"

  • Якщо сайт розподілу неможливо легко зробити (тобто слухачі подій):

  • Інструментуйте конструктор утримуючого об'єкта через консоль JS, щоб зберегти слід стека для виділень

  • Використовуєте закриття? Увімкніть відповідний існуючий прапор (тобто goog.events.Listener.ENABLE_MONITORING), щоб встановити властивість createStack під час будівництва

Я вважаю себе більш розгубленим, прочитавши це, не менше. І, знову ж таки, це просто підказує мені робити речі, а не як їх робити. З моєї точки зору, вся інформація там або занадто розпливчаста, або мала б сенс лише той, хто вже зрозумів процес.

Деякі з цих більш конкретних питань були порушені у відповіді @Jonathan Naguin нижче.


2
Я нічого не знаю про тестування використання пам’яті в браузерах, але якщо ви цього не бачили, стаття Адді Османі про веб-інспектора Chrome може бути корисною.
Пол Д. Уейт

1
Дякую за пропозицію, Пол. Однак, коли я роблю один знімок перед натисканням клавіші видалити, а потім інший після його натискання, а потім вибираю "об'єкти, виділені між знімками 1 і 2" (як це запропоновано в його статті), є ще понад 2000 об'єктів. Наприклад, є 4 "HTMLButtonElement" записи, що для мене немає сенсу. Справді, я поняття не маю, що відбувається.
EleventyOne

3
До, це не здається особливо корисним. Можливо, з мовою, зібраною зі сміттям, як JavaScript, ви насправді не маєте права перевіряти, що ви робите із пам’яттю на такому рівні, як ваш тест. Кращим способом перевірити наявність витоків пам’яті може бути зателефонувавши main10 000 разів замість одного разу та побачити, чи закінчується ви набагато більше пам’яті, яка використовується в кінці.
Пол Д. Уейт

3
@ PaulD.Waite Так, можливо. Але мені здається, що мені все-таки потрібен детальний аналіз рівня, щоб точно визначити, у чому проблема, а не просто бути в змозі сказати (чи не сказати): "Гаразд, тут десь проблема пам'яті". І у мене складається враження, що я маю змогу користуватися їхнім профілером на такому детальному рівні ... Я просто не впевнений, як :(
EleventyOne

Спробуйте поглянути на youtube.com/watch?v=L3ugr9BJqIs
май

Відповіді:


205

Хороший робочий процес для пошуку витоків пам’яті - це три технології знімка , вперше використані Лоріною Лі та командою Gmail для вирішення деяких проблем із пам’яттю. Загалом, такі дії:

  • Зробіть куповий знімок.
  • Робіть речі.
  • Зробіть ще один знімок купи.
  • Повторіть те ж саме.
  • Зробіть ще один знімок купи.
  • Фільтруйте об'єкти, розподілені між знімками 1 та 2 у вікні "Підсумок" "Знімок 3".

Для вашого прикладу я адаптував код для відображення цього процесу (ви можете знайти його тут ), затримуючи створення Перегляду магістралі до події натискання кнопки «Пуск». Зараз:

  • Запустіть HTML (збережено локально від використання цієї адреси) ) та зробіть знімок.
  • Клацніть Почати, щоб створити подання.
  • Зробіть ще один знімок.
  • Натисніть кнопку Видалити.
  • Зробіть ще один знімок.
  • Фільтруйте об'єкти, розподілені між знімками 1 та 2 у вікні "Підсумок" "Знімок 3".

Тепер ви готові знайти витоки пам'яті!

Ви помітите вузли кількох різних кольорів. Червоні вузли не мають прямих посилань з Javascript на них, але живі, оскільки вони є частиною відокремленого дерева DOM. У дереві може бути вузол, на який посилається Javascript (можливо, як закриття або змінна), але збіг випадково перешкоджає збору сміття для всього дерева DOM.

введіть тут опис зображення

Однак жовті вузли мають прямі посилання на Javascript. Шукайте жовті вузли в тому самому відокремленому дереві DOM, щоб знайти посилання зі свого Javascript. Повинно бути ланцюг властивостей, що веде від вікна DOM до елемента.

У вашому конкретному елементі ви можете бачити елемент HTML Div, позначений червоним кольором. Якщо ви розгорнете елемент, ви побачите, на який посилається функція "кеш".

введіть тут опис зображення

Виберіть рядок і в консолі наберіть $ 0, ви побачите фактичну функцію та розташування:

>$0
function cache( key, value ) {
        // Use (key + " ") to avoid collision with native prototype properties (see Issue #157)
        if ( keys.push( key += " " ) > Expr.cacheLength ) {
            // Only keep the most recent entries
            delete cache[ keys.shift() ];
        }
        return (cache[ key ] = value);
    }                                                     jquery-2.0.2.js:1166

Тут посилається на ваш елемент. На жаль, ви не дуже багато можете зробити, це внутрішній механізм від jQuery. Але, лише для тестування, перейдіть до функції та змініть метод на:

function cache( key, value ) {
    return value;
}

Тепер, якщо ви повторите процес, ви не побачите жодного червоного вузла :)

Документація:


8
Я ціную ваші зусилля. Дійсно, три методики знімків регулярно згадуються в навчальних посібниках. На жаль, деталі часто залишаються осторонь. Наприклад, я ціную введення $0функції в консолі, що було для мене новим - я, звичайно, не маю уявлення, що це робить, і як ти знав, як нею користуватися ( $1здається марним, хоча, $2здається, робить те саме). По-друге, як ви знали виділити рядок, #button in function cache()а не жоден з інших десятків рядків? Нарешті, є червоні вузли NodeListі HTMLInputElementтеж, але я не можу їх зрозуміти.
EleventyOne

7
Звідки ви дізналися, що cacheрядок містить інформацію, а інші -? Є численні гілки, які мають меншу відстань від cacheтієї. І я не впевнений, як ти знав, що HTMLInputElementце дитина HTMLDivElement. Я бачу, що на нього посилається всередині нього ("уроджене в HTMLDivElement"), але воно також посилається на себе та два HTMLButtonElements, що для мене не має сенсу. Я, безумовно, вдячний, що ви визначили відповідь для цього прикладу, але я справді не маю уявлення, як узагальнити це на інші питання.
EleventyOne

2
Це дивно, я використовував ваш приклад, і я отримав інший результат, ніж ви (зі свого скріншоту). Тим не менш, я дуже ціную всю вашу допомогу. Я думаю, що мені зараз достатньо, і коли у мене є приклад із реального життя, що мені потрібна конкретна допомога, я буду створювати нове запитання тут. Знову дякую.
одинадцять

2
Пояснення до $ 0 можна знайти тут: developer.chrome.com/devtools/docs/commandline-api#0-4
Сукрит Гупта

4
Що Filter objects allocated between Snapshots 1 and 2 in Snapshot 3's "Summary" view.означає?
K - Токсичність в SO зростає.

8

Ось порада щодо профілювання пам'яті jsfiddle: Використовуйте наступну URL-адресу, щоб ізолювати результат jsfiddle, він видаляє всі рамки jsfiddle і завантажує лише ваш результат.

http://jsfiddle.net/4QhR2/show/

Мені ніколи не вдалося зрозуміти, як за допомогою шкали часу та Profiler відстежувати витоки пам’яті, доки я не прочитав наступну документацію. Прочитавши розділ «Відстежувач розподілу об’єктів», я зміг скористатися інструментом «Записати розподіл кучки» та відстежити деякі окремі вузли DOM.

Я вирішив проблему, перейшовши від прив'язки подій jQuery до використання делегування подій Backbone. Я розумію, що новіші версії Backbone автоматично відключать події для вас, якщо ви телефонуєтеView.remove() . Виконайте деякі демонстрації самостійно, вони налаштовані з витоками пам’яті, щоб ви могли ідентифікувати. Сміливо задайте тут питання, якщо ви все ще не отримаєте його після вивчення цієї документації.

https://developers.google.com/chrome-developer-tools/docs/javascript-memory-profiling


6

В основному вам потрібно переглянути кількість об’єктів всередині вашої купки. Якщо кількість об'єктів збільшується між двома знімками, і ви утилізуєте об'єкти, у вас є витік пам'яті. Моя порада - шукати в своєму коді обробників подій, які не відриваються.


3
Наприклад, якщо я переглянув знімок jsfiddle, перш ніж натиснути «Видалити», є набагато більше 100 000 об’єктів. Де я б шукав об’єкти, які насправді створив код моєї jsfiddle? Я думав, що це Window/http://jsfiddle.net/4QhR2/showможе бути корисним, але це просто нескінченні функції. Я поняття не маю, що там відбувається.
EleventyOne

@EleventyOne: Я б не використовував jsFiddle. Чому б просто не створити файл на власному комп’ютері для тестування?
Blue Skies

1
@BlueSkies Я створив jsfiddle, щоб люди могли працювати з тієї ж кодової бази. Тим не менше, коли я створюю на власному комп’ютері файл для тестування, у купі знімків ще є 50 000+ об’єктів.
EleventyOne

@EleventyOne Один куповий знімок не дає вам уявлення про те, чи є у вас витік пам'яті чи ні. Вам потрібно як мінімум два.
Костянтин Дінев

2
Справді. Я наголошував на тому, як важко знати, на що звертати увагу, коли присутні тисячі об’єктів.
EleventyOne


3

Ви також можете подивитися на вкладку "Шкала" в інструментах розробників. Запишіть використання програми та слідкуйте за кількістю слухачів вузла DOM та слухачів подій.

Якщо графік пам’яті дійсно вказував би на витік пам’яті, то можна скористатися профілером, щоб з’ясувати, що витікає.


3

Ви також можете прочитати:

http://addyosmani.com/blog/taming-the-unicorn-easing-javascript-memory-profiling-in-devtools/

Він пояснює використання інструментів для розробників хрому та дає кілька покрокових порад щодо підтвердження та виявлення витоку пам’яті за допомогою порівняння знімків у групі та різних доступних зображень гепатозу.


2

Я надаю пораду зробити знімок у купі, вони чудові для виявлення витоків пам’яті, хром робить відмінну роботу з знімком.

У своєму дослідницькому проекті на мій ступінь я будував інтерактивний веб-додаток, який повинен був генерувати безліч даних, накопичених у "шарах", багато з цих шарів буде "видалено" в інтерфейсі, але чомусь пам'ять не була маючи місце розташування, використовуючи інструмент знімка, я зміг визначити, що JQuery зберігає посилання на об’єкт (джерело було тоді, коли я намагався викликати .load()подію, яка зберігала посилання, незважаючи на те, що виходило за рамки). Маючи цю інформацію під рукою вручну врятувавши мій проект, це дуже корисний інструмент, коли ви користуєтеся чужими бібліотеками, і у вас є ця проблема тривалих посилань, що не дозволяють GC виконувати свою роботу.

EDIT: Також корисно заздалегідь спланувати, які дії ви збираєтеся виконати, щоб мінімізувати витрачений час на знімок, висунути гіпотезу, що може спричинити проблему, і протестувати кожен сценарій, роблячи знімки до і після.


0

Кілька важливих зауважень щодо виявлення витоків пам'яті за допомогою інструментів розробника Chrome:

1) Сам Chrome має витоки пам'яті для певних елементів, таких як поля пароля та числа. https://bugs.chromium.org/p/chromium/isissue/detail?id=967438 . Уникайте використання цих під час налагодження під час налагодження, коли вони переробляють знімок вашої купи під час пошуку відокремлених елементів.

2) Уникайте що- небудь реєструвати на консолі браузера. Chrome не буде збирати сміття з предметів, записаних на консоль, це вплине на ваш результат. Ви можете придушити вихід, розмістивши наступний код на початку сценарію / сторінки:

console.log = function() {};
console.warn = console.log;
console.error = console.log;

3) Використовуйте знімки купи та шукайте "від'єднати" для ідентифікації відокремлених елементів DOM. Наведіть курсор на об’єкти, ви отримаєте доступ до всіх властивостей, включаючи id та externalHTML, які можуть допомогти визначити кожен елемент. Знімок екрана JS Heap Snapshot з деталями про окремий елемент DOM Якщо відокремлені елементи все ще занадто загальні для розпізнавання, призначте їм унікальні ідентифікатори за допомогою консолі браузера перед запуском тесту, наприклад:

var divs = document.querySelectorAll("div");
for (var i = 0 ; i < divs.length ; i++)
{
    divs[i].id = divs[i].id || "AutoId_" + i;
}
divs = null; // Free memory

Тепер, коли ви ідентифікуєте відокремлений елемент, дозвольте сказати id = "AutoId_49", перезавантажте сторінку, виконайте фрагмент ще раз і знайдіть елемент з id = "AutoId_49" за допомогою інспектора DOM або document.querySelector (..) . Природно, це працює лише в тому випадку, якщо вміст вашої сторінки передбачуваний.

Як запускати тести, щоб виявити витоки пам'яті

1) Завантажити сторінку (із витиском консолі пригніченим!)

2) Робіть на сторінці речі, які можуть призвести до витоку пам'яті

3) Використовуйте Інструменти для розробників, щоб зробити знімок у купі та шукати "від'єднати"

4) Наведіть курсор на елементи, щоб ідентифікувати їх за своїми властивостями id або externalHTML


Крім того, завжди корисно відключити мінімізацію / зняття помилок, оскільки це ускладнює налагодження в браузері.
Джиммі Томсен
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.