Znajdowanie wycieków pamięci JavaScript w przeglądarce Chrome

163

Stworzyłem bardzo prosty przypadek testowy, który tworzy widok Backbone, dołącza procedurę obsługi do zdarzenia i tworzy instancję klasy zdefiniowanej przez użytkownika. Uważam, że po kliknięciu przycisku „Usuń” w tym przykładzie wszystko zostanie wyczyszczone i nie powinno być żadnych wycieków pamięci.

Plik jsfiddle dla kodu jest tutaj: 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();

Nie mam jednak pewności, jak używać profilera Google Chrome, aby sprawdzić, czy tak jest w rzeczywistości. Jest miliard rzeczy, które pojawiają się na migawce programu profilującego sterty i nie mam pojęcia, jak rozszyfrować, co jest dobre, a co złe. Samouczki, które widziałem do tej pory, albo po prostu mówią mi, żebym „użył profilera migawek”, albo podają bardzo szczegółowy manifest na temat działania całego profilera. Czy można po prostu użyć programu profilującego jako narzędzia, czy naprawdę muszę zrozumieć, jak wszystko zostało zaprojektowane?

EDYCJA: Poradniki takie jak te:

Naprawianie wycieku pamięci w Gmailu

Korzystanie z DevTools

Są reprezentatywne dla niektórych silniejszych materiałów, z tego, co widziałem. Jednak poza przedstawieniem koncepcji 3 Snapshot Technique , uważam, że oferują one bardzo mało praktycznej wiedzy (dla początkującego takiego jak ja). Samouczek „Korzystanie z narzędzi dla programistów” nie opiera się na prawdziwym przykładzie, więc jego niejasny i ogólny koncepcyjny opis rzeczy nie jest zbyt pomocny. Jeśli chodzi o przykład „Gmail”:

Więc znalazłeś wyciek. Co teraz?

  • Sprawdź ścieżkę mocowania przecieków w dolnej połowie panelu Profile

  • Jeśli nie można łatwo wywnioskować lokalizacji alokacji (tj. Nasłuchiwania zdarzeń):

  • Instrumentuj konstruktora obiektu przechowującego za pośrednictwem konsoli JS, aby zapisać ślad stosu dla alokacji

  • Używasz zamknięcia? Włącz odpowiednią istniejącą flagę (np. Goog.events.Listener.ENABLE_MONITORING), aby ustawić właściwość createStack podczas tworzenia

Po przeczytaniu tego jestem bardziej zdezorientowany, a nie mniej. I znowu, po prostu mówi mi, żebym coś robił , a nie jak to robić. Z mojej perspektywy wszystkie dostępne informacje są albo zbyt niejasne, albo miałyby sens tylko dla kogoś, kto już zrozumiał ten proces.

Niektóre z tych bardziej szczegółowych kwestii zostały poruszone w odpowiedzi @Jonathana Naguina poniżej.

Eleventy One
źródło
2
Nie wiem nic o testowaniu wykorzystania pamięci w przeglądarkach, ale jeśli tego nie widziałeś, pomocny może być artykuł Addy Osmani o inspektorze sieci Chrome .
Paul D. Waite
1
Dzięki za sugestię, Paul. Jednak gdy wykonuję jedną migawkę przed kliknięciem przycisku Usuń, a następnie drugą po jej kliknięciu, a następnie wybieram „obiekty przydzielone między migawkami 1 i 2” (zgodnie z sugestią jego artykułu), nadal jest obecnych ponad 2000 obiektów. Na przykład istnieją 4 wpisy „HTMLButtonElement”, które nie mają dla mnie sensu. Naprawdę nie mam pojęcia, co się dzieje.
EleventyOne
3
doh, to nie brzmi szczególnie pomocnie. Może się zdarzyć, że z językiem gromadzącym śmieci, takim jak JavaScript, tak naprawdę nie jesteś przeznaczony do weryfikowania tego, co robisz z pamięcią na poziomie tak szczegółowym, jak test. Lepszym sposobem sprawdzenia wycieków pamięci może być wywołanie main10000 razy zamiast jednego razu i sprawdzenie, czy na końcu zostanie użytych dużo więcej pamięci.
Paul D. Waite
3
@ PaulD.Waite Yeah, może. Ale wydaje mi się, że nadal potrzebowałbym szczegółowej analizy poziomu, aby dokładnie określić, na czym polega problem, zamiast po prostu móc powiedzieć (lub nie powiedzieć): „OK, gdzieś tu jest problem z pamięcią”. I mam wrażenie, że powinienem móc używać ich profilera na tak szczegółowym poziomie ... Nie jestem tylko pewien jak :(
EleventyOne
Powinieneś zajrzeć na youtube.com/watch?v=L3ugr9BJqIs
maja

Odpowiedzi:

205

Dobrym sposobem na znalezienie wycieków pamięci jest technika trzech migawek , po raz pierwszy zastosowana przez Loreenę Lee i zespół Gmaila do rozwiązania niektórych problemów z pamięcią. Ogólnie kroki są następujące:

  • Zrób migawkę sterty.
  • Robić coś.
  • Zrób kolejną migawkę sterty.
  • Powtórz te same rzeczy.
  • Zrób kolejną migawkę sterty.
  • Filtruj obiekty przydzielone między migawkami 1 i 2 w widoku „Podsumowanie” migawki 3.

Na przykład dostosowałem kod, aby pokazać ten proces (możesz go znaleźć tutaj ), opóźniając tworzenie widoku szkieletu do momentu kliknięcia przycisku Start. Teraz:

  • Uruchom kod HTML (zapisany lokalnie pod tym adresem ) i zrób migawkę.
  • Kliknij Start, aby utworzyć widok.
  • Zrób kolejną migawkę.
  • Kliknij usuń.
  • Zrób kolejną migawkę.
  • Filtruj obiekty przydzielone między migawkami 1 i 2 w widoku „Podsumowanie” migawki 3.

Teraz jesteś gotowy, aby znaleźć wycieki pamięci!

Zauważysz węzły o kilku różnych kolorach. Węzły czerwone nie mają bezpośrednich odwołań z JavaScript do nich, ale są żywe, ponieważ są częścią odłączonego drzewa DOM. W drzewie może znajdować się węzeł, do którego odwołuje się kod JavaScript (może to być zamknięcie lub zmienna), ale przypadkowo uniemożliwia to zebranie elementów bezużytecznych w całym drzewie DOM.

wprowadź opis obrazu tutaj

Jednak żółte węzły mają bezpośrednie odniesienia z JavaScript. Poszukaj żółtych węzłów w tym samym odłączonym drzewie DOM, aby zlokalizować odwołania z JavaScript. Powinien istnieć łańcuch właściwości prowadzący od okna DOM do elementu.

W szczególności możesz zobaczyć element HTML Div oznaczony na czerwono. Jeśli rozwiniesz element, zobaczysz, że odwołuje się do niego funkcja „cache”.

wprowadź opis obrazu tutaj

Wybierz wiersz iw konsoli wpisz $ 0, zobaczysz aktualną funkcję i lokalizację:

>$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

Tutaj znajduje się odniesienie do Twojego elementu. Niestety niewiele można zrobić, jest to wewnętrzny mechanizm z jQuery. Ale dla celów testowych przejdź do funkcji i zmień metodę na:

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

Teraz, jeśli powtórzysz proces, nie zobaczysz żadnego czerwonego węzła :)

Dokumentacja:

Jonathan Naguin
źródło
8
Doceniam twój trud, twoje starania. Rzeczywiście, technika trzech migawek jest regularnie wymieniana w samouczkach. Niestety, szczegóły są często pomijane. Na przykład doceniam wprowadzenie $0funkcji w konsoli, która była dla mnie nowa - oczywiście nie mam pojęcia, co to robi ani skąd wiedziałeś, jak go używać ( $1wydaje się bezużyteczne, podczas gdy $2wydaje się robić to samo). Po drugie, skąd wiedziałeś, że należy wyróżnić wiersz, #button in function cache()a nie którykolwiek z pozostałych dziesiątek wierszy? Wreszcie pojawiają się czerwone węzły NodeListi HTMLInputElementteż, ale nie mogę ich rozgryźć.
EleventyOne
7
Skąd wiedziałeś, że cachewiersz zawiera informacje, a pozostałe nie? Istnieje wiele gałęzi, które mają mniejszą odległość niż ta cache. I nie jestem pewien, skąd wiedziałeś, że HTMLInputElementjest dzieckiem HTMLDivElement. Widzę, że odwołuje się do niego w środku („natywny w HTMLDivElement”), ale odwołuje się również do siebie i dwóch HTMLButtonElement, co nie ma dla mnie sensu. Z pewnością doceniam, że podałeś odpowiedź na ten przykład, ale naprawdę nie miałbym pojęcia, jak uogólnić to na inne kwestie.
EleventyOne
2
To dziwne, użyłem twojego przykładu i otrzymałem inny wynik niż ty (z twojego zrzutu ekranu). Niemniej jednak bardzo doceniam twoją pomoc. Myślę, że na razie mam dość, a kiedy mam przykład z życia wzięty, z którym potrzebuję konkretnej pomocy, utworzę tutaj nowe pytanie. Dzięki jeszcze raz.
EleventyOne
2
Wyjaśnienie do 0 $ można znaleźć tutaj: developer.chrome.com/devtools/docs/commandline-api#0-4
Sukrit Gupta
4
Co to Filter objects allocated between Snapshots 1 and 2 in Snapshot 3's "Summary" view.znaczy?
K - Toksyczność SO rośnie.
8

Oto wskazówka dotycząca profilowania pamięci jsfiddle: Użyj następującego adresu URL, aby wyodrębnić wynik jsfiddle, usuwa on całą strukturę jsfiddle i ładuje tylko twój wynik.

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

Nigdy nie byłem w stanie dowiedzieć się, jak używać osi czasu i profilera do śledzenia wycieków pamięci, dopóki nie przeczytałem poniższej dokumentacji. Po przeczytaniu sekcji zatytułowanej „Śledzenie alokacji obiektów” udało mi się użyć narzędzia „Rejestruj alokacje sterty” i śledzić kilka odłączonych węzłów DOM.

Rozwiązałem problem, przełączając się z wiązania zdarzeń jQuery na delegowanie zdarzeń Backbone. Rozumiem, że nowsze wersje Backbone automatycznie usuwają powiązania wydarzeń, jeśli zadzwonisz View.remove(). Wykonaj samodzielnie niektóre dema, są one skonfigurowane z wyciekami pamięci, które możesz zidentyfikować. Jeśli po przestudiowaniu tej dokumentacji nadal nie otrzymasz odpowiedzi, możesz zadawać pytania.

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

Rick Suggs
źródło
6

Zasadniczo musisz przyjrzeć się liczbie obiektów wewnątrz migawki sterty. Jeśli liczba obiektów wzrośnie między dwoma migawkami i pozbyłeś się obiektów, masz wyciek pamięci. Radzę poszukać w swoim kodzie programów obsługi zdarzeń, które nie są odłączane.

Konstantin Dinev
źródło
3
Na przykład, jeśli spojrzę na migawkę sterty pliku jsfiddle, zanim kliknę „Usuń”, jest tam dużo ponad 100 000 obiektów. Gdzie miałbym szukać obiektów, które faktycznie utworzył kod mojego jsfiddle? Pomyślałem, że Window/http://jsfiddle.net/4QhR2/showmoże się przydać, ale to tylko nieskończone funkcje. Nie mam pojęcia, co się tam dzieje.
EleventyOne
@EleventyOne: Nie użyłbym jsFiddle. Dlaczego po prostu nie utworzyć pliku na własnym komputerze do testów?
Blue Skies
1
@BlueSkies Zrobiłem jsfiddle, aby ludzie tutaj mogli pracować z tego samego kodu. Niemniej jednak, kiedy tworzę plik na własnym komputerze do testowania, w migawce sterty wciąż znajduje się ponad 50 000 obiektów.
EleventyOne
@EleventyOne One migawka sterty nie daje pojęcia, czy masz wyciek pamięci, czy nie. Potrzebujesz co najmniej dwóch.
Konstantin Dinev
2
W rzeczy samej. Podkreślałam, jak trudno jest wiedzieć, czego szukać, gdy obecne są tysiące obiektów.
EleventyOne
3

Możesz również spojrzeć na kartę Oś czasu w narzędziach programistycznych. Rejestruj użycie swojej aplikacji i miej oko na liczbę węzłów DOM i odbiorników zdarzeń.

Jeśli wykres pamięci rzeczywiście wskazywałby na wyciek pamięci, możesz użyć profilera, aby dowiedzieć się, co przecieka.

Robert Falkén
źródło
2

Popieram radę, aby zrobić migawkę sterty, są doskonałe do wykrywania wycieków pamięci, chrome świetnie radzi sobie z migawkami.

W moim projekcie badawczym na stopień naukowy budowałem interaktywną aplikację internetową, która musiała generować dużo danych zgromadzonych w „warstwach”, wiele z tych warstw zostanie „usuniętych” w interfejsie użytkownika, ale z jakiegoś powodu pamięć nie po zwolnieniu przydziału, za pomocą narzędzia do tworzenia migawek byłem w stanie określić, że JQuery zachowywał odniesienie do obiektu (źródło było, gdy próbowałem wyzwolić .load()zdarzenie, które zachowało odniesienie pomimo wyjścia poza zakres). Mając te informacje pod ręką, samodzielnie zapisując mój projekt, jest to bardzo przydatne narzędzie, gdy używasz bibliotek innych osób i masz problem z utrzymującymi się referencjami, które uniemożliwiają GC wykonywanie swojej pracy.

EDYCJA: Warto również zaplanować z wyprzedzeniem, jakie czynności zamierzasz wykonać, aby zminimalizować czas spędzony na tworzeniu migawek, postawić hipotezę, co może być przyczyną problemu i przetestować każdy scenariusz, wykonując migawki przed i po.

ProgrammerInProgress
źródło
0

Kilka ważnych uwag dotyczących identyfikowania wycieków pamięci za pomocą narzędzi Chrome Developer:

1) Sam Chrome ma wycieki pamięci dla niektórych elementów, takich jak hasła i pola liczbowe. https://bugs.chromium.org/p/chromium/issues/detail?id=967438 . Unikaj używania ich podczas debugowania, ponieważ sprawdzają one migawkę sterty podczas wyszukiwania odłączonych elementów.

2) Unikaj rejestrowania czegokolwiek w konsoli przeglądarki. Chrome nie zbiera śmieci zapisanych na konsoli, co wpływa na wynik. Możesz wyłączyć wyjście, umieszczając następujący kod na początku skryptu / strony:

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

3) Użyj migawek sterty i wyszukaj „odłącz”, aby zidentyfikować odłączone elementy DOM. Umieszczając kursor na obiektach, uzyskujesz dostęp do wszystkich właściwości, w tym id i externalHTML, które mogą pomóc w identyfikacji każdego elementu. Zrzut ekranu migawki JS Heap ze szczegółami dotyczącymi odłączonego elementu DOM Jeśli odłączone elementy są nadal zbyt ogólne, aby je rozpoznać, przed uruchomieniem testu przypisz im unikalne identyfikatory za pomocą konsoli przeglądarki, np .:

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

Teraz, gdy zidentyfikujesz odłączony element za pomocą, powiedzmy id = "AutoId_49", załaduj ponownie stronę, ponownie wykonaj powyższy fragment i znajdź element z id = "AutoId_49" za pomocą inspektora DOM lub document.querySelector (..) . Oczywiście działa to tylko wtedy, gdy zawartość strony jest przewidywalna.

Jak przeprowadzam testy, aby zidentyfikować wycieki pamięci

1) Załaduj stronę (z wyłączonym wyjściem konsoli!)

2) Rób na stronie rzeczy, które mogą spowodować wycieki pamięci

3) Użyj narzędzi deweloperskich, aby wykonać migawkę sterty i wyszukać „odłącz”

4) Najedź na elementy, aby zidentyfikować je na podstawie ich właściwości id lub externalHTML

Jimmy Thomsen
źródło
Ponadto zawsze dobrze jest wyłączyć minifikację / uglifikowanie, ponieważ utrudnia to debugowanie w przeglądarce.
Jimmy Thomsen