Efektywne zarządzanie pamięcią na dużą skalę w Gmailu

John McCutchan
John McCutchan
Loreena Lee
Loreena Lee

Wprowadzenie

Chociaż JavaScript używa zbierania elementów do automatycznego zarządzania pamięcią, nie zastępuje to skutecznego zarządzania pamięcią w aplikacjach. Aplikacje JavaScript mają te same problemy z pamięcią co natywne aplikacje, np. wycieki pamięci i zaśmiecanie pamięci, ale muszą też radzić sobie z przerwami w zbieraniu pamięci podręcznej. Duże aplikacje, takie jak Gmail, mają te same problemy co mniejsze aplikacje. Czytaj dalej, aby dowiedzieć się, jak zespół Gmaila używał Narzędzi programistycznych Chrome do identyfikowania, izolowania i rozwiązywania problemów z pamięcią.

Sesja z Google I/O 2013

Materiał ten został zaprezentowany na konferencji Google I/O 2013. Obejrzyj film poniżej:

Gmail, mamy problem…

Zespół Gmaila miał poważny problem. Coraz częściej słyszeliśmy o tym, że karty Gmaila zużywają wiele gigabajtów pamięci na laptopach i komputerach stacjonarnych z ograniczonymi zasobami, co często kończyło się zawieszaniem całej przeglądarki. historie o procesorach przypinanych na poziomie 100%, aplikacjach, które przestają odpowiadać, i kartach Chrome, które stają się smutne („Jest martwy, Jimie”). Zespół nie wiedział, jak zacząć diagnozować problem, nie mówiąc już o jego rozwiązaniu. Nie mieli pojęcia, jak rozpowszechniony jest ten problem, a dostępne narzędzia nie nadawały się do dużych aplikacji. Współpracując z zespołami Chrome, opracowali nowe techniki diagnozowania problemów z pamięcią, ulepszali istniejące narzędzia oraz umożliwili zbieranie danych o pamięci z pola. Zanim jednak przejdziemy do narzędzi, omówimy podstawy zarządzania pamięcią w JavaScript.

Podstawy zarządzania pamięcią

Zanim zaczniesz efektywnie zarządzać pamięcią w JavaScript, musisz poznać podstawy. W tej sekcji omówimy typy proste, graf obiektów oraz definicje ogólnego zaśmiecania pamięci i wycieku pamięci w JavaScript. Pamięć w JavaScript może być postrzegana jako graf, dlatego teoria grafów odgrywa pewną rolę w zarządzaniu pamięcią w JavaScript i w profilowaniu sterty.

Typy elementów podstawowych

JavaScript ma 3 typy prymitywne:

  1. Liczba (np. 4, 3,14159)
  2. Wartość logiczna (prawda lub fałsz)
  3. Ciąg znaków („Hello World”)

Te typy prymitywne nie mogą odwoływać się do żadnych innych wartości. W grach obiektów te wartości są zawsze węzłami końcowymi lub węzłami liściastymi, co oznacza, że nigdy nie mają krawędzi wychodzących.

Istnieje tylko jeden typ kontenera: obiekt. W JavaScript obiekt jest tablicą asocjacyjną. Niepusty obiekt to wewnętrzny węzeł z krawędziami wychodzącymi do innych wartości (węzłów).

Co z tablicami?

Tablica w JavaScript jest w rzeczywistości obiektem z kluczami numerycznymi. Jest to uproszczenie, ponieważ środowisko uruchomieniowe JavaScript będzie optymalizować obiekty podobne do tablic i reprezentować je pod maską jako tablice.

Terminologia

  1. Wartość – instancja typu prymitywnego, obiektu, tablicy itp.
  2. Zmienna – nazwa odwołująca się do wartości.
  3. Właściwość – nazwa w obiekcie, która odwołuje się do wartości.

Obiektowy wykres

Wszystkie wartości w JavaScriptzie są częścią grafu obiektów. Graf zaczyna się od korzeni, np. obiektu okna. Nie masz kontroli nad czasem trwania korzeni GC, ponieważ są one tworzone przez przeglądarkę i usuwane po usunięciu strony. Zmienne globalne to w istocie właściwości okna.

Graf obiektów

Kiedy wartość staje się śmieciem?

Wartość staje się nieużyteczna, gdy nie ma ścieżki od korzenia do tej wartości. Innymi słowy, zaczynając od źródeł i wyczerpująco przeszukując wszystkie właściwości i zmienną obiektu, które są aktywne w ramce stosu, nie można uzyskać wartości, ponieważ stała się ona odpadem.

Wykres śmieci

Czym jest wyciek pamięci w JavaScript?

Wyciek pamięci w JavaScript najczęściej występuje, gdy istnieją węzły DOM, do których nie można dotrzeć z drzewa DOM strony, ale są one nadal dostępne dla obiektu JavaScript. Chociaż nowoczesne przeglądarki utrudniają przypadkowe wycieki danych, są one nadal łatwiejsze do wywołania, niż mogłoby się wydawać. Załóżmy, że dołączasz element do drzewa DOM w ten sposób:

email.message = document.createElement("div");
displayList.appendChild(email.message);

A później usuwasz element z listy wyświetlania:

displayList.removeAllChildren();

Dopóki element email istnieje, element DOM, do którego odwołuje się wiadomość, nie zostanie usunięty, nawet jeśli jest teraz odłączony od drzewa DOM strony.

Co to jest bloat?

Gdy używasz więcej pamięci niż jest to konieczne do zapewnienia optymalnej szybkości strony, Twoja strona jest zablokowana. Ucieknięcia pamięci również pośrednio powodują nadmierne rozrosty, ale nie jest to zamierzone. Pamięć podręczna aplikacji, która nie ma żadnych ograniczeń rozmiaru, jest częstym źródłem rozrostu pamięci. Strona może też być za duża z powodu danych hosta, np. danych pikselowych załadowanych z obrazów.

Czym jest czyszczenie pamięci?

Czyszczenie pamięci to sposób odzyskiwania pamięci w JavaScript. O tym, kiedy to nastąpi, decyduje przeglądarka. Podczas zbioru wszystkie skrypty na stronie są zawieszone, a wartości na żywo są wykrywane przez przeszukiwanie grafu obiektów, zaczynając od korzeni zbioru. Wszystkie wartości, które nie są dostępne, są klasyfikowane jako odpad. Pamięć dla wartości nieużytecznych jest odzyskiwana przez menedżera pamięci.

Szczegóły dotyczące zbieracza śmieci V8

Aby lepiej zrozumieć, jak działa zbieranie elementów, przyjrzyjmy się bliżej zbieraczowi elementów V8. V8 używa kolekcjonera generacyjnego. Pamięć jest podzielona na 2 pokolenia: młodsze i starsze. Przydzielanie i zbieranie danych w przypadku młodego pokolenia jest szybkie i częste. Przydzielanie i gromadzenie danych w ramach starszej generacji jest wolniejsze i rzadziej występujące.

Generational Collector

V8 używa kolektora generującego 2 pokolenia. Wiek wartości jest zdefiniowany jako liczba bajtów przypisanych od momentu przypisania. W praktyce wiek wartości jest często przybliżony na podstawie liczby kolekcji z młodszego pokolenia, które przetrwały. Gdy wartość jest wystarczająco stara, staje się częścią starszej generacji.

W praktyce świeżo przydzielone wartości nie są długotrwałe. Badanie programów Smalltalk wykazało, że tylko 7% wartości przetrwało po zebraniu danych przez młodsze pokolenie. Z badań przeprowadzonych w różnych środowiskach uruchomieniowych wynika, że średnio od 90% do 70% świeżo przydzielonych wartości nigdy nie jest przenoszonych do starszej generacji.

Young Generation

Stos młodej generacji w V8 jest podzielony na 2 przestrzenie o nazwach from i to. Pamięć jest przydzielana z przestrzeni to. Przydzielanie jest bardzo szybkie, dopóki miejsce nie zostanie zapełnione. Wtedy uruchamiana jest kolekcja młodego pokolenia. Kolekcja młodszej generacji najpierw zamienia pola „From” (od) i „To” (do). Stare pole „To” (teraz pole „From”) jest skanowane, a wszystkie aktualne wartości są kopiowane do pola „To” lub przenoszone do starszej generacji. Typowa kolekcja młodego pokolenia zajmuje około 10 milisekund (ms).

Zwyczajowo każda alokacja dokonywana przez aplikację przybliża Cię do wyczerpania miejsca i zatrzymania GC. Uwaga deweloperzy gier: aby zapewnić czas wyświetlania klatki wynoszący 16 ms (wymagany do osiągnięcia 60 klatek na sekundę), aplikacja musi nie przydzielać żadnych zasobów, ponieważ pojedyncze zbiory młodej generacji pochłoną większość czasu przeznaczonego na wyświetlanie klatki.

Stos pamięci młodej generacji

Stara generacja

Kopie zapasowe starej generacji w V8 są zbierane przy użyciu algorytmu znakowania i skompresowania. Przypisania starej generacji występują, gdy wartość jest przenoszona z młodej generacji do starej. Gdy występuje kolekcja starej generacji, tworzona jest też kolekcja nowej generacji. Aplikacja zostanie wstrzymana na kilka sekund. W praktyce jest to dopuszczalne, ponieważ kolekcje starszej generacji są rzadkie.

V8 GC podsumowanie

Automatyczne zarządzanie pamięcią z wykorzystaniem funkcji zbierania elementów do usunięcia jest bardzo korzystne dla wydajności programistów, ale za każdym razem, gdy przydzielasz wartość, zbliżasz się do przerwy w zbieraniu elementów do usunięcia. Pauzy w zbieraniu pamięci mogą zepsuć odbiór aplikacji, powodując jej zacinanie. Teraz, gdy już wiesz, jak JavaScript zarządza pamięcią, możesz dokonać odpowiednich wyborów w przypadku swojej aplikacji.

Rozwiązywanie problemów z Gmailem

W ciągu ostatniego roku do narzędzi deweloperskich w Chrome dodano wiele funkcji i poprawek, dzięki którym są one teraz jeszcze potężniejsze. Ponadto sama przeglądarka wprowadziła kluczową zmianę w interfejsie performance.memory API, która umożliwia Gmailowi i innym aplikacjom zbieranie statystyk pamięci z pola. Dzięki tym wspaniałym narzędziom to, co kiedyś wydawało się niemożliwe, stało się ekscytującą zabawą w śledzenie za winowajcami.

Narzędzia i techniki

Zgromadzone dane i interfejs performance.memory API

Od wersji 22 przeglądarki Chrome interfejs performance.memory API jest domyślnie włączony. W przypadku aplikacji długo działających, takich jak Gmail, dane od rzeczywistych użytkowników są bezcenne. Te informacje pozwalają nam odróżnić użytkowników zaawansowanych (korzystających z Gmaila przez 8–16 godzin dziennie i otrzymujących setki wiadomości dziennie) od użytkowników przeciętnych, którzy spędzają w Gmailu kilka minut dziennie i otrzymują około tuzina wiadomości tygodniowo.

Ten interfejs API zwraca 3 elementy danych:

  1. jsHeapSizeLimit – ilość pamięci (w bajtach), do której ograniczony jest stos JavaScript.
  2. totalJSHeapSize – ilość pamięci (w bajtach) przydzielonej stosowi JavaScript, w tym wolne miejsce.
  3. usedJSHeapSize – ilość pamięci (w bajtach), która jest obecnie używana.

Pamiętaj, że interfejs API zwraca wartości pamięci dla całego procesu Chrome. Chociaż nie jest to domyślny tryb, w pewnych okolicznościach Chrome może otworzyć wiele kart w tym samym procesie renderowania. Oznacza to, że wartości zwracane przez performance.memory mogą zawierać informacje o zajętej pamięci przez inne karty przeglądarki oprócz tej, na której znajduje się Twoja aplikacja.

Pomiar pamięci na dużą skalę

Gmail zmodyfikował swój kod JavaScript, aby używać interfejsu performance.memory API do zbierania informacji o pamięci około raz na 30 minut. Ponieważ wielu użytkowników Gmaila pozostawia aplikację otwartą przez wiele dni, zespół mógł śledzić wzrost wykorzystania pamięci na przestrzeni czasu oraz ogólne statystyki dotyczące wykorzystania pamięci. W ciągu kilku dni od instrumentowania Gmaila w celu zbierania informacji o pamięci od losowo wybranych użytkowników zespół zebrał wystarczającą ilość danych, aby zrozumieć, jak powszechne są problemy z pamięcią wśród przeciętnych użytkowników. Użytkownicy określili punkt odniesienia i wykorzystywali strumień danych wejściowych do śledzenia postępów w realizacji celu polegającego na zmniejszeniu wykorzystania pamięci. Ostatecznie te dane będą również wykorzystywane do wykrywania regresji pamięci.

Poza śledzeniem pomiary w polu dają też wgląd w korelację między zapotrzebowaniem na pamięć a wydajnością aplikacji. Wbrew powszechnemu przekonaniu, że „więcej pamięci to lepsza wydajność”, zespół Gmaila odkrył, że im większy ślad pamięci, tym dłuższy czas oczekiwania na wykonanie typowych działań w Gmailu. Dzięki temu odkryciu byli bardziej zmotywowani niż kiedykolwiek do ograniczenia zużycia pamięci.

Pomiar pamięci na dużą skalę

Wykrywanie problemów z pamięcią za pomocą osi czasu w Narzędziach deweloperskich

Pierwszym krokiem w rozwiązywaniu problemu ze skutecznością jest udowodnienie, że problem istnieje, utworzenie testu, który można powtórzyć, i przeprowadzenie pomiaru bazowego. Bez programu, który można odtworzyć, nie można wiarygodnie zmierzyć problemu. Bez pomiaru podstawowego nie wiesz, o ile wzrosła skuteczność.

Panel osi czasu w Narzędziach deweloperskich jest idealnym miejscem do udowodnienia, że problem istnieje. Zawiera ona pełny przegląd tego, ile czasu zajmuje wczytywanie aplikacji internetowej lub strony i interakcja z nimi. Na osi czasu są nanoszone wszystkie zdarzenia, od wczytywania zasobów po analizowanie kodu JavaScript, obliczanie stylów, przerwy w zbieraniu elementów do usunięcia i odświeżanie. W celu zbadania problemów z pamięcią panel osi czasu zawiera też tryb Pamięć, który śledzi łączną przydzieloną pamięć, liczbę węzłów DOM, liczbę obiektów okna i liczba przypisanych odbiorników zdarzeń.

Udowadnianie, że problem istnieje

Na początek zidentyfikuj sekwencję działań, które Twoim zdaniem powodują wyciek pamięci. Zacznij nagrywać oś czasu i wykonaj sekwencję działań. Aby wymusić pełne usunięcie elementów z kosza, kliknij przycisk kosza u dołu ekranu. Jeśli po kilku iteracjach zobaczysz wykres w kształcie piły, oznacza to, że przydzielasz wiele obiektów o krótkim czasie życia. Jeśli jednak sekwencja działań nie powinna powodować zatrzymania pamięci, a liczba węzłów DOM nie spada do wartości początkowej, masz podstawy, aby podejrzewać wyciek pamięci.

Wykres w kształcie piły

Gdy potwierdzisz, że problem istnieje, możesz zidentyfikować jego źródło za pomocą narzędzia Heap Profiler w Narzędziach deweloperskich.

Wykrywanie wycieków pamięci za pomocą narzędzia Heap Profiler w DevTools

Panel Profiler zawiera profilowanie procesora i profilowanie sterty. Profilowanie stosu polega na tworzeniu zrzutu graficznego grafu obiektów. Zanim zostanie wykonany zrzut, zarówno starsze, jak i młodsze generacje są zbierane do kosza. Innymi słowy, zobaczysz tylko wartości, które były aktywne w momencie wykonania zrzutu ekranu.

W tym artykule nie da się wyczerpująco omówić wszystkich funkcji profilatora Heap, ale szczegółową dokumentację znajdziesz na stronie dla deweloperów Chrome. Skupimy się tutaj na programie profilującym alokację sterty.

Korzystanie z programu profilującego alokację sterty

Profilowanie alokacji Heap łączy szczegółowe informacje o migawkach z Heap Profiler z ciągłym aktualizowaniem i śledzeniem w panelu osi czasu. Otwórz panel Profile, uruchom profil Rejestruj alokacje sterty, wykonaj sekwencję działań, a potem zatrzymaj nagrywanie na potrzeby analizy. Profilator alokacji okresowo tworzy migawki stosu (tak często jak co 50 ms!) i jedną końcową migawkę na końcu nagrywania.

Program profilujący alokacji sterty

Paski u góry wskazują, kiedy w pniu znaleziono nowe obiekty. Wysokość każdego słupka odpowiada rozmiarowi niedawno przydzielonych obiektów, a kolor słupków wskazuje, czy te obiekty są nadal aktywne w końcowym zrzucie stosu: niebieskie słupki wskazują obiekty, które są nadal aktywne na końcu osi czasu, a szare słupki wskazują obiekty, które zostały przydzielone w trakcie osi czasu, ale zostały już zebrane przez mechanizm garbage collection.

W przykładzie powyżej działanie zostało wykonane 10 razy. Przykładowy program przechowuje w pamięci podręcznej 5 obiektów, więc spodziewane są 5 ostatnich niebieskich pasków. Jednak niebieski pasek po lewej stronie wskazuje na potencjalny problem. Następnie możesz użyć suwaków na osi czasu powyżej, aby powiększyć wybrany moment i zobaczyć obiekty, które zostały niedawno przydzielone w tym miejscu. Kliknięcie konkretnego obiektu w steku spowoduje wyświetlenie w dolnej części migawki sterty drzewa utrzymującego. Przeanalizowanie ścieżki przechowywania obiektu powinno dostarczyć wystarczającej ilości informacji, aby zrozumieć, dlaczego obiekt nie został zebrany. Możesz wprowadzić niezbędne zmiany w kodzie, aby usunąć niepotrzebne odwołanie.

Rozwiązywanie problemu z pamięcią w Gmailu

Korzystając z omawianych wyżej narzędzi i technik, zespół Gmaila zidentyfikował kilka kategorii błędów: nieograniczone pamięci podręczne, nieskończenie rosnące tablice funkcji wywołujących, które czekają na coś, co nigdy się nie wydarzy, oraz odbiorców zdarzeń, które nieumyślnie przechowują swoje cele. Dzięki rozwiązaniu tych problemów udało się znacznie zmniejszyć ogólne wykorzystanie pamięci przez Gmaila. Użytkownicy w 99% przypadków zużywali o 80% mniej pamięci niż wcześniej, a zużycie pamięci przez użytkowników w średniej spadło o prawie 50%.

Wykorzystanie pamięci przez Gmaila

Ponieważ Gmail zużywa mniej pamięci, czas oczekiwania na wstrzymanie GC został skrócony, co poprawiło ogólny komfort użytkowników.

Ponadto zespół Gmaila, który zbierał statystyki dotyczące wykorzystania pamięci, odkrył regresję w Chrome związaną z odzyskiwaniem pamięci. W szczególności wykryto 2 błędy powodujące podział, gdy dane dotyczące pamięci w Gmailu zaczęły wykazywać znaczny wzrost różnicy między łączną przydzieloną pamięcią a pamięcią aktywną.

Wezwanie do działania

Zadaj sobie te pytania:

  1. Ile pamięci używa moja aplikacja? Możliwe, że używasz zbyt dużo pamięci, co wbrew powszechnemu przekonaniu ma negatywny wpływ na ogólną wydajność aplikacji. Trudno jest określić, ile dokładnie stron jest odpowiednia, ale pamiętaj, aby sprawdzić, czy dodatkowe buforowanie używane przez Twoją stronę ma wymierny wpływ na wydajność.
  2. Czy moja strona jest wolna od wycieków? Jeśli na Twojej stronie występuje wyciek pamięci, może to wpływać nie tylko na jej wydajność, ale też na inne karty. Użyj lokalizatora obiektów, aby zlokalizować wycieki.
  3. Jak często moja strona jest indeksowana przez Google? Każdą przerwę w GC możesz sprawdzić w panelu osi czasuNarzędziach dla deweloperów w Chrome. Jeśli Twoja strona często wykonuje GC, prawdopodobnie zbyt często przydzielasz pamięć, co powoduje częste odzyskiwanie pamięci z młodej generacji.

Podsumowanie

Zaczęliśmy od kryzysu. Omówiono podstawy zarządzania pamięcią w JavaScriptzie, a w szczególności w V8. Dowiedli się, jak korzystać z tych narzędzi, w tym z nowej funkcji śledzenia obiektów dostępnej w najnowszych wersjach Chrome. Dzięki tym informacjom zespół Gmaila rozwiązał problem z wykorzystaniem pamięci i poprawił wydajność. To samo możesz zrobić z aplikacją internetową.