Hoe we de stacktraces van Chrome DevTools tien keer sneller hebben gemaakt

Benedikt Meurer
Benedikt Meurer

Webontwikkelaars verwachten weinig tot geen impact op de prestaties bij het debuggen van hun code. Deze verwachting is echter zeker niet universeel. Een C++-ontwikkelaar zou nooit verwachten dat een debug-build van zijn applicatie productieprestaties zou bereiken, en in de beginjaren van Chrome had het simpelweg openen van DevTools een aanzienlijke invloed op de prestaties van de pagina.

Het feit dat deze prestatievermindering niet langer voelbaar is, is het resultaat van jarenlange investeringen in de foutopsporingsmogelijkheden van DevTools en V8 . Niettemin zullen we de prestatieoverhead van DevTools nooit tot nul kunnen terugbrengen . Het instellen van breekpunten, het doorlopen van de code, het verzamelen van stacktraces, het vastleggen van een prestatietracering, enz. hebben allemaal in verschillende mate invloed op de uitvoeringssnelheid. Iets observeren verandert het immers.

Maar natuurlijk moet de overhead van DevTools - zoals elke debugger - redelijk zijn. Onlangs hebben we een aanzienlijke toename gezien in het aantal meldingen dat DevTools in bepaalde gevallen de applicatie zodanig zou vertragen dat deze niet meer bruikbaar is. Hieronder ziet u een vergelijking naast elkaar uit het rapport chroom:1069425 , ter illustratie van de prestatieoverhead als u DevTools letterlijk open heeft staan.

Zoals je in de video kunt zien, ligt de vertraging in de orde van 5-10x , wat duidelijk niet acceptabel is. De eerste stap was om te begrijpen waar de tijd naartoe gaat en wat deze enorme vertraging veroorzaakt wanneer DevTools open was. Het gebruik van Linux perf in het Chrome Renderer-proces bracht de volgende verdeling van de totale uitvoeringstijd van de renderer aan het licht:

Uitvoeringstijd van Chrome Renderer

Hoewel we enigszins hadden verwacht iets te zien dat verband hield met het verzamelen van stapelsporen, hadden we niet verwacht dat ongeveer 90% van de totale uitvoeringstijd gaat naar het symboliseren van stapelframes. Symbolisering verwijst hier naar de handeling van het omzetten van functienamen en concrete bronposities (lijn- en kolomnummers in scripts) uit onbewerkte stapelframes.

Inferentie van methodenaam

Wat zelfs nog verrassender was, was het feit dat bijna altijd naar de JSStackFrame::GetMethodName() functie in V8 gaat - hoewel we uit eerdere onderzoeken wisten dat JSStackFrame::GetMethodName() geen onbekende is in het land van prestatieproblemen. Deze functie probeert de naam van de methode te berekenen voor frames die worden beschouwd als methode-aanroepen (frames die functie-aanroepen vertegenwoordigen in de vorm obj.func() in plaats van func() ). Een snelle blik in de code onthulde dat deze werkt door het object en de prototypeketen volledig te doorlopen en te zoeken

  1. gegevenseigenschappen waarvan value de func afsluiting is, of
  2. accessor-eigenschappen waarbij get of set gelijk is aan de func sluiting.

Hoewel dit op zichzelf niet bijzonder goedkoop klinkt, klinkt het ook niet alsof het deze vreselijke vertraging zou verklaren. Dus begonnen we ons te verdiepen in het voorbeeld dat wordt gerapporteerd in chromium:1069425 , en we ontdekten dat de stacktraces werden verzameld voor zowel asynchrone taken als voor logberichten afkomstig van classes.js - een JavaScript-bestand van 10 MiB. Bij nadere beschouwing bleek dat dit in feite een Java-runtime plus applicatiecode was, gecompileerd naar JavaScript. De stapelsporen bevatten verschillende frames met methoden die werden aangeroepen op object A , dus we dachten dat het de moeite waard zou kunnen zijn om te begrijpen met wat voor soort object we te maken hebben.

stapelsporen van een object

Blijkbaar genereerde de Java-naar-JavaScript-compiler één enkel object met maar liefst 82.203 functies erop - dit begon duidelijk interessant te worden. Vervolgens gingen we terug naar V8's JSStackFrame::GetMethodName() om te begrijpen of er laaghangend fruit was dat we daar konden plukken.

  1. Het werkt door eerst de "name" van de functie op te zoeken als een eigenschap van het object en als deze wordt gevonden, wordt gecontroleerd of de eigenschapswaarde overeenkomt met de functie.
  2. Als de functie geen naam heeft of als het object geen overeenkomende eigenschap heeft, valt deze terug op een reverse lookup door alle eigenschappen van het object en zijn prototypes te doorlopen.

In ons voorbeeld zijn alle functies anoniem en hebben ze lege "name" eigenschappen.

A.SDV = function() {
   // ...
};

De eerste bevinding was dat de reverse lookup in twee stappen werd opgesplitst (uitgevoerd voor het object zelf en elk object in zijn prototypeketen):

  1. Extraheer de namen van alle opsombare eigenschappen, en
  2. Voer voor elke naam een ​​algemene eigenschapszoekopdracht uit en test of de resulterende eigenschapswaarde overeenkomt met de sluiting waarnaar we op zoek waren.

Dat leek een vrij laaghangend fruit, aangezien het extraheren van de namen al door alle eigendommen moet lopen. In plaats van de twee stappen uit te voeren - O(N) voor de naamextractie en O(N log(N)) voor de tests - konden we alles in één keer doen en direct de eigenschapswaarden controleren. Dat maakte de hele functie ongeveer 2-10x sneller.

De tweede bevinding was zelfs nog interessanter. Hoewel de functies technisch gezien anonieme functies waren, had de V8-motor er toch een zogenaamde afgeleide naam voor vastgelegd. Voor functieletterlijke waarden die aan de rechterkant van toewijzingen verschijnen in de vorm obj.foo = function() {...} onthoudt de V8-parser "obj.foo" als afgeleide naam voor de letterlijke functie. Dus in ons geval betekent dit dat we, hoewel we niet de juiste naam hadden die we gewoon konden opzoeken, wel iets hadden dat dichtbij genoeg was: voor het voorbeeld A.SDV = function() {...} hierboven hadden we de "A.SDV" als afgeleide naam, en we kunnen de eigenschapsnaam afleiden uit de afgeleide naam door naar de laatste punt te zoeken en vervolgens op jacht te gaan naar de eigenschap "SDV" voor het object. Dat voldeed in bijna alle gevallen, waarbij een dure volledige doorgang werd vervangen door een enkele zoekopdracht naar eigendommen. Deze twee verbeteringen zijn onderdeel van deze CL en hebben de vertraging voor het voorbeeld gerapporteerd in chroom:1069425 aanzienlijk verminderd.

Fout.stack

We hadden het hier een dag kunnen noemen. Maar er was iets verdachts aan de hand, aangezien DevTools de methodenaam nooit gebruikt voor stapelframes. In feite biedt de klasse v8::StackFrame in de C++ API niet eens een manier om bij de naam van de methode te komen. Het leek dus verkeerd dat we uiteindelijk JSStackFrame::GetMethodName() zouden aanroepen. In plaats daarvan is de enige plaats waar we de methodenaam gebruiken (en blootleggen) in de JavaScript stack trace API . Om dit gebruik te begrijpen, kunt u het volgende eenvoudige voorbeeld error-methodname.js overwegen:

function foo() {
    console.log((new Error).stack);
}

var object = {bar: foo};
object.bar();

Hier hebben we een functie foo die is geïnstalleerd onder de naam "bar" op object . Het uitvoeren van dit fragment in Chromium levert de volgende uitvoer op:

Error
    at Object.foo [as bar] (error-methodname.js:2)
    at error-methodname.js:6

Hier zien we het opzoeken van de naam van de methode in het spel: Er wordt getoond dat het bovenste stapelframe de functie foo aanroept op een exemplaar van Object via de methode met de naam bar . De niet-standaard eigenschap error.stack maakt dus intensief gebruik van JSStackFrame::GetMethodName() en in feite geven onze prestatietests ook aan dat onze wijzigingen de zaken aanzienlijk sneller hebben gemaakt.

Versnel uw StackTrace-microbenchmarks

Maar terug naar het onderwerp Chrome DevTools: het feit dat de naam van de methode wordt berekend, ook al wordt error.stack niet gebruikt, ziet er niet goed uit. Er is hier wat geschiedenis die ons helpt: traditioneel had V8 twee verschillende mechanismen om een ​​stacktrace te verzamelen en weer te geven voor de twee verschillende API's die hierboven zijn beschreven (de C++ v8::StackFrame API en de JavaScript stacktrace-API). Het hebben van twee verschillende manieren om (grofweg) hetzelfde te doen, was foutgevoelig en leidde vaak tot inconsistenties en bugs. Daarom zijn we eind 2018 met een project begonnen om een ​​enkel knelpunt voor het vastleggen van stacktraces op te lossen.

Dat project was een groot succes en verminderde het aantal problemen met betrekking tot het verzamelen van stacktraces drastisch. De meeste informatie die via de niet-standaard eigenschap error.stack werd verstrekt, was ook lui berekend en alleen wanneer dit echt nodig was, maar als onderdeel van de refactoring hebben we dezelfde truc toegepast op v8::StackFrame objecten. Alle informatie over het stapelframe wordt berekend de eerste keer dat er een methode op wordt aangeroepen.

Dit verbetert over het algemeen de prestaties, maar helaas bleek het enigszins in strijd te zijn met de manier waarop deze C++ API-objecten worden gebruikt in Chromium en DevTools. Met name sinds we een nieuwe klasse v8::internal::StackFrameInfo hadden geïntroduceerd, die alle informatie bevatte over een stapelframe dat werd blootgesteld via v8::StackFrame of via error.stack , berekenden we altijd de superset van de informatie die door beide API's werd verstrekt, wat betekende dat we voor gebruik van v8::StackFrame (en in het bijzonder voor DevTools) ook de naam van de methode zouden berekenen, zodra er informatie over een stackframe wordt gevraagd. Het blijkt dat DevTools altijd direct bron- en scriptinformatie opvraagt.

Op basis van dat besef konden we de stackframe-representatie drastisch vereenvoudigen en nog lui maken, zodat gebruikers in V8 en Chromium nu alleen de kosten betalen voor het berekenen van de informatie waar ze om vragen. Dit gaf een enorme prestatieverbetering voor de DevTools en andere Chromium-gebruiksscenario's, die slechts een fractie van de informatie over stapelframes nodig hebben (in wezen alleen de scriptnaam en de bronlocatie in de vorm van lijn- en kolomverschuiving), en opende de deur voor meer prestatieverbeteringen.

Functienamen

Nu de hierboven genoemde refactorings achter de rug waren, werd de overhead van symbolisatie (de tijd besteed aan v8_inspector::V8Debugger::symbolize ) teruggebracht tot ongeveer 15% van de totale uitvoeringstijd, en konden we duidelijker zien waar V8 tijd doorbracht wanneer (verzamelen en) symboliseren van stapelframes voor consumptie in DevTools.

Symboliseringskosten

Het eerste dat opviel waren de cumulatieve kosten voor het berekenen van het regel- en kolomnummer. Het dure deel hier is eigenlijk het berekenen van de karakter-offset binnen het script (gebaseerd op de bytecode-offset die we krijgen van V8), en het bleek dat we dat dankzij onze refactoring hierboven twee keer hebben gedaan, een keer bij het berekenen van het regelnummer en een andere keer bij het berekenen van het kolomnummer. Het cachen van de bronpositie op v8::internal::StackFrameInfo -instanties hielp om dit snel op te lossen en v8::internal::StackFrameInfo::GetColumnNumber volledig uit alle profielen te elimineren.

De interessantere bevinding voor ons was dat v8::StackFrame::GetFunctionName verrassend hoog was in alle profielen die we bekeken. Toen we hier dieper gingen graven, realiseerden we ons dat het onnodig duur was om de naam te berekenen die we zouden laten zien voor de functie in het stapelframe in DevTools,

  1. eerst op zoek naar de niet-standaard eigenschap "displayName" en als dat een data-eigenschap met een tekenreekswaarde zou opleveren, zouden we die gebruiken,
  2. anders terugvallen op het zoeken naar de standaard eigenschap "name" en opnieuw controleren of dat een data-eigenschap oplevert waarvan de waarde een string is,
  3. en uiteindelijk terugvallen op een interne debug-naam die wordt afgeleid door de V8-parser en opgeslagen in de functie letterlijk.

De eigenschap "displayName" is toegevoegd als oplossing voor de eigenschap "name" van Function instanties die alleen-lezen en niet-configureerbaar zijn in JavaScript, maar is nooit gestandaardiseerd en werd niet op grote schaal gebruikt, aangezien de browserontwikkelaar tools hebben functienaaminferentie toegevoegd die in 99,9% van de gevallen het werk doet. Bovendien heeft ES2015 de eigenschap "name" op Function -instanties configureerbaar gemaakt, waardoor de noodzaak voor een speciale eigenschap "displayName" volledig werd geëlimineerd. Omdat de negatieve zoekopdracht voor "displayName" behoorlijk kostbaar is en niet echt nodig is (ES2015 werd meer dan vijf jaar geleden uitgebracht), hebben we besloten de ondersteuning voor de niet-standaard eigenschap fn.displayName uit V8 (en DevTools) te verwijderen .

Nu de negatieve opzoeking van "displayName" achter de rug was, werd de helft van de kosten van v8::StackFrame::GetFunctionName verwijderd. De andere helft gaat naar de algemene zoekopdracht naar de eigenschap "name" . Gelukkig hadden we al enige logica om dure zoekopdrachten van de eigenschap "name" op (onaangeroerde) Function instanties te vermijden, die we een tijdje geleden in V8 introduceerden om Function.prototype.bind() zelf sneller te maken. We hebben de nodige controles geport die ons in staat stellen om de kostbare generieke zoekopdracht in de eerste plaats over te slaan, met als resultaat dat v8::StackFrame::GetFunctionName niet meer verschijnt in de profielen die we hebben overwogen.

Conclusie

Met de bovenstaande verbeteringen hebben we de overhead van DevTools in termen van stacktraces aanzienlijk verminderd.

We weten dat er nog steeds verschillende mogelijke verbeteringen zijn - de overhead bij het gebruik MutationObserver s is bijvoorbeeld nog steeds merkbaar, zoals gerapporteerd in chromium:1077657 - maar voorlopig hebben we de belangrijkste pijnpunten aangepakt en komen we misschien nog eens terug in de toekomst. toekomst om de foutopsporingsprestaties verder te stroomlijnen.

Download de voorbeeldkanalen

Overweeg om Chrome Canary , Dev of Beta te gebruiken als uw standaard ontwikkelingsbrowser. Deze preview-kanalen geven u toegang tot de nieuwste DevTools-functies, testen geavanceerde webplatform-API's en ontdekken problemen op uw site voordat uw gebruikers dat doen!

Neem contact op met het Chrome DevTools-team

Gebruik de volgende opties om de nieuwe functies en wijzigingen in het bericht te bespreken, of iets anders gerelateerd aan DevTools.

  • Stuur ons een suggestie of feedback via crbug.com .
  • Rapporteer een DevTools-probleem met behulp van de opties MeerMeer > Help > Rapporteer een DevTools-probleem in DevTools.
  • Tweet op @ChromeDevTools .
  • Laat reacties achter op onze Wat is er nieuw in DevTools YouTube-video's of DevTools Tips YouTube-video's .