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

John McCutchan
John McCutchan
Loreena Lee
Loreena Lee

Wstęp

JavaScript umożliwia automatyczne zarządzanie pamięcią za pomocą funkcji czyszczenia pamięci, ale nie zastępuje efektywnego zarządzania pamięcią w aplikacjach. Aplikacje JavaScript borykają się z tymi samymi problemami z pamięcią co aplikacje natywne (np. wyciekami pamięci i zrostem), ale muszą też radzić sobie z przerwami w odśnieżaniu. Duże aplikacje, takie jak Gmail, napotykają te same problemy, które są widoczne w mniejszych aplikacjach. Czytaj dalej, aby dowiedzieć się, jak zespół Gmaila wykorzystał Narzędzia deweloperskie w Chrome, aby zidentyfikować, wyizolować i rozwiązać problemy z pamięcią.

Sesja Google I/O 2013

Prezentowany materiał został zaprezentowany na konferencji Google I/O 2013. Obejrzyj ten film:

Gmail, mamy problem...

Zespół Gmaila napotkał poważny problem. Coraz częściej dochodziło do wypowiedzi, w których karty Gmaila zużywały wiele gigabajtów pamięci na laptopach i komputerach z ograniczonymi zasobami. Dopiero do tego doszło do całkowitego wyłączenia przeglądarki. Historie przypiętych procesorów w 100%, niedziałających aplikacji i smutnych kart w Chrome („Ciemność, widzę ciemność”). Zespół nie wiedział, jak zacząć diagnozować problem, a co dopiero go rozwiązać. Nie mieli pojęcia, na ile powszechny jest problem, a dostępne narzędzia nie były dostosowane do dużych aplikacji. Zespół połączył siły z zespołami Chrome i wspólnie opracował nowe metody rozwiązywania problemów z pamięcią, ulepszył istniejące narzędzia i umożliwił zbieranie danych dotyczących pamięci. Jednak zanim przejdziemy do tych narzędzi, przypomnijmy podstawy zarządzania pamięcią JavaScriptu.

Podstawy zarządzania pamięcią

Aby skutecznie zarządzać pamięcią w języku JavaScript, musisz poznać podstawy. W tej sekcji zostaną omówione typy podstawowe, wykres obiektów oraz ogólne definicje nadwyżki pamięci i wycieku pamięci w JavaScript. Pamięć w JavaScripcie można wyobrazić sobie w postaci grafu, dlatego teoria grafu odgrywa rolę w zarządzaniu pamięcią JavaScriptu i programie profilowania Heap Profiler.

Typy podstawowe

JavaScript ma 3 typy podstawowe:

  1. Liczba (np. 4, 3.14159)
  2. Wartość logiczna (prawda lub fałsz)
  3. Tekst („Hello World”)

Te typy podstawowe nie mogą się odwoływać do żadnych innych wartości. Na wykresie obiektów te wartości są zawsze węzłami liśćmi lub węzłami kończącymi się, co oznacza, że nigdy nie mają krawędzi wychodzącej.

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

A co z tablicami?

Tablica w JavaScripcie jest w rzeczywistości obiektem, który ma klucze liczbowe. Jest to uproszczenie, ponieważ środowiska wykonawcze JavaScriptu optymalizują obiekty przypominające tablice i przedstawiają je jako tablice.

Terminologia

  1. Wartość – instancja typu podstawowego, 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.

Wykres obiektów

Wszystkie wartości w JavaScripcie są częścią grafu obiektów. Wykres zaczyna się od pierwiastków (np. obiektu window). Nie masz wpływu na zarządzanie czasem przechowywania głównych certyfikatów GC, ponieważ są one tworzone przez przeglądarkę i niszczone podczas wyładowywania strony. Zmienne globalne są właściwościami w oknie.

Wykres obiektowy

Kiedy wartość staje się śmieciami?

Wartość staje się nieczytelna, gdy nie ma ścieżki od katalogu głównego do wartości. Innymi słowy, począwszy od korzeni, a następnie po dokładnym przeszukaniu wszystkich aktywnych właściwości i zmiennych obiektów w ramce stosu, nie uda się osiągnąć wartości, która staje się śmieciem.

Wykres śmieciowego zanieczyszczenia

Co to jest wyciek pamięci w JavaScript?

Do wycieku pamięci w JavaScripcie dochodzi najczęściej wtedy, gdy istnieją węzły DOM, które są nieosiągalne z drzewa DOM strony, ale nadal odwołują się do nich obiekt JavaScript. W nowoczesnych przeglądarkach coraz trudniej jest przypadkowo wyciek danych, ale i tak jest to łatwiejsze, niż mogłoby się wydawać. Załóżmy, że dołączasz element do drzewa DOM w następujący sposób:

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

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, mimo że jest teraz odłączony od drzewa DOM strony.

Co to jest Bloat?

Strona jest zbyt duża, gdy wykorzystuje jej więcej pamięci, niż jest to konieczne dla uzyskania optymalnej szybkości działania. Pośrednie wycieki pamięci również powodują wzdęcia, ale nie jest to z założenia. Częstym źródłem nadmiarów pamięci jest pamięć podręczna aplikacji bez żadnych ograniczeń rozmiaru. Poza tym zawartość strony może być przepełniona danymi hosta, na przykład ładowanymi z obrazów pikselami.

Czym jest wywóz śmieci?

Czyszczenie pamięci to sposób odzyskiwania pamięci w języku JavaScript. To przeglądarka decyduje, kiedy ma to nastąpić. W trakcie kolekcji całe wykonanie skryptu na stronie jest zawieszane, a rzeczywiste wartości są wykrywane podczas przemierzania wykresu obiektów, zaczynając od głównych wartości GC. Wszystkie wartości, które nie są osiągalne, są klasyfikowane jako śmieci. Pamięć dla wartości czyszczenia pamięci jest odzyskiwana przez menedżera pamięci.

Szczegółowy opis kosza na śmieci V8

Aby lepiej zrozumieć, jak działa funkcja odśmiecania V8, przyjrzyjmy się jej dokładniej. V8 używa kolektora generatywnego. Pamięć jest podzielona na 2 pokolenia: młode i stare. Proces przydzielania i zbierania danych w ramach młodego pokolenia przebiega szybko i często. Alokacja i zbieranie w starszej generacji są wolniejsze i rzadsze.

Kolektor generacyjny

V8 wykorzystuje kolektor dwóch generacji. Wiek wartości jest zdefiniowany jako liczba bajtów przydzielonych od czasu jej przydziału. W praktyce wiek danej wartości jest często szacowany na podstawie liczby kolekcji młodych pokolenia, które przetrwały. Jeśli wartość jest wystarczająco stara, zostaje zachowana do starego pokolenia.

W praktyce nowo przypisane wartości nie są ważne przez długi czas. Badanie programów Smalltalk wykazało, że tylko 7% wartości przetrwa po młodym pokoleniu. Podobne badania dotyczące środowisk wykonawczych wykazały, że średnio 90%–70% nowo przydzielonych wartości nigdy nie trafia do starej generacji.

Młode pokolenie

Stos młodej generacji w V8 jest podzielony na 2 przestrzenie o nazwach „od i do”. Pamięć jest przydzielana z przestrzeni do miejsca docelowego. Przydzielanie przebiega bardzo szybko, dopóki miejsce nie zostanie zapełnione. W tym momencie pojawia się kolekcja młodego pokolenia. Młode pokolenia najpierw zamieniają je w kosmos, stare w kosmos (obecnie kosmiczne), a wszystkie aktywne wartości są kopiowane w kosmos lub przekazywane do starego pokolenia. Typowe zbieranie danych przez młode pokolenie trwa 10 milisekund (ms).

Należy pamiętać, że każdy przydział w aplikacji przybliża Cię do wykorzystania przestrzeni kosmicznej i powodowania wstrzymania GC. Twórcy gier powinni pamiętać o tym, że aby czas renderowania klatki wynosił 16 ms (co jest wymagane do uzyskania 60 klatek na sekundę), aplikacja nie może mieć przydziału, ponieważ jedna kolekcja młodego pokolenia pochłania większość czasu renderowania klatki.

Stos młodej generacji

Stara generacja

Sterta starej generacji w V8 do zbierania danych korzysta z algorytmu oznaczenie-kompakt. Przydziały w starej generacji występują wtedy, gdy wartość jest przekazywana z młodego pokolenia do starego. Za każdym razem, gdy dochodzi do powstania kolekcji starszej generacji, wykonywana jest również kolekcja młodych pokoleń. Twoja aplikacja zostanie wstrzymana na kilka sekund. W praktyce jest to akceptowalne, ponieważ kolekcje starszej generacji występują rzadko.

Podsumowanie GC wersji 8

Automatyczne zarządzanie pamięcią z funkcją czyszczenia pamięci doskonale zwiększa produktywność programistów, ale za każdym razem, gdy przydzielasz wartość, przybliżasz się do wstrzymania procesu odśmiecania pamięci. Wstrzymania czyszczenia pamięci mogą zepsuć działanie aplikacji przez wprowadzenie jank. Wiesz już, jak JavaScript zarządza pamięcią, więc możesz dokonać wyboru odpowiedniego dla swojej aplikacji.

Naprawianie Gmaila

W ciągu ostatniego roku w Narzędziach deweloperskich w Chrome pojawiło się wiele funkcji i poprawek błędów, dzięki czemu stają się one jeszcze bardziej wydajne niż kiedykolwiek wcześniej. Ponadto sama przeglądarka wprowadziła kluczową zmianę do interfejsu performance.memory API, co umożliwiło Gmailowi i innym aplikacjom zbieranie statystyk pamięci z pola. Wyposażone w te świetne narzędzia coś, co kiedyś wydawało się niemożliwe do wykonania, stało się ekscytującą grą w poszukiwaniu sprawców.

Narzędzia i techniki

Field Data i performance.memory API

Od Chrome 22 interfejs performance.memory API jest domyślnie włączony. W przypadku aplikacji, które działają od dawna, takich jak Gmail, dane od rzeczywistych użytkowników są bezcenne. Dzięki tym informacjom możemy odróżniać doświadczonych użytkowników – tych, którzy korzystają z Gmaila 8–16 godzin dziennie i otrzymują setki wiadomości dziennie – od bardziej przeciętnych użytkowników, którzy spędzają kilka minut dziennie na korzystaniu z Gmaila i odbierają kilkanaście wiadomości tygodniowo.

Ten interfejs API zwraca 3 rodzaje danych:

  1. jsHeapSizeLimit – ilość pamięci (w bajtach), do której jest ograniczony sterty JavaScriptu.
  2. totalJSHeapSize – ilość pamięci (w bajtach) przydzielona przez stertę JavaScriptu z uwzględnieniem wolnego miejsca.
  3. usedJSHeapSize – ilość używanej obecnie pamięci (w bajtach).

Pamiętaj, że interfejs API zwraca wartości pamięci dla całego procesu Chrome. Chociaż nie jest to tryb domyślny, w pewnych okolicznościach Chrome może otwierać wiele kart w ramach tego samego mechanizmu renderowania. Oznacza to, że wartości zwracane przez parametr performance.memory mogą obejmować nie tylko tę, w której znajduje się Twoja aplikacja, ale też inne karty przeglądarki.

Pomiar pamięci na dużą skalę

Gmail dostosował kod JavaScript do używania interfejsu performance.memory API do zbierania informacji o pamięci mniej więcej raz na 30 minut. Wielu użytkowników Gmaila nie korzysta z aplikacji przez kilka dni, więc zespół mógł śledzić wzrost ilości pamięci w czasie oraz ogólne statystyki dotyczące zużycia pamięci. W ciągu kilku dni od uruchomienia Gmaila w celu zebrania informacji o pamięci na podstawie losowej próbki użytkowników zespół miał wystarczającą ilość danych, aby określić, na ile poważne są problemy z pamięcią u przeciętnych użytkowników. Określają one wartość bazową i używają strumienia przychodzących danych do śledzenia postępów w realizacji celu, jakim jest zmniejszenie zużycia pamięci. Docelowo dane te będą również używane do wykrywania wszelkich regresji pamięci.

Oprócz śledzenia pomiary w terenie pozwalają też dokładnie określić korelację między wykorzystaniem pamięci a wydajnością aplikacji. Wbrew powszechnemu przekonaniu, że „więcej pamięci oznacza lepszą wydajność”, zespół Gmaila odkrył, że im większa ilość pamięci, tym dłuższe opóźnienia w działaniu Gmaila. Te informacje sprawiły, że byli oni bardziej niż kiedykolwiek bardziej zmotywowani do utrzymania pamięci.

Pomiar pamięci na dużą skalę

Identyfikowanie problemu z pamięcią na osi czasu Narzędzi deweloperskich

Pierwszym krokiem do rozwiązania każdego problemu z wydajnością jest udowodnienie, że problem istnieje, stworzenie powtarzalnego testu i wykonanie podstawowej oceny problemu. Bez programu, który da się odtworzyć, nie uda Ci się miarodajnie zmierzyć problemu. Bez pomiaru bazowego nie wiadomo, jak bardzo poprawiła się skuteczność.

Panel osi czasu w Narzędziach deweloperskich to idealne narzędzie do udowodnienia, że problem istnieje. Daje pełny przegląd czasu spędzonego podczas wczytywania aplikacji internetowej lub strony i korzystania z niej. Wszystkie zdarzenia – od wczytywania zasobów przez analizowanie kodu JavaScript, obliczanie stylów, zatrzymań odśmiecania pamięci i ponowne renderowanie, są przedstawione na osi czasu. Na potrzeby badania problemów z pamięcią panel Oś czasu zawiera też tryb pamięci, który śledzi łączną przydzieloną pamięć, liczbę węzłów DOM, liczbę obiektów okien i liczbę przydzielonych detektorów zdarzeń.

Udowodnienie, że problem istnieje

Zacznij od zidentyfikowania sekwencji działań, które mogą spowodować wyciek pamięci. Zacznij rejestrować oś czasu i wykonaj sekwencję działań. Użyj przycisku kosza na dole, aby wymusić wyczyszczenie całego miejsca na śmieci. Jeśli po kilku iteracjach widzisz wykres w kształcie zębata, oznacza to, że przypisujesz wiele krótko żyjących obiektów. Jeśli jednak sekwencja działań nie powinna skutkować przechowywaniem pamięci w pamięci, a liczba węzłów DOM nie spada do wartości bazowej, na której rozpoczęto, masz dobry powód, aby podejrzewać, że wystąpił wyciek.

Wykres w kształcie zęba

Po potwierdzeniu, że problem istnieje, możesz uzyskać pomoc w identyfikacji jego źródła za pomocą narzędzia Heap Profiler w narzędziach deweloperskich.

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

Panel Profiler zawiera zarówno narzędzie profilujące CPU, jak i narzędzie profilujące stertę. Profilowanie sterty polega na robieniu zrzutu grafu obiektów. Przed utworzeniem zrzutu śmieci są śmieciowane zarówno przez młode, jak i starsze pokolenia. Innymi słowy, zobaczysz tylko te wartości, które były aktywne podczas wykonywania zrzutu.

Program profilujący sterty ma zbyt dużo funkcji, aby można było je opisać w tym artykule. Szczegółową dokumentację znajdziesz na stronie dla programistów Chrome. W tym miejscu skupimy się na narzędziu do profilowania alokacji sterty.

Używanie programu profilującego alokacji sterty

Program profilujący alokacji sterty łączy szczegółowe informacje o migawce narzędzia Profile Heap Profiler z przyrostowym aktualizowaniem i śledzeniem panelu Oś czasu. Otwórz panel Profile, uruchom profil rejestruj alokacje sterty, wykonaj sekwencję działań, a następnie zatrzymaj rejestrowanie na potrzeby analizy. Narzędzie do profilowania alokacji okresowo tworzy zrzuty stosu podczas nagrywania (coraz częściej co 50 ms) i tworzy ostatnią migawkę na koniec nagrania.

Program profilujący alokacji sterty

Słupki u góry wskazują, kiedy na stercie znajdują się nowe obiekty. Wysokość każdego słupka odpowiada rozmiarowi ostatnio przydzielonych obiektów, a kolory słupków wskazują, czy te obiekty są nadal aktywne w ostatecznym podglądzie sterty: niebieskie paski wskazują obiekty, które są wciąż aktywne na końcu osi czasu, szare paski wskazują obiekty, które zostały przydzielone na osi czasu, ale zostały już zebrane.

W przykładzie powyżej działanie zostało wykonane 10 razy. Przykładowy program zapisuje 5 obiektów w pamięci podręcznej, dlatego oczekiwanych jest 5 ostatnich niebieskich pasków. Niebieski pasek znajdujący się najbardziej po lewej stronie wskazuje jednak na potencjalny problem. Następnie możesz użyć suwaków na powyższej osi czasu, aby powiększyć dany zrzut i wyświetlić obiekty, które zostały ostatnio przydzielone w tym momencie. Kliknięcie konkretnego obiektu na stercie spowoduje wyświetlenie jego drzewa przechowywania w dolnej części zrzutu stosu. Zbadanie ścieżki przechowywania obiektu powinno dać Ci wystarczającą ilość informacji, aby zrozumieć, dlaczego obiekt nie został zebrany. Możesz też wprowadzić niezbędne zmiany w kodzie, aby usunąć zbędne odwołanie.

Rozwiązywanie kryzysu pamięci w Gmailu

Korzystając z omówionych powyżej narzędzi i technik, zespół Gmaila był w stanie zidentyfikować kilka kategorii błędów: nieograniczone pamięci podręczne, nieskończenie rosnące zbiory wywołań zwrotnych czekających na coś, co nigdy się nie wydarzy, oraz detektory zdarzeń przypadkowo zachowujące swoje cele. Dzięki rozwiązaniu tych problemów ogólne wykorzystanie pamięci przez Gmaila zostało znacznie zmniejszone. Użytkownicy z 99% procent używali o 80% mniej pamięci niż wcześniej, a zużycie pamięci przez medianę użytkowników spadło o prawie 50%.

Wykorzystanie pamięci w Gmailu

Gmail zużywał mniej pamięci, więc czas oczekiwania na wstrzymanie GC był krótszy, co poprawiło ogólne wrażenia użytkownika.

Zespół Gmaila gromadził też statystyki wykorzystania pamięci, dzięki czemu udało mu się wykryć regresje w zakresie czyszczenia pamięci w Chrome. W szczególności wykryto 2 błędy fragmentacji, gdy dane pamięci Gmaila zaczęły powodować znaczny wzrost rozbieżności między łączną ilością pamięci przydzielonej a pamięcią aktywną.

Wezwanie do działania

Zadaj sobie te pytania:

  1. Ile pamięci wykorzystuje moja aplikacja? Możliwe, że używasz zbyt dużo pamięci, co wbrew powszechnym przekonaniom ma negatywny wpływ na ogólną wydajność aplikacji. Ciężko stwierdzić, jaka jest właściwa liczba, ale upewnij się, że dodatkowe buforowanie, z którego korzysta Twoja strona, ma wymierny wpływ na jej wydajność.
  2. Czy moja strona jest wolna od wycieków? Jeśli na stronie występują wycieki pamięci, może to wpłynąć nie tylko na jej wydajność, ale także na inne karty. Użyj narzędzia do śledzenia obiektów, aby wykryć przecieki.
  3. Jak często moja strona jest generowana? Każdą pauzę GC możesz zobaczyć w panelu osi czasu w Narzędziach deweloperskich w Chrome. Jeśli Twoja strona często generuje GC, prawdopodobnie za często przydzielasz jej zawartość, zaśmiecając pamięć młodego pokolenia.

Podsumowanie

Zaczęliśmy w kryzysie. Omówiliśmy podstawowe podstawowe informacje o zarządzaniu pamięcią w języku JavaScript i w wersji 8. Wiesz już, jak korzystać z tych narzędzi, w tym z nowej funkcji śledzenia obiektów dostępnej w najnowszych wersjach Chrome. Dzięki tej wiedzy zespół Gmaila rozwiązał problem z wykorzystaniem pamięci i odnotował wzrost wydajności. To samo możesz zrobić ze swoimi aplikacjami internetowymi.