Webentwickler erwarten beim Entfernen von Fehlern in ihrem Code nur geringe bis gar keine Leistungseinbußen. Diese Erwartung ist jedoch keineswegs universell. Ein C++-Entwickler würde nie erwarten, dass ein Debug-Build seiner Anwendung die Produktionsleistung erreicht. In den frühen Jahren von Chrome hatte das Öffnen von DevTools bereits einen erheblichen Einfluss auf die Leistung der Seite.
Dass diese Leistungseinbußen nicht mehr spürbar sind, ist das Ergebnis jahrelanger Investitionen in die Debugging-Funktionen von DevTools und V8. Trotzdem werden wir den Leistungsoverhead von DevTools nie auf null reduzieren können. Das Festlegen von Haltepunkten, das Durchlaufen des Codes, das Erfassen von Stack-Traces und das Erfassen eines Leistungs-Traces wirken sich in unterschiedlichem Maße auf die Ausführungsgeschwindigkeit aus. Wenn man etwas beobachtet, verändert sich das Objekt.
Aber natürlich sollte der Overhead von DevTools wie bei jedem Debugger angemessen sein. In letzter Zeit haben wir eine deutliche Zunahme der Meldungen erhalten, dass DevTools die Anwendung in bestimmten Fällen so stark verlangsamt, dass sie nicht mehr verwendet werden kann. Unten sehen Sie einen direkten Vergleich aus dem Bericht chromium:1069425, der den Leistungsoverhead zeigt, der durch das bloße Öffnen der DevTools entsteht.
Wie Sie im Video sehen können, ist die Verzögerung 5- bis 10-mal höher als normal, was natürlich nicht akzeptabel ist. Der erste Schritt bestand darin, herauszufinden, wo die Zeit verloren geht und was diese massive Verlangsamung beim Öffnen der DevTools verursacht. Die Verwendung von Linux perf für den Chrome-Renderer-Prozess ergab die folgende Verteilung der Gesamtausführungszeit des Renderers:
Wir hatten zwar erwartet, dass etwas mit dem Erfassen von Stack-Traces zusammenhängt, aber nicht, dass etwa 90% der gesamten Ausführungszeit für die Symbolisierung von Stack-Frames aufgewendet werden. Die Symbolisierung bezieht sich hier auf die Auflösung von Funktionsnamen und konkreten Quellpositionen (Zeilen- und Spaltennummern in Scripts) aus Roh-Stack-Frames.
Methodennamen-Inferenz
Noch überraschender war, dass fast die gesamte Zeit auf die JSStackFrame::GetMethodName()
-Funktion in V8 entfiel. Wir wussten jedoch aus vorherigen Untersuchungen, dass JSStackFrame::GetMethodName()
kein Unbekannter in der Welt der Leistungsprobleme ist. Mit dieser Funktion wird versucht, den Namen der Methode für Frames zu berechnen, die als Methodeaufrufe betrachtet werden (Frames, die Funktionsaufrufe vom Typ obj.func()
statt func()
darstellen). Ein kurzer Blick in den Code hat ergeben, dass das Objekt und seine Prototypkette vollständig durchlaufen und nach
- Dateneigenschaften, deren
value
diefunc
-Schließung sind, oder - Zugriffseigenschaften, bei denen entweder
get
oderset
derfunc
-Schließung entspricht.
Das klingt zwar nicht besonders günstig, aber auch nicht so, als würde es diese schreckliche Verlangsamung erklären. Wir haben uns das Beispiel in chromium:1069425 genauer angesehen und festgestellt, dass die Stack-Traces sowohl für asynchrone Aufgaben als auch für Protokollmeldungen aus classes.js
erfasst wurden, einer JavaScript-Datei mit 10 MiB. Bei genauerer Betrachtung stellte sich heraus, dass es sich im Grunde um eine Java-Laufzeit und Anwendungscode handelte, der in JavaScript kompiliert wurde. Die Stack-Traces enthielten mehrere Frames mit Methoden, die auf ein Objekt A
aufgerufen wurden. Daher wollten wir herausfinden, um welche Art von Objekt es sich handelt.
Offenbar hatte der Java-zu-JavaScript-Compiler ein einzelnes Objekt mit 82.203 Funktionen generiert. Das wurde langsam interessant. Als Nächstes haben wir uns die JSStackFrame::GetMethodName()
von V8 noch einmal angesehen, um herauszufinden, ob es dort noch einfache Verbesserungsmöglichkeiten gibt.
- Dazu wird zuerst die
"name"
der Funktion als Attribut des Objekts abgerufen. Wenn sie gefunden wird, wird geprüft, ob der Attributwert mit der Funktion übereinstimmt. - Wenn die Funktion keinen Namen hat oder das Objekt keine übereinstimmende Eigenschaft hat, wird eine umgekehrte Suche durchgeführt, bei der alle Eigenschaften des Objekts und seiner Prototypen durchlaufen werden.
In unserem Beispiel sind alle Funktionen anonym und haben leere "name"
-Attribute.
A.SDV = function() {
// ...
};
Die erste Erkenntnis war, dass die Rückwärtssuche in zwei Schritte unterteilt wurde (für das Objekt selbst und jedes Objekt in der Prototypkette ausgeführt):
- Extrahieren Sie die Namen aller aufzählbaren Properties und
- Führen Sie für jeden Namen eine allgemeine Property-Suche durch und prüfen Sie, ob der resultierende Property-Wert mit der gesuchten Schließung übereinstimmt.
Das schien eine relativ einfache Aufgabe zu sein, da zum Extrahieren der Namen alle Unterkünfte durchgegangen werden müssen. Anstatt zwei Durchläufe auszuführen – O(N) für die Namensextraktion und O(N log(N)) für die Tests – könnten wir alles in einem einzigen Durchlauf erledigen und die Property-Werte direkt prüfen. Dadurch wurde die gesamte Funktion um 2–10 Mal schneller.
Die zweite Erkenntnis war noch interessanter. Obwohl es sich technisch gesehen um anonyme Funktionen handelt, hat die V8-Engine für sie einen sogenannten abgeleiteten Namen erfasst. Bei Funktionsliteralen, die rechts neben Zuweisungen in der Form obj.foo = function() {...}
erscheinen, speichert der V8-Parser "obj.foo"
als abgeleiteten Namen für das Funktionsliteral. In unserem Fall bedeutet das, dass wir zwar keinen korrekten Namen hatten, den wir einfach nachschlagen konnten, aber einen ziemlich ähnlichen: Im Beispiel A.SDV = function() {...}
oben hatten wir "A.SDV"
als abgeleiteten Namen. Wir konnten den Property-Namen aus dem abgeleiteten Namen ableiten, indem wir nach dem letzten Punkt suchten und dann nach der Property "SDV"
auf dem Objekt suchten. Das hat in fast allen Fällen funktioniert und eine teure vollständige Durchsuchung durch eine einzelne Property-Suche ersetzt. Diese beiden Verbesserungen wurden im Rahmen von dieser CL eingeführt und haben die Verlangsamung für das in chromium:1069425 beschriebene Beispiel erheblich reduziert.
Error.stack
Wir hätten hier aufhören können. Aber irgendetwas stimmte nicht, da in den DevTools nie der Methodenname für Stackframes verwendet wird. Tatsächlich gibt es in der C++ API für die Klasse v8::StackFrame
nicht einmal eine Möglichkeit, den Methodennamen abzurufen. Daher erschien es uns falsch, dass wir JSStackFrame::GetMethodName()
überhaupt anrufen würden. Stattdessen wird der Methodenname nur in der JavaScript-Stack-Trace-API verwendet (und freigegeben). Das folgende einfache Beispiel error-methodname.js
veranschaulicht diese Verwendung:
function foo() {
console.log((new Error).stack);
}
var object = {bar: foo};
object.bar();
Hier sehen wir eine Funktion foo
, die unter dem Namen "bar"
auf object
installiert ist. Wenn Sie dieses Snippet in Chromium ausführen, erhalten Sie die folgende Ausgabe:
Error
at Object.foo [as bar] (error-methodname.js:2)
at error-methodname.js:6
Hier sehen wir die Suche nach dem Methodennamen: Der oberste Stackframe ruft die Funktion foo
über die Methode bar
auf einer Instanz von Object
auf. Die nicht standardmäßige error.stack
-Property nutzt also JSStackFrame::GetMethodName()
intensiv. Unsere Leistungstests haben außerdem gezeigt, dass unsere Änderungen die Leistung deutlich verbessert haben.
Zurück zu den Chrome-Entwicklertools: Der Name der Methode wird berechnet, obwohl error.stack
nicht verwendet wird. Das ist nicht richtig. Hier ist ein wenig Hintergrundwissen hilfreich: Traditionell gab es in V8 zwei unterschiedliche Mechanismen zum Erfassen und Darstellen eines Stack-Traces für die beiden oben beschriebenen APIs (die C++-v8::StackFrame
API und die JavaScript-Stack-Trace API). Zwei verschiedene Möglichkeiten, (in etwa) dasselbe zu tun, waren fehleranfällig und führten oft zu Inkonsistenzen und Fehlern. Deshalb haben wir Ende 2018 ein Projekt gestartet, um ein einziges Nadelöhr für die Erfassung von Stacktraces zu finden.
Dieses Projekt war ein großer Erfolg und die Anzahl der Probleme im Zusammenhang mit der Stack-Trace-Erfassung konnte drastisch reduziert werden. Die meisten Informationen, die über die nicht standardmäßige error.stack
-Property bereitgestellt wurden, wurden ebenfalls nur bei Bedarf und verzögert berechnet. Im Rahmen des Refaktorings haben wir diesen Trick jedoch auch auf v8::StackFrame
-Objekte angewendet. Alle Informationen zum Stackframe werden berechnet, wenn eine Methode zum ersten Mal darauf aufgerufen wird.
Dies verbessert in der Regel die Leistung, aber leider widerspricht es der Art und Weise, wie diese C++ API-Objekte in Chromium und DevTools verwendet werden. Insbesondere da wir eine neue v8::internal::StackFrameInfo
-Klasse eingeführt hatten, die alle Informationen zu einem Stack-Frame enthielt, die entweder über v8::StackFrame
oder über error.stack
freigegeben wurden, berechneten wir immer die Übermenge der von beiden APIs bereitgestellten Informationen. Das bedeutete, dass bei der Verwendung von v8::StackFrame
(und insbesondere in DevTools) auch der Methodenname berechnet wurde, sobald Informationen zu einem Stack-Frame angefordert wurden. Es stellt sich heraus, dass DevTools immer sofort Quell- und Scriptinformationen anfordert.
Auf dieser Grundlage konnten wir die Darstellung des Stack Frames überarbeiten und drastisch vereinfachen und noch effizienter gestalten. So werden in V8 und Chromium jetzt nur noch die Kosten für die Berechnung der angeforderten Informationen fällig. Dies führte zu einer enormen Leistungssteigerung für die DevTools und andere Chromium-Anwendungsfälle, für die nur ein Bruchteil der Informationen zu Stackframes benötigt wird (im Wesentlichen nur der Scriptname und der Quellspeicherort in Form von Zeilen- und Spaltenoffset). Außerdem eröffnete dies die Möglichkeit für weitere Leistungsverbesserungen.
Funktionsnamen
Nachdem die oben genannten Refactorings abgeschlossen waren, wurde der Overhead der Symbolisierung (die Zeit, die in v8_inspector::V8Debugger::symbolize
verbracht wurde) auf etwa 15% der gesamten Ausführungszeit reduziert. Außerdem konnten wir besser nachvollziehen, wo V8 Zeit beim Erfassen und Symbolisieren von Stackframes für die Verwendung in DevTools verbrauchte.
Als Erstes fielen mir die kumulativen Kosten für die Berechnung der Zeilen- und Spaltennummer auf. Der kostspielige Teil besteht darin, den Zeichenoffset im Script zu berechnen (basierend auf dem Bytecode-Offset, den wir von V8 erhalten). Aufgrund der oben beschriebenen Refaktorisierung haben wir das zweimal getan: einmal bei der Berechnung der Zeilennummer und einmal bei der Berechnung der Spaltennummer. Durch das Caching der Quellposition in v8::internal::StackFrameInfo
-Instanzen konnte das Problem schnell behoben und v8::internal::StackFrameInfo::GetColumnNumber
vollständig aus allen Profilen entfernt werden.
Für uns war es jedoch interessanter, dass v8::StackFrame::GetFunctionName
in allen Profilen, die wir uns angesehen haben, überraschend hoch war. Bei der genaueren Untersuchung haben wir festgestellt, dass es unnötig aufwendig war, den Namen zu berechnen, den wir für die Funktion im Stack-Frame in DevTools anzeigen.
- zuerst nach der nicht standardmäßigen
"displayName"
-Property suchen. Wenn diese eine Datenproperty mit einem Stringwert liefert, wird diese verwendet. - andernfalls wird nach der Standard-
"name"
-Property gesucht und noch einmal geprüft, ob diese eine Datenproperty mit einem Stringwert liefert. - und greift schließlich auf einen internen Debugnamen zurück, der vom V8-Parser abgeleitet und im Funktionsliteral gespeichert wird.
Das Attribut "displayName"
wurde als Problemumgehung für das Attribut "name"
hinzugefügt, da Function
-Instanzen in JavaScript nur lesend und nicht konfigurierbar sind.Es wurde jedoch nie standardisiert und fand keine breite Verwendung, da die Browser-Entwicklertools eine Funktion zur Namensableitung hinzugefügt haben, die in 99,9% der Fälle funktioniert. Außerdem wurde in ES2015 die Property "name"
in Function
-Instanzen konfigurierbar, sodass keine spezielle "displayName"
-Property mehr erforderlich ist. Da die negative Suche nach "displayName"
recht kostspielig und nicht wirklich notwendig ist (ES2015 wurde vor über fünf Jahren veröffentlicht), haben wir beschlossen, den Support für die nicht standardmäßige Eigenschaft fn.displayName
aus V8 (und den DevTools) zu entfernen.
Da die negative Suche nach "displayName"
nicht mehr erforderlich ist, wurden die Hälfte der Kosten für v8::StackFrame::GetFunctionName
entfernt. Die andere Hälfte wird an die allgemeine "name"
-Property-Suche weitergeleitet. Glücklicherweise hatten wir bereits eine Logik implementiert, um kostspielige Suchanfragen der "name"
-Property in (unberührten) Function
-Instanzen zu vermeiden. Diese Logik wurde vor einiger Zeit in V8 eingeführt, um Function.prototype.bind()
selbst schneller zu machen. Wir haben die erforderlichen Prüfungen portiert, sodass wir die kostspielige generische Suche von vornherein überspringen können. Das Ergebnis ist, dass v8::StackFrame::GetFunctionName
in keinem der von uns berücksichtigten Profile mehr angezeigt wird.
Fazit
Durch die oben genannten Verbesserungen konnten wir den Overhead der DevTools im Hinblick auf Stack-Traces erheblich reduzieren.
Wir wissen, dass es noch verschiedene Verbesserungsmöglichkeiten gibt. So ist beispielsweise der Overhead bei der Verwendung von MutationObserver
s immer noch spürbar, wie in chromium:1077657 beschrieben. Vorerst haben wir jedoch die wichtigsten Probleme behoben. Wir werden uns in Zukunft möglicherweise noch einmal damit befassen, um die Debugging-Leistung weiter zu optimieren.
Vorschaukanäle herunterladen
Verwenden Sie als Standard-Entwicklungsbrowser Chrome Canary, Chrome Dev oder Chrome Beta. Diese Vorabversionen bieten Ihnen Zugriff auf die neuesten DevTools-Funktionen, ermöglichen es Ihnen, innovative Webplattform-APIs zu testen, und helfen Ihnen, Probleme auf Ihrer Website zu finden, bevor Ihre Nutzer sie bemerken.
Chrome-Entwicklertools-Team kontaktieren
Mit den folgenden Optionen können Sie über neue Funktionen, Updates oder andere Themen im Zusammenhang mit den DevTools sprechen.
- Senden Sie uns Feedback und Funktionsanfragen unter crbug.com.
- Melden Sie ein DevTools-Problem über das Dreipunkt-Menü Weitere Optionen > Hilfe > DevTools-Problem melden.
- Tweeten Sie an @ChromeDevTools.
- Hinterlassen Sie Kommentare unter den YouTube-Videos zu den Neuigkeiten in den DevTools oder den YouTube-Videos mit Tipps zu den DevTools.