Die Geschichte eines Problems mit dem Tachometer oder wie Chromium den Speicher verwaltet

Ein moderner Browser ist ein äußerst komplexes Projekt, bei dem selbst harmlos aussehende Änderungen zu unerwarteten Überraschungen führen können. Daher gibt es viele interne Tests, die solche Änderungen vor der Veröffentlichung erfassen sollten. Es gibt nie zu viele Tests, daher ist es nützlich, auch öffentliche Benchmarks von Drittanbietern zu verwenden.

Mein Name ist Andrey Logvinov, ich arbeite in der Yandex.Browser Rendering Engine Development Group in Nischni Nowgorod. Heute werde ich Habrs Lesern anhand des Beispiels eines mysteriösen Problems, das zu Leistungseinbußen im Tachometer- Test führte, erzählen, wie die Speicherverwaltung im Chromium-Projekt funktioniert. Dieser Beitrag basiert auf meinem Bericht vom Yandex.Inside-Event.




Auf unserem Leistungs-Dashboard haben wir eine Verschlechterung der Geschwindigkeit des Tachometer-Tests festgestellt. Dieser Test misst die Gesamtleistung des Browsers in einer realitätsnahen Anwendung - einer Aufgabenliste, in der der Test Elemente zur Liste hinzufügt und diese dann durchstreichen. Die Testergebnisse werden sowohl von der Leistung der V8 JS-Engine als auch von der Geschwindigkeit beim Rendern von Seiten in der Blink-Engine beeinflusst. Der Tacho-Test besteht aus mehreren Untertests, bei denen die Testanwendung mit einem der gängigen JS-Frameworks geschrieben wird, z. B. jQuery oder ReactJS. Das gesamte Testergebnis ist als Durchschnitt der Ergebnisse für alle Frameworks definiert. Mit dem Test können Sie jedoch die Leistung für jedes Framework einzeln anzeigen. Es ist anzumerken, dass der Test nicht darauf abzielt, die Leistung von Frameworks zu bewerten. Sie werden nur verwendet, um den Test weniger synthetisch und realer zu machen. Die Detaillierung nach Subtest zeigte, dass eine Verschlechterung nur für die mit jQuery erstellte Version der Testanwendung beobachtet wird. Und das ist schon interessant, stimme zu.

Die Untersuchung solcher Situationen beginnt ziemlich normal - wir bestimmen, welche bestimmte Verpflichtung gegenüber dem Code zu dem Problem geführt hat. Zu diesem Zweck speichern wir Yandex.Browser-Baugruppen für jedes (!) Commit in den letzten Jahren (ein Zusammenbau wäre unpraktisch, da die Baugruppe mehrere Stunden dauert). Dies nimmt viel Platz auf den Servern in Anspruch, hilft jedoch normalerweise dabei, die Ursache des Problems schnell zu finden. Aber diesmal hat es schnell nicht geklappt. Es stellte sich heraus, dass die Verschlechterung der Testergebnisse mit einem Commit zusammenfiel, bei dem die nächste Version von Chromium integriert wurde. Das Ergebnis ist nicht ermutigend, da die neue Version von Chromium eine Vielzahl von Änderungen gleichzeitig mit sich bringt.

Da wir keine Informationen über eine bestimmte Änderung erhalten haben, musste ich das Problem inhaltlich untersuchen. Zu diesem Zweck haben wir die Entwicklertools mithilfe der Entwicklertools entfernt. Wir haben eine seltsame Funktion bemerkt - "zerrissene" Intervalle für die Ausführung von Javascript-Testfunktionen.

Bild

Wir entfernen eine technischere Ablaufverfolgung mit about: tracing und stellen fest, dass es sich bei Blink um Garbage Collection (GC) handelt .

Bild

Die folgende Speicherspur zeigt, dass diese GC-Pausen nicht nur viel Zeit in Anspruch nehmen, sondern auch nicht dazu beitragen, das Wachstum des Speicherverbrauchs zu stoppen.

Bild

Wenn Sie jedoch einen expliziten GC-Aufruf in den Test einfügen, sehen wir ein völlig anderes Bild - der Speicher wird im Nullbereich gehalten und leckt nicht. Wir haben also keine Speicherlecks und das Problem hängt mit den Funktionen des Kollektors zusammen. Wir graben weiter. Wir starten den Debugger und sehen, dass der Garbage Collector ungefähr 500.000 Objekte umgangen hat! Eine solche Anzahl von Objekten konnte die Leistung nicht beeinträchtigen. Aber woher kamen sie?

Und hier brauchen wir einen kleinen Rückblick auf das Garbage Collector-Gerät in Blink. Es entfernt tote Objekte, verschiebt jedoch keine lebenden Objekte, was das Arbeiten mit bloßen Zeigern in lokalen Variablen in C ++ - Code ermöglicht. Dieses Muster wird in Blink aktiv verwendet. Aber es hat auch seinen Preis: Wenn Sie Müll sammeln, müssen Sie den Stream- Stack scannen. Wenn dort etwas Ähnliches wie ein Zeiger auf ein Objekt von einem Heap (Heap) gefunden wird, betrachten Sie das Objekt und alles, worauf es direkt oder indirekt verweist, als lebendig. Dies führt dazu, dass einige praktisch unzugängliche und damit „tote“ Objekte als lebend identifiziert werden. Daher wird diese Form der Speicherbereinigung auch als konservativ bezeichnet.

Wir überprüfen die Verbindung mit dem Stack-Scan und überspringen sie. Das Problem ist verschwunden.

Was kann so in einem Stapel sein, der 500.000 Objekte enthält? Wir setzen einen Haltepunkt in die Funktion des Hinzufügens von Objekten - unter anderem sehen wir, dass es verdächtig ist:

blink :: TraceTrait <blink :: HeapHashTableBacking <WTF :: HashTable <blink :: WeakMember ...

Eine Hash-Tabellenreferenz ist ein wahrscheinlicher Verdächtiger! Wir testen die Hypothese, indem wir das Hinzufügen dieses Links überspringen. Das Problem ist verschwunden. Nun, wir sind der Antwort einen Schritt näher gekommen.

Wir erinnern uns an eine weitere Funktion des Garbage Collector in Blink: Wenn er einen Zeiger auf das Innere der Hash-Tabelle sieht, betrachtet er dies als Zeichen einer fortlaufenden Iteration über die Tabelle, was bedeutet, dass alle Links in dieser Tabelle nützlich sind und diese weiterhin umgehen. In unserem Fall im Leerlauf. Aber welche Funktion hat dieser Link?

Wir rücken einige Frames des Stapels höher vor, nehmen die aktuelle Position des Scanners ein und sehen uns den Stack-Frame an, in welche Funktion er fällt. Dies ist eine Funktion namens ScheduleGCIfNeeded . Es scheint, dass er hier der Schuldige ist, aber ... wir schauen uns den Quellcode der Funktion an und sehen, dass es dort überhaupt keine Hash-Tabellen gibt. Darüber hinaus ist dies bereits Teil des Garbage Collectors selbst und muss einfach nicht auf Objekte aus dem Blink-Heap verweisen. Woher kommt dieser "schlechte" Link dann?

Wir haben einen Haltepunkt beim Ändern der Speicherzelle festgelegt, in dem wir einen Link zur Hash-Tabelle gefunden haben. Wir sehen, dass eine der internen Funktionen namens V8PerIsolateData :: AddActiveScriptWrappable dort schreibt. Dort fügen sie einer einzelnen Hash-Tabelle active_script_wrappables_ einige erstellte HTML-Elemente einiger Typen, einschließlich Eingaben, hinzu. Diese Tabelle wird benötigt, um das Entfernen von Elementen zu verhindern, auf die nicht mehr in Javascript oder im DOM-Baum verwiesen wird, die jedoch externen Aktivitäten zugeordnet sind, die beispielsweise Ereignisse generieren können.

Der Garbage Collector berücksichtigt beim normalen Durchlaufen der Tabelle den Status der darin enthaltenen Elemente und markiert sie entweder als aktiv oder nicht. Anschließend werden sie in der nächsten Phase der Montage gelöscht. In unserem Fall wird jedoch beim Scannen des Stapels ein Zeiger auf den internen Speicher dieser Tabelle angezeigt, und alle Elemente der Tabelle werden als aktiv markiert.

Aber wie hat der Wert aus dem Stapel einer Funktion den Stapel einer anderen Funktion getroffen ?!

Denken Sie an ScheduleGCIfNeeded. Denken Sie daran, dass im Quellcode dieser Funktion nichts Nützliches gefunden wurde. Dies bedeutet jedoch nur, dass es Zeit ist, auf eine niedrigere Ebene zu wechseln und den Compiler zu überprüfen. Das zerlegte Prolog der ScheduleGCIfNeeded-Funktion sieht folgendermaßen aus:

0FCDD13A push ebp 0FCDD13B mov ebp,esp 0FCDD13D push edi 0FCDD13E push esi 0FCDD13F and esp,0FFFFFFF8h 0FCDD142 sub esp,0B8h 0FCDD148 mov eax,dword ptr [__security_cookie (13DD3888h)] 0FCDD14D mov esi,ecx 0FCDD14F xor eax,ebp 0FCDD151 mov dword ptr [esp+0B4h],eax 

Es ist zu sehen, dass sich die Funktion besonders auf 0B8h nach unten bewegt und dieser Ort nicht weiter verwendet wird. Aus diesem Grund sieht der Stapelscanner jedoch, was zuvor von anderen Funktionen aufgezeichnet wurde. Und zufällig gelangt ein Zeiger auf die Innenseite der Hash-Tabelle, die von der AddActiveScriptWrappable-Funktion hinterlassen wurde, in dieses „Loch“. Wie sich herausstellte, war der Grund für das Auftreten des „Lochs“ in diesem Fall das VLOG- Debug- Makro innerhalb der Funktion, das zusätzliche Informationen im Protokoll anzeigt.

Aber warum hatte die Tabelle active_script_wrappable_ Hunderttausende von Elementen? Warum wird eine Leistungsverschlechterung nur beim jQuery-Test beobachtet? Die Antwort auf beide Fragen ist dieselbe - in diesem speziellen Test wird für jede Änderung (wie ein Häkchen im Kontrollkästchen) die gesamte Benutzeroberfläche vollständig neu erstellt. Der Test erzeugt Elemente, die sich fast sofort in Müll verwandeln. Der Rest der Tests im Tachometer ist umsichtiger und erzeugt keine unnötigen Elemente. Daher wird für sie keine Leistungsverschlechterung beobachtet. Wenn Sie Webdienste entwickeln, sollten Sie dies berücksichtigen, um keine unnötige Arbeit für den Browser zu verursachen.

Aber warum trat das Problem erst jetzt auf, wenn das VLOG-Makro vorher war? Es gibt keine genaue Antwort, aber höchstwahrscheinlich hat sich während der Aktualisierung die relative Position der Elemente auf dem Stapel geändert, wodurch der Zeiger auf die Hash-Tabelle versehentlich für den Scanner zugänglich wurde. Tatsächlich haben wir im Lotto gewonnen. Um das „Loch“ schnell zu schließen und die Leistung wiederherzustellen, haben wir das VLOG-Debug-Makro entfernt. Für Benutzer ist es nutzlos und für unsere eigenen Diagnoseanforderungen können wir es jederzeit wieder einschalten. Wir haben unsere Erfahrungen auch mit anderen Chromium-Entwicklern geteilt. Die Antwort bestätigte unsere Befürchtungen: Dies ist ein grundlegendes Problem der konservativen Speicherbereinigung in Blink, für das es keine systemische Lösung gibt.

Interessante Links


1. Wenn Sie mehr über den ungewöhnlichen Alltag unserer Gruppe erfahren möchten, erinnern wir uns an die Geschichte des schwarzen Rechtecks , die nicht nur zur Beschleunigung von Yandex.Browser, sondern des gesamten Chromium-Projekts führte.

2. Ich lade Sie auch ein, sich beim nächsten Yandex.Inside- Event am 16. Februar andere Berichte anzuhören. Die Registrierung ist offen und die Sendung wird ebenfalls ausgestrahlt.

Source: https://habr.com/ru/post/de436154/


All Articles