Effektive Speicherverwaltung im Gmail-Maßstab

John McCutchan
John McCutchan
Loreena Lee
Loreena Lee

Einleitung

JavaScript nutzt zwar die automatische Speicherverwaltung für die automatische Speicherbereinigung, ist jedoch kein Ersatz für eine effektive Speicherverwaltung in Anwendungen. JavaScript-Anwendungen leiden mit denselben speicherbezogenen Problemen wie native Anwendungen, z. B. Speicherlecks und Bloat, müssen aber auch mit Pausen bei der automatischen Speicherbereinigung umgehen. Bei großen Anwendungen wie Gmail treten die gleichen Probleme auf, denen auch Ihre kleineren Anwendungen gegenüberstehen. Lesen Sie weiter, um zu erfahren, wie das Gmail-Team die Chrome-Entwicklertools eingesetzt hat, um Arbeitsspeicherprobleme zu identifizieren, zu isolieren und zu beheben.

Google I/O Session 2013

Dieses Material haben wir auf der Google I/O 2013 vorgestellt. Sehen Sie sich das folgende Video an:

Gmail, wir haben ein Problem...

Das Gmail-Team stand vor einem ernsten Problem. Immer häufiger hörten wir immer wieder Anekdoten von Gmail-Tabs, die auf ressourcenbeschränkten Laptops und Desktop-Computern mehrere Gigabyte an Arbeitsspeicher verbrauchten. Oftmals kam es zu dem Schluss, dass der Browser komplett abgeschaltet wurde. Geschichten von CPUs, die zu 100 % angepinnt sind, nicht reagierende Apps und traurige Chrome-Tabs („Er ist tot, Jim“). Das Team war sich nicht sicher, wie es überhaupt mit der Diagnose des Problems beginnen sollte, geschweige denn es zu beheben. Sie hatten keine Ahnung, wie groß das Problem war, und die verfügbaren Tools ließen sich nicht für große Anwendungen skalieren. Gemeinsam mit den Chrome-Teams entwickelte das Team neue Techniken zur Erkennung von Speicherproblemen, verbesserte vorhandene Tools und ermöglichte die Erhebung von Speicherdaten aus der Praxis. Bevor wir jedoch zu den Tools kommen, sehen wir uns die Grundlagen der JavaScript-Arbeitsspeicherverwaltung an.

Grundlagen der Arbeitsspeicherverwaltung

Bevor Sie den Arbeitsspeicher in JavaScript effektiv verwalten können, müssen Sie die Grundlagen verstehen. In diesem Abschnitt werden primitive Typen und die Objektgrafik behandelt. Außerdem werden Definitionen für Speicher-Bloat im Allgemeinen und Speicherlecks in JavaScript gegeben. Der Arbeitsspeicher in JavaScript kann als Diagramm dargestellt werden. Daher spielt die Grafiktheorie eine Rolle bei der JavaScript-Arbeitsspeicherverwaltung und im Heap Profiler.

Einfache Typen

JavaScript hat drei primitive Typen:

  1. Zahl (z.B. 4, 3.14159)
  2. Boolesch (wahr oder falsch)
  3. String („Hello World“)

Diese primitiven Typen können nicht auf andere Werte verweisen. Im Objektdiagramm sind diese Werte immer Blatt- oder Endknoten, d. h. sie haben nie eine ausgehende Edge.

Es gibt nur einen Containertyp: das Objekt. In JavaScript ist das Objekt ein assoziatives Array. Ein nicht leeres Objekt ist ein innerer Knoten mit ausgehenden Kanten zu anderen Werten (Knoten).

Was ist mit Arrays?

Ein Array in JavaScript ist eigentlich ein Objekt mit numerischen Schlüsseln. Dies ist eine Vereinfachung, da JavaScript-Laufzeiten Array-ähnliche Objekte optimieren und sie im Hintergrund als Arrays darstellen.

Terminologie

  1. Wert: Eine Instanz eines primitiven Typs, eines Objekts, eines Arrays usw.
  2. Variable: Ein Name, der auf einen Wert verweist.
  3. Eigenschaft: Ein Name in einem Objekt, der auf einen Wert verweist.

Objektgrafik

Alle Werte in JavaScript sind Teil der Objektgrafik. Die Grafik beginnt mit Wurzeln, z. B. dem window-Objekt. Die Verwaltung der Lebensdauer von GC-Roots liegt nicht in Ihrer Kontrolle, da sie vom Browser erstellt und beim Entfernen der Seite gelöscht werden. Globale Variablen sind eigentlich Eigenschaften des Fensters.

Objektgrafik

Wann wird aus einem Wert Müll?

Ein Wert wird automatisch gelöscht, wenn es keinen Pfad vom Stamm zum Wert gibt. Mit anderen Worten: Wenn Sie von der Wurzel aus alle Objekteigenschaften und Variablen, die im Stapelframe aktiv sind, vollständig durchsucht werden, kann kein Wert erreicht werden. Er wurde als Speicherinhalt bezeichnet.

Speicherdiagramm

Was ist ein Speicherleck in JavaScript?

Ein Speicherleck in JavaScript tritt am häufigsten auf, wenn DOM-Knoten vorhanden sind, die vom DOM-Baum der Seite nicht erreichbar sind, aber dennoch von einem JavaScript-Objekt referenziert werden. Obwohl es mit modernen Browsern immer schwieriger wird, versehentlich Datenlecks zu erstellen, ist es immer noch einfacher, als man denkt. Angenommen, Sie hängen ein Element wie folgt an den DOM-Baum an:

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

Später entfernen Sie das Element aus der Anzeigeliste:

displayList.removeAllChildren();

Solange email vorhanden ist, wird das DOM-Element, auf das die Nachricht verweist, nicht entfernt, auch wenn es jetzt vom DOM-Baum der Seite getrennt ist.

Was ist Bloat?

Wenn Sie mehr Arbeitsspeicher nutzen, als für eine optimale Seitengeschwindigkeit erforderlich ist, wird die Seite aufgebläht. Indirekt verursachen Speicherlecks ebenfalls Bloß. Das ist jedoch nicht beabsichtigt. Ein Anwendungscache ohne Größenbeschränkung ist eine häufige Ursache für Speicheraufgebläht. Außerdem kann Ihre Seite durch Hostdaten wie Pixeldaten, die aus Bildern geladen werden, aufgebläht werden.

Was ist die Müllabfuhr?

Bei der automatischen Speicherbereinigung wird Arbeitsspeicher in JavaScript freigegeben. Der Browser entscheidet, wann dies geschieht. Während einer Erfassung wird die gesamte Skriptausführung auf Ihrer Seite ausgesetzt, während bei einem Durchlauf der Objektgrafik, der an den GC-Stammwerten beginnt, Livewerte erkannt werden. Alle Werte, die nicht erreichbar sind, werden als automatische Speicherbereinigung klassifiziert. Der Arbeitsspeicher für automatische Werte wird vom Arbeitsspeichermanager freigegeben.

V8 Garbage Collector im Detail

Damit Sie besser verstehen, wie die automatische Speicherbereinigung abläuft, sehen wir uns die automatische Speicherbereinigung V8 im Detail an. V8 nutzt einen Generations-Collector. Das Gedächtnis wird in zwei Generationen unterteilt: die junge und die alte. Die junge Generation nimmt die Aufteilung und Erfassung schnell und häufig vor. Die Zuweisung und Erfassung innerhalb der alten Generation ist langsamer und erfolgt weniger häufig.

Erzeuger von Generationen

V8 verwendet einen Collector mit zwei Generationen. Das Alter eines Werts ist definiert als die Anzahl der Byte, die seit seiner Zuweisung zugewiesen wurden. In der Praxis wird das Alter eines Werts häufig durch die Anzahl der Sammlungen junger Generationen nahegelegt, die er überlebte. Wenn ein Wert alt genug ist, wird er in der alten Generation gespeichert.

In der Praxis haben neu zugewiesene Werte keine lange Lebensdauer. Eine Studie zu Smalltalk-Programmen ergab, dass nur 7% der Werte nach einer Sammlung einer jungen Generation überleben. Ähnliche Studien zu Laufzeiten ergaben, dass im Durchschnitt zwischen 90% und 70% der neu zugewiesenen Werte nie in der alten Generation gespeichert werden.

Junge Generation

Der Heap der jungen Generation in V8 ist in zwei Bereiche unterteilt, die von und nach benannt sind. Arbeitsspeicher wird vom dem Bereich zugewiesen. Die Zuweisung geht sehr schnell, bis das Weltall voll ist. Dann wird eine Sammlung einer jungen Generation ausgelöst. Die Sammlung der jungen Generation tauscht zuerst den Übergang vom und zum Weltraum aus, das Alte in den Weltraum (jetzt das aus dem Weltraum) wird gescannt und alle Live-Werte werden in den Weltraum kopiert oder in die alte Generation gespeichert. Eine typische Sammlung einer jungen Generation nimmt eine Größenordnung von 10 Millisekunden (ms) ein.

Natürlich sollten Sie verstehen, dass jede Zuweisung, die Ihre Anwendung vornimmt, dazu führt, dass Sie den Weltraum erschöpfen und eine GC-Pause verursachen. Hinweis für Spieleentwickler: Um eine Frame Time von 16 ms zu gewährleisten (erforderlich für 60 Bilder pro Sekunde), muss Ihre App keine Zuweisungen vornehmen, da eine einzelne Sammlung junger Generation die meiste Frame-Time verbraucht.

Die junge Generation

Alte Generation

Der alte Heap der Generation in V8 verwendet für die Erfassung einen mark-compact-Algorithmus. Zuweisungen alter Generationen erfolgen immer dann, wenn ein Wert von der jungen Generation bis zur alten Generation gespeichert wird. Immer, wenn eine Sammlung alter Generationen auftritt, wird auch eine Sammlung der jungen Generationen erstellt. Ihre Anwendung wird im Abstand von wenigen Sekunden pausiert. In der Praxis ist dies akzeptabel, da Sammlungen alter Generationen selten sind.

V8 GC-Zusammenfassung

Die automatische Speicherverwaltung mit automatischer Speicherbereinigung ist hervorragend für die Produktivität von Entwicklern geeignet, aber jedes Mal, wenn Sie einen Wert zuweisen, kommt einer Pause der automatischen Speicherbereinigung immer näher. Pausen bei der automatischen Speicherbereinigung können Ihre Anwendung durch die Einführung von Verzögerungen beeinträchtigen. Da Sie nun wissen, wie JavaScript den Arbeitsspeicher verwaltet, können Sie die richtigen Entscheidungen für Ihre Anwendung treffen.

Probleme mit Gmail beheben

Im letzten Jahr wurden die Chrome-Entwicklertools mit zahlreichen Funktionen und Fehlerkorrekturen eingeführt. Dadurch sind sie noch leistungsstärker als je zuvor. Im Browser selbst wurde außerdem eine wichtige Änderung an der performance.memory API vorgenommen. Dadurch konnten Gmail und andere Anwendungen Speicherstatistiken aus dem Feld erfassen. Ausgerüstet mit diesen tollen Tools schien eine Aufgabe wie eine unmögliche Aufgabe schnell ein spannendes Spiel zur Aufspürung von Tätern zu werden.

Tools und Techniken

Field Data und performance.memory API

Ab Chrome 22 ist die performance.memory API standardmäßig aktiviert. Für lang andauernde Anwendungen wie Gmail sind Daten von echten Nutzern von unschätzbarem Wert. Anhand dieser Informationen können wir zwischen Powerusern, die acht bis 16 Stunden pro Tag mit Gmail verbringen und Hunderte von Nachrichten pro Tag empfangen, von größeren durchschnittlichen Nutzern unterscheiden, die nur wenige Minuten pro Tag in Gmail verbringen und etwa ein Dutzend Nachrichten pro Woche erhalten.

Diese API gibt drei Daten zurück:

  1. jsHeapSizeLimit: die Größe des Arbeitsspeichers (in Byte), auf den der JavaScript-Heap begrenzt ist.
  2. totalJSHeapSize - die Größe des vom JavaScript-Heap zugewiesenen Arbeitsspeichers (in Byte), einschließlich des kostenlosen Speichers.
  3. useJSHeapSize - die aktuell genutzte Speichermenge (in Byte).

Beachten Sie, dass die API Speicherwerte für den gesamten Chrome-Prozess zurückgibt. Obwohl dies nicht der Standardmodus ist, kann Chrome unter bestimmten Umständen mehrere Tabs im selben Renderer-Prozess öffnen. Das bedeutet, dass die von „performance.memory“ zurückgegebenen Werte möglicherweise den Arbeitsspeicherbedarf anderer Browsertabs zusätzlich zum Tab mit Ihrer App enthalten.

Arbeitsspeicher in großem Maßstab messen

Gmail nutzte das JavaScript so, dass ungefähr alle 30 Minuten Speicherinformationen über die performance.memory API erhoben wurden. Da viele Gmail-Nutzer die App tagelang verlassen, konnte das Team das Wachstum des Arbeitsspeichers im Laufe der Zeit und die allgemeine Arbeitsspeichernutzungsstatistiken verfolgen. Innerhalb weniger Tage, nachdem das Team Gmail für das Erfassen von Speicherinformationen aus einer Zufallsstichprobe von Nutzern eingerichtet hatte, hatte das Team genug Daten, um nachzuvollziehen, wie groß die Arbeitsspeicherprobleme bei den durchschnittlichen Nutzern waren. Das Team legte eine Basislinie fest und verfolgte anhand des Stroms eingehender Daten den Fortschritt in Richtung der Reduzierung des Arbeitsspeicherverbrauchs. Letztendlich werden diese Daten auch dazu verwendet, etwaige Speicherabfälle zu erkennen.

Neben dem Tracking ermöglichen die Feldmessungen auch einen genauen Einblick in die Korrelation zwischen dem Speicherbedarf und der Anwendungsleistung. Entgegen der weitverbreiteten Annahme, dass „mehr Arbeitsspeicher zu einer besseren Leistung führt“ stellte das Gmail-Team fest, dass die Latenzen bei gängigen Gmail-Aktionen umso größer sind, je größer der Speicherbedarf ist. Dank dieser Enthüllung waren sie motivierter, ihren Gedächtniskonsum zu kontrollieren.

Arbeitsspeicher in großem Maßstab messen

Probleme mit dem Arbeitsspeicher über die Zeitachse der Entwicklertools erkennen

Der erste Schritt zur Lösung eines Leistungsproblems besteht darin, nachzuweisen, dass das Problem existiert, einen reproduzierbaren Test zu erstellen und eine Basismessung für das Problem durchzuführen. Ohne ein reproduzierbares Programm können Sie das Problem nicht zuverlässig messen. Ohne Referenzmessung wissen Sie nicht, inwieweit sich die Leistung verbessert hat.

Das Steuerfeld „Zeitachse“ der Entwicklertools eignet sich hervorragend, um zu belegen, dass das Problem existiert. Sie bietet einen vollständigen Überblick darüber, wo beim Laden Ihrer Webanwendung oder -seite Zeit aufgewendet wird. Alle Ereignisse – vom Laden von Ressourcen bis zum Parsen von JavaScript-Code, Berechnen von Stilen, Pausen bei der automatischen Speicherbereinigung und Neuerstellung der Darstellung – werden auf einer Zeitachse dargestellt. Zur Untersuchung von Arbeitsspeicherproblemen enthält das Steuerfeld „Zeitachse“ auch einen Arbeitsspeichermodus, in dem der insgesamt zugewiesene Arbeitsspeicher, die Anzahl der DOM-Knoten, die Anzahl der Fensterobjekte und die Anzahl der zugewiesenen Ereignis-Listener erfasst werden.

Nachweis, dass ein Problem vorliegt

Identifizieren Sie zuerst eine Abfolge von Aktionen, die Ihrer Meinung nach zu Speicherverlusten führen. Beginnen Sie mit der Aufzeichnung der Zeitachse und führen Sie die Abfolge der Aktionen aus. Verwenden Sie die Papierkorbsymbole unten, um eine vollständige automatische Speicherbereinigung zu erzwingen. Wenn Sie nach einigen Iterationen eine Sägezahnförmig-Grafik sehen, haben Sie viele kurzlebige Objekte zugeordnet. Wenn die Abfolge der Aktionen jedoch nicht zu einem beibehaltenen Arbeitsspeicher führen wird und die Anzahl der DOM-Knoten nicht auf den Ausgangswert sinkt, bei dem Sie begonnen haben, haben Sie guten Grund zu der Annahme, dass es sich um ein Speicherleck handelt.

Sägezahnförmiges Diagramm

Sobald Sie sich vergewissert haben, dass das Problem vorliegt, können Sie die Ursache des Problems über den Heap-Profiler der Entwicklertools ermitteln.

Mit dem Heap-Profiler der Entwicklertools Speicherlecks finden

Das Profiler-Steuerfeld enthält sowohl einen CPU-Profiler als auch einen Heap-Profiler. Bei der Heap-Profilerstellung wird ein Snapshot des Objektdiagramms erstellt. Bevor ein Snapshot erstellt wird, werden sowohl die jungen als auch die alten Generationen bereinigt. Sie sehen also nur Werte, die zum Zeitpunkt der Erstellung des Snapshots aktiv waren.

Der Heap-Profiler enthält zu viele Funktionen, die in diesem Artikel nicht ausreichend behandelt werden. Eine ausführliche Dokumentation finden Sie auf der Website für Chrome-Entwickler. Wir konzentrieren uns hier auf den Profiler für die Heap-Zuweisung.

Heap-Zuweisungs-Profiler verwenden

Der Heap-Zuweisungs-Profiler kombiniert die detaillierten Snapshot-Informationen des Heap-Profilers mit der inkrementellen Aktualisierung und dem Tracking des Steuerfelds „Zeitachse“. Öffnen Sie den Bereich „Profile“, starten Sie ein Profil für Heap-Zuweisungen aufzeichnen, führen Sie eine Reihe von Aktionen aus und beenden Sie dann die Aufzeichnung zur Analyse. Der Zuweisungs-Profiler erstellt während der Aufzeichnung regelmäßig Heap-Snapshots (beispielsweise alle 50 ms) und am Ende der Aufzeichnung einen abschließenden Snapshot.

Heap-Zuweisungs-Profiler

Die Balken oben geben an, wann neue Objekte im Heap gefunden werden. Die Höhe der einzelnen Balken entspricht der Größe der kürzlich zugewiesenen Objekte. Die Farbe der Balken gibt an, ob diese Objekte im endgültigen Heap-Snapshot noch aktiv sind: Blaue Balken kennzeichnen Objekte, die am Ende der Zeitachse noch aktiv sind, graue Balken kennzeichnen Objekte, die während der Zeitachse zugewiesen wurden, aber inzwischen automatisch bereinigt wurden.

Im Beispiel oben wurde eine Aktion zehnmal ausgeführt. Das Beispielprogramm speichert fünf Objekte im Cache, sodass die letzten fünf blauen Balken zu erwarten sind. Aber der blaue Balken ganz links deutet auf ein potenzielles Problem hin. Anschließend können Sie mithilfe der Schieberegler auf der Zeitachse oben den jeweiligen Snapshot heranzoomen und die Objekte sehen, die zu diesem Zeitpunkt kürzlich zugewiesen wurden. Wenn Sie auf ein bestimmtes Objekt im Heap klicken, wird dessen beibehaltener Baum im unteren Teil des Heap-Snapshots angezeigt. Die Untersuchung des Pfades für die Aufbewahrung des Objekts sollte Ihnen genügend Informationen liefern, aus denen hervorgeht, warum das Objekt nicht erfasst wurde. Sie können dann die erforderlichen Codeänderungen vornehmen, um den unnötigen Verweis zu entfernen.

Die Gedächtniskrise von Gmail lösen

Mithilfe der oben beschriebenen Tools und Techniken konnte das Gmail-Team einige Fehlerkategorien identifizieren: unbegrenzte Caches, unendlich wachsende Reihen von Callbacks, die darauf warten, dass etwas passiert, aber nie passiert, und Event-Listener haben unbeabsichtigt ihre Ziele beibehalten. Durch die Behebung dieser Probleme konnte die Arbeitsspeichernutzung von Gmail insgesamt drastisch reduziert werden. Die Nutzer in den 99% der Nutzer haben 80% weniger Arbeitsspeicher als zuvor verwendet und der Arbeitsspeicherverbrauch der durchschnittlichen Nutzer ging um fast 50 % zurück.

Gmail-Arbeitsspeichernutzung

Da in Gmail weniger Arbeitsspeicher verwendet wurde, wurde die Pausenlatenz von GC reduziert und damit die Nutzerfreundlichkeit insgesamt verbessert.

Wichtig ist auch, dass das Gmail-Team beim Erfassen von Statistiken zur Arbeitsspeichernutzung Probleme bei der automatischen Speicherbereinigung in Chrome aufdecken konnte. Insbesondere wurden zwei Fragmentierungsfehler entdeckt, als in den Speicherdaten von Gmail ein deutlicher Anstieg der Lücke zwischen dem insgesamt zugewiesenen Arbeitsspeicher und dem Live-Speicher zu verzeichnen war.

Call-to-Action

Stellen Sie sich folgende Fragen:

  1. Wie viel Speicher nutzt meine App? Es ist möglich, dass Sie zu viel Speicher verwenden, was entgegen der allgemeinen Auffassung sich negativ auf die Gesamtleistung der Anwendung auswirkt. Die richtige Anzahl lässt sich nur schwer ermitteln. Achten Sie jedoch darauf, dass sich zusätzliches Caching auf Ihrer Seite messbar auf die Leistung auswirkt.
  2. Gibt es keine Lecks auf meiner Seite? Speicherlecks auf Ihrer Seite können sich nicht nur auf deren Leistung auswirken, sondern auch auf andere Tabs. Verwenden Sie den Objekt-Tracker, um Lecks einzugrenzen.
  3. Wie oft wird die automatische Speicherbereinigung der Seite genutzt? Sie können eventuelle Pausen von GCs in den Chrome-Entwicklertools im Bereich „Zeitachse“ sehen. Wenn Ihre Seite häufig GCing aufweist, ist die Wahrscheinlichkeit groß, dass Sie zu oft zu viele Aufgaben zuweisen, wodurch das Gedächtnis der jungen Generation verloren geht.

Fazit

Wir begannen in einer Krise. Wir haben die grundlegenden Grundlagen der Speicherverwaltung insbesondere in JavaScript und V8 behandelt. Jetzt wissen Sie, wie Sie die Tools verwenden, einschließlich des neuen Objekt-Trackers, der in den neuesten Builds von Chrome verfügbar ist. Mit diesem Wissen konnte das Gmail-Team das Problem mit der Arbeitsspeichernutzung lösen und die Leistung verbessern. Dasselbe können Sie auch mit Ihren Web-Apps tun.