Bekämpfung von Speicherlecks in Webanwendungen

Als wir von der Entwicklung von Websites, deren Seiten auf dem Server erstellt werden, zur Erstellung von einseitigen Webanwendungen übergingen, die auf dem Client gerendert werden, haben wir bestimmte Spielregeln übernommen. Eine davon ist der genaue Umgang mit Ressourcen auf dem Gerät des Benutzers. Dies bedeutet: Blockieren Sie nicht den Hauptstrom, drehen Sie den Laptop-Lüfter nicht und legen Sie den Akku des Telefons nicht ein. Wir tauschten eine Verbesserung der Interaktivität von Webprojekten und die Tatsache aus, dass ihr Verhalten eher dem Verhalten gewöhnlicher Anwendungen ähnelte, einer neuen Klasse von Problemen, die es in der Welt des Server-Renderings nicht gab.



Ein solches Problem sind Speicherlecks. Eine schlecht gestaltete einseitige Anwendung kann leicht Megabyte oder sogar Gigabyte Speicher verschlingen. Es kann immer mehr Ressourcen beanspruchen, selbst wenn es sich ruhig auf der Registerkarte "Hintergrund" befindet. Die Seite einer solchen Anwendung kann nach der Erfassung einer exorbitanten Menge an Ressourcen stark "langsamer" werden. Außerdem kann der Browser den Tab einfach herunterfahren und dem Benutzer mitteilen: "Es ist ein Fehler aufgetreten."


Etwas ist schief gelaufen

Natürlich können Sites, die auf dem Server gerendert werden, auch unter einem Speicherverlustproblem leiden. Aber hier geht es um Serverspeicher. Gleichzeitig ist es sehr unwahrscheinlich, dass solche Anwendungen einen Speicherverlust auf dem Client verursachen, da der Browser den Speicher nach jedem Benutzerübergang zwischen den Seiten löscht.

Das Thema Speicherlecks wird in Webentwicklungspublikationen nicht gut behandelt. Trotzdem bin ich mir fast sicher, dass die meisten nicht trivialen Einzelseitenanwendungen unter Speicherlecks leiden - es sei denn, die Teams, die sich mit ihnen befassen, verfügen über zuverlässige Tools, um dieses Problem zu erkennen und zu beheben. Der Punkt hier ist, dass es in JavaScript extrem einfach ist, eine bestimmte Menge an Speicher zufällig zuzuweisen und dann einfach zu vergessen, diesen Speicher freizugeben.

Der Autor des Artikels, dessen Übersetzung wir heute veröffentlichen, wird den Lesern seine Erfahrungen bei der Bekämpfung von Speicherlecks in Webanwendungen mitteilen und Beispiele für deren effektive Erkennung nennen.

Warum ist so wenig darüber geschrieben?


Zunächst möchte ich darüber sprechen, warum so wenig über Speicherlecks geschrieben wird. Ich nehme an, hier finden Sie mehrere Gründe:

  • Fehlende Beschwerden von Benutzern: Die meisten Benutzer sind nicht damit beschäftigt, den Task-Manager beim Surfen im Internet genau zu überwachen. In der Regel treten beim Entwickler keine Beschwerden von Benutzern auf, bis der Speicherverlust so schwerwiegend ist, dass die Anwendung nicht mehr funktioniert oder die Anwendung verlangsamt wird.
  • : Chrome - , . .
  • : .
  • : «» . , , , , -.


Moderne Bibliotheken und Frameworks für die Entwicklung von Webanwendungen wie React, Vue und Svelte verwenden das Komponentenmodell der Anwendung. In diesem Modell ist der häufigste Weg, einen Speicherverlust zu verursachen, ungefähr so:

window.addEventListener('message', this.onMessage.bind(this));

Das ist alles. Dies ist alles, was benötigt wird, um ein Projekt mit einem Speicherverlust auszustatten. Rufen Sie dazu einfach die addEventListener- Methode eines globalen Objekts (wie windowoder <body>oder ähnliches) auf und vergessen Sie beim Aufheben der Bereitstellung der Komponente, den Ereignis-Listener mit der removeEventListener- Methode zu entfernen .

Die Folgen sind jedoch noch schlimmer, da das Leck der gesamten Komponente auftritt. Dies liegt an der Tatsache, dass die Methode this.onMessageangehängt ist this. Zusammen mit dieser Komponente tritt ein Leck ihrer untergeordneten Komponenten auf. Es ist sehr wahrscheinlich, dass alle dieser Komponente zugeordneten DOM-Knoten auslaufen. Infolgedessen kann die Situation sehr schnell außer Kontrolle geraten, was zu sehr schlimmen Konsequenzen führt.

So lösen Sie dieses Problem:

//   
this.onMessage = this.onMessage.bind(this);
window.addEventListener('message', this.onMessage);
 
//   
window.removeEventListener('message', this.onMessage);

Situationen, in denen Speicherlecks am häufigsten auftreten


Die Erfahrung zeigt, dass Speicherlecks am häufigsten auftreten, wenn die folgenden APIs verwendet werden:

  1. Methode addEventListener. Hier treten am häufigsten Speicherverluste auf. Um das Problem zu lösen, reicht es aus, zum richtigen Zeitpunkt anzurufen removeEventListener.
  2. setTimeout setInterval. , (, 30 ), , , , , clearTimeout clearInterval. , setTimeout, «» , , setInterval-. , setTimeout .
  3. API IntersectionObserver, ResizeObserver, MutationObserver . , , . . - , , , , , disconnect . , DOM , , -. -, . — <body>, document, header footer, .
  4. Promise-, , . , , — , . , , «» , . «» .then()-.
  5. Repositorys, die durch globale Objekte dargestellt werden. Wenn Sie den Status einer Anwendung mit Redux steuern , wird der Statusspeicher durch ein globales Objekt dargestellt. Wenn Sie mit einem solchen Speicher unachtsam umgehen, werden unnötige Daten nicht gelöscht, wodurch seine Größe ständig zunimmt.
  6. Unendliches DOM-Wachstum. Wenn die Seite endloses Scrollen ohne Virtualisierung implementiert , bedeutet dies, dass die Anzahl der DOM-Knoten auf dieser Seite unbegrenzt zunehmen kann.

Oben haben wir Situationen untersucht, in denen Speicherlecks am häufigsten auftreten, aber es gibt natürlich viele andere Fälle, die das für uns interessante Problem verursachen.

Identifizierung von Speicherlecks


Jetzt haben wir uns der Herausforderung zugewandt, Speicherlecks zu identifizieren. Zunächst denke ich nicht, dass eines der vorhandenen Tools dafür sehr gut geeignet ist. Ich habe die Firefox-Speicheranalysetools und die Tools von Edge und IE ausprobiert. Getestet sogar Windows Performance Analyzer. Die besten dieser Tools sind jedoch immer noch Chrome Developer Tools. Es stimmt, in diesen Werkzeugen gibt es viele "scharfe Ecken", die es wert sind, kennengelernt zu werden.

Unter den Tools, die der Chrome-Entwickler zur Verfügung stellt, interessiert uns am meisten der Profiler Heap snapshotauf der Registerkarte Memory, mit dem Sie Heap-Snapshots erstellen können. Es gibt andere Tools zum Analysieren von Speicher in Chrome, aber ich konnte keine besonderen Vorteile daraus ziehen, um Speicherlecks zu erkennen.


Mit dem Heap-Snapshot-Tool können Sie Snapshots des Speichers des Hauptstroms, der Web-Worker oder der Iframe-Elemente erstellen.

Wenn das Chrome-Tool-Fenster wie in der vorherigen Abbildung dargestellt aussieht, werden beim Klicken auf die SchaltflächeTake snapshotInformationen zu allen Objekten im Speicher der ausgewählten virtuellen Maschine erfasst JavaScript der untersuchten Seite. Dies umfasst Objekte, auf die verwiesen wirdwindow, Objekte, auf die durch die im Aufruf verwendeten Rückrufe verwiesen wirdsetInterval, usw. Eine Momentaufnahme des Gedächtnisses kann als „eingefrorener Moment“ der Arbeit der untersuchten Entität wahrgenommen werden, der Informationen über den gesamten von dieser Entität verwendeten Speicher darstellt.

Nachdem das Bild aufgenommen wurde, kommen wir zum nächsten Schritt, um Lecks zu finden. Es besteht darin, ein Szenario zu reproduzieren, in dem laut Entwickler ein Speicherverlust auftreten kann. Zum Beispiel öffnet und schließt es ein bestimmtes modales Fenster. Nachdem das ähnliche Fenster geschlossen wurde, wird erwartet, dass die Menge des zugewiesenen Speichers auf die Ebene zurückkehrt, die vor dem Öffnen des Fensters vorhanden war. Daher machen sie ein anderes Bild und vergleichen es dann mit dem zuvor aufgenommenen Bild. In der Tat ist der Vergleich von Bildern das wichtigste Merkmal, das uns interessiert Heap snapshot.


Wir machen den ersten Schnappschuss, dann ergreifen wir Maßnahmen, die einen Speicherverlust verursachen können, und dann machen wir einen weiteren Schnappschuss. Wenn es kein Leck gibt, ist die Größe des zugewiesenen Speichers gleich.

Richtig, diesHeap snapshotist alles andere als ein ideales Werkzeug. Es gibt einige wissenswerte Einschränkungen:

  1. Selbst wenn Sie auf die kleine Schaltfläche im Bedienfeld klicken, mit der die Speicherbereinigung Memorygestartet wird ( Collect garbage), müssen Sie möglicherweise mehrere aufeinanderfolgende Bilder aufnehmen, um sicherzustellen, dass der Speicher wirklich gelöscht wird. Ich habe normalerweise drei Schüsse. Hier lohnt es sich, sich auf die Gesamtgröße jedes Bildes zu konzentrieren - es sollte sich am Ende stabilisieren.
  2. -, -, iframe, , , . , JavaScript. — , , .
  3. «». .

Wenn Ihre Anwendung zu diesem Zeitpunkt recht komplex ist, stellen Sie möglicherweise beim Vergleichen von Schnappschüssen viele „undichte“ Objekte fest. Hier ist die Situation etwas kompliziert, da das, was mit einem Speicherverlust verwechselt werden kann, nicht immer der Fall ist. Vieles, was verdächtig ist, sind nur normale Prozesse für die Arbeit mit Objekten. Der von einigen Objekten belegte Speicher wird gelöscht, um andere Objekte in diesem Speicher abzulegen, etwas wird in den Cache geleert, und der entsprechende Speicher wird nicht sofort gelöscht, und so weiter.

Wir machen uns auf den Weg durch das Informationsrauschen


Ich fand heraus, dass der beste Weg, um das Rauschen von Informationen zu durchbrechen, darin besteht, die Aktionen zu wiederholen, die einen Speicherverlust verursachen sollen. Anstatt das modale Fenster nach der ersten Aufnahme nur einmal zu öffnen und zu schließen, kann dies beispielsweise sieben Mal erfolgen. Warum 7? Ja, wenn auch nur, weil 7 eine wahrnehmbare Primzahl ist. Dann müssen Sie eine zweite Aufnahme machen und im Vergleich zur ersten herausfinden, ob ein bestimmtes Objekt 7 Mal (oder 14 Mal oder 21 Mal) „durchgesickert“ ist.


Vergleichen Sie Heap-Snapshots. Bitte beachten Sie, dass wir Bild Nr. 3 mit Bild Nr. 6 vergleichen. Tatsache ist, dass ich drei Aufnahmen hintereinander gemacht habe, damit Chrome mehr Speicherbereinigungssitzungen hat. Beachten Sie außerdem, dass einige Objekte sieben Mal „durchgesickert“ sind.

Ein weiterer nützlicher Trick besteht darin, dass Sie zu Beginn der Studie, bevor Sie das erste Bild erstellen, den Vorgang einmal ausführen und dabei erwartungsgemäß ausführen. Speicherleck. Dies wird insbesondere empfohlen, wenn im Projekt die Codeaufteilung verwendet wird. In einem solchen Fall ist es sehr wahrscheinlich, dass bei der ersten Ausführung der verdächtigen Aktion die erforderlichen JavaScript-Module geladen werden, was sich auf die Menge des zugewiesenen Speichers auswirkt.

Jetzt haben Sie möglicherweise eine Frage, warum Sie besonders auf die Anzahl der Objekte und nicht auf die Gesamtspeichermenge achten sollten. Hier können wir sagen, dass wir intuitiv danach streben, die Menge an "undichtem" Speicher zu reduzieren. In diesem Zusammenhang könnten Sie denken, dass Sie die Gesamtmenge des verwendeten Speichers überwachen sollten. Aber dieser Ansatz passt aus einem wichtigen Grund nicht besonders gut zu uns.

Wenn etwas „leckt“, passiert es, weil Sie (nacherzählt von Joe Armstrong ) eine Banane brauchen, aber am Ende eine Banane, den Gorilla, der sie hält, und zusätzlich den ganzen Dschungel. Wenn wir uns auf die Gesamtmenge des Gedächtnisses konzentrieren, ist dies dasselbe wie das „Messen“ des Dschungels und nicht der Banane, die uns interessiert.


Gorilla isst eine Banane.

Nun zurück zum obigen Beispiel mitaddEventListener. Eine Leckquelle ist ein Ereignis-Listener, der auf eine Funktion verweist. Und diese Funktion bezieht sich wiederum auf eine Komponente, die möglicherweise Links zu einer Reihe guter Dinge wie Arrays, Strings und Objekten speichert.

Wenn Sie den Unterschied zwischen den Bildern analysieren und die Entitäten nach dem Speicherplatz sortieren, den sie belegen, können Sie viele Arrays, Linien und Objekte sehen, von denen die meisten höchstwahrscheinlich nicht mit dem Leck zusammenhängen. Und schließlich müssen wir genau den Ereignis-Listener finden, von dem aus alles begann. Er nimmt im Vergleich zu dem, worauf er sich bezieht, sehr wenig Gedächtnis ein. Um das Leck zu beheben, müssen Sie eine Banane finden, nicht den Dschungel.

Wenn Sie die Datensätze nach der Anzahl der "durchgesickerten" Objekte sortieren, werden Sie 7 Ereignis-Listener bemerken. Und vielleicht 7 Komponenten und 14 Unterkomponenten und vielleicht noch etwas Ähnliches. Diese Zahl 7 sollte sich vom Gesamtbild abheben, da es sich dennoch um eine eher auffällige und ungewöhnliche Zahl handelt. In diesem Fall spielt es keine Rolle, wie oft die verdächtige Aktion wiederholt wird. Wenn bei der Untersuchung von Bildern der Verdacht berechtigt ist, werden ebenso viele „durchgesickerte“ Objekte aufgezeichnet. Auf diese Weise können Sie die Quelle eines Speicherverlusts schnell identifizieren.

Linkbaumanalyse


Das Tool zum Erstellen von Snapshots bietet die Möglichkeit, „Verknüpfungsketten“ anzuzeigen, mit denen Sie herausfinden können, auf welche Objekte andere Objekte verweisen. Dadurch kann die Anwendung funktionieren. Durch die Analyse solcher "Ketten" oder "Bäume" von Links können Sie genau herausfinden, wo der Speicher für das "undichte" Objekt zugewiesen wurde.


Über die Gliederkette können Sie herausfinden, welches Objekt sich auf das "undichte" Objekt bezieht. Beim Lesen dieser Ketten muss berücksichtigt werden, dass sich die darin befindlichen Objekte auf die oben befindlichen Objekte beziehen.

Im obigen Beispiel gibt es eine Variable namenssomeObjectreferenziert in dem Abschluss (context), auf den der Ereignis-Listener verweist. Wenn Sie auf den Link zum Quellcode klicken, wird ein ziemlich verständlicher Text des Programms angezeigt:

class SomeObject () { /* ... */ }
 
const someObject = new SomeObject();
const onMessage = () => { /* ... */ };
window.addEventListener('message', onMessage);

Wenn wir diesen Code mit der vorherigen Abbildung vergleichen, stellt sich heraus, dass es sich bei contextder Abbildung um einen Abschluss handelt onMessage, auf den verwiesen wird someObject. Dies ist ein künstliches Beispiel . Echte Speicherlecks können viel weniger offensichtlich sein.

Es ist erwähnenswert, dass das Heap-Snapshot-Tool einige Einschränkungen aufweist:

  1. Wenn Sie eine Snapshot-Datei speichern und dann erneut hochladen, gehen Links zu Dateien mit Code verloren. Wenn Sie beispielsweise einen Snapshot heruntergeladen haben, können Sie nicht feststellen, dass sich der Abschlusscode des Ereignis-Listeners in Zeile 22 der Datei befindet foo.js. Da diese Informationen äußerst wichtig sind, ist es fast nutzlos, Heap-Snapshot-Dateien zu speichern oder sie beispielsweise an jemanden zu übertragen.
  2. WeakMap, Chrome , . , , , , . WeakMap — .
  3. Chrome , . , , , , . , object, EventListener. object — , , , «» 7 .

Dies ist eine Beschreibung meiner grundlegenden Strategie zur Identifizierung von Speicherlecks. Ich habe diese Technik erfolgreich eingesetzt, um Dutzende von Lecks zu erkennen.

Es stimmt, ich muss sagen, dass dieser Leitfaden zum Auffinden von Speicherlecks nur einen kleinen Teil dessen abdeckt, was in der Realität geschieht. Dies ist nur der Anfang der Arbeit. Darüber hinaus müssen Sie in der Lage sein, die Installation von Haltepunkten, die Protokollierung und das Testen von Korrekturen durchzuführen, um festzustellen, ob sie das Problem lösen. Und leider bedeutet dies im Wesentlichen eine ernsthafte Zeitinvestition.

Automatisierte Speicherverlustanalyse


Ich möchte diesen Abschnitt mit der Tatsache beginnen, dass ich keinen guten Ansatz zur Automatisierung der Erkennung von Speicherlecks finden konnte. Chrome verfügt über eine eigene Performance.memory- API . Aus Datenschutzgründen können Sie jedoch keine ausreichend detaillierten Daten erfassen. Daher kann diese API in der Produktion nicht zum Erkennen von Lecks verwendet werden. Die W3C Web - Performance - Arbeitsgruppe zuvor diskutierten Speicher Tools , aber ihre Mitglieder haben noch einen neuen Standard zu vereinbaren entwickelt , um diese API zu ersetzen.

In Testumgebungen können Sie die Granularität der Datenausgabe performance.memorymithilfe des Chrome-Flags erhöhen - Enable-Precision-Memory-Info. Heap-Snapshots können weiterhin mit dem eigenen Team des Chromedriver erstellt werden : takeHeapSnapshot . Dieses Team hat die gleichen Einschränkungen, die wir bereits besprochen haben. Wenn Sie diesen Befehl verwenden, ist es aus den oben beschriebenen Gründen wahrscheinlich sinnvoll, ihn dreimal aufzurufen und dann nur das zu verwenden, was als Ergebnis seines letzten Aufrufs empfangen wurde.

Da Ereignis-Listener die häufigste Ursache für Speicherverluste sind, werde ich über eine andere von mir verwendete Leckerkennungstechnik sprechen. Es besteht darin, Affen-Patches für die API zu erstellen addEventListenerund removeEventListenerdie Links zu zählen, um zu überprüfen, ob ihre Anzahl auf Null zurückkehrt. Hier ist ein Beispiel, wie dies gemacht wird.

In den Chrome Developer Tools können Sie auch die native API getEventListeners verwenden , um herauszufinden, welche Ereignis-Listener an ein bestimmtes Element angehängt sind. Dieser Befehl ist jedoch nur in der Entwicklersymbolleiste verfügbar.

Ich möchte hinzufügen, dass Matthias Binens mir von einer anderen nützlichen Chrome-Tools-API erzählt hat. Dies sind queryObjects . Damit können Sie Informationen zu allen Objekten abrufen, die mit einem bestimmten Konstruktor erstellt wurden. Hier finden Sie gutes Material zu diesem Thema zur Automatisierung der Speicherleckerkennung in Puppeteer.

Zusammenfassung


Das Suchen und Beheben von Speicherlecks in Webanwendungen steckt noch in den Kinderschuhen. Hier habe ich über einige Techniken gesprochen, die in meinem Fall gut funktionierten. Es sollte jedoch anerkannt werden, dass die Anwendung dieser Techniken immer noch mit gewissen Schwierigkeiten verbunden und zeitaufwändig ist.

Wie bei allen Leistungsproblemen ist eine Prise im Voraus ein Pfund wert. Vielleicht findet es jemand nützlich, die entsprechenden synthetischen Tests vorzubereiten, anstatt das Leck zu analysieren, nachdem es bereits aufgetreten ist. Und wenn es sich nicht um ein Leck handelt, sondern um mehrere, kann sich die Analyse des Problems in etwas wie das Schälen von Zwiebeln verwandeln: Nachdem ein Problem behoben wurde, wird ein anderes entdeckt, und dieser Vorgang wiederholt sich (und die ganze Zeit wie bei Zwiebeln) Tränen in den Augen). Codeüberprüfungen können auch dazu beitragen, häufige Leckmuster zu identifizieren. Aber dies - wenn Sie wissen - wo Sie suchen müssen.

JavaScript ist eine Sprache, die sicheres Arbeiten mit Speicher ermöglicht. Daher ist es etwas ironisch, wie leicht Speicherverluste in Webanwendungen auftreten. Dies ist teilweise auf die Funktionen der Benutzeroberflächen des Geräts zurückzuführen. Sie müssen viele Ereignisse anhören: Mausereignisse, Bildlaufereignisse, Tastaturereignisse. Das Anwenden all dieser Muster kann leicht zu Speicherlecks führen. Um sicherzustellen, dass unsere Webanwendungen den Speicher sparsam nutzen, können wir ihre Leistung steigern und sie vor „Abstürzen“ schützen. Darüber hinaus zeigen wir damit Respekt vor den Ressourcengrenzen von Benutzergeräten.

Liebe Leser! Sind in Ihren Webprojekten Speicherlecks aufgetreten?


All Articles