Gli sviluppatori web si aspettano un impatto minimo o nullo sulle prestazioni durante il debug del loro codice. Tuttavia, questa aspettativa non è affatto universale. Uno sviluppatore C++ non si aspetterebbe mai che una build di debug della sua applicazione raggiunga le prestazioni di produzione e, nei primi anni di Chrome, l'apertura di DevTools influiva in modo significativo sulle prestazioni della pagina.
Il fatto che questo peggioramento delle prestazioni non sia più il risultato di anni di investimenti nelle funzionalità di debug di DevTools e V8. Tuttavia, non saremo mai in grado di ridurre a zero il sovraccarico delle prestazioni di DevTools. L'impostazione di breakpoint, l'esplorazione del codice, la raccolta di tracce dello stack, l'acquisizione di una traccia del rendimento e così via influiscono sulla velocità di esecuzione in misura diversa. Dopotutto, osservare qualcosa lo cambia.
Tuttavia, l'overhead di DevTools, come per qualsiasi debugger, deve essere ragionevole. Di recente abbiamo registrato un aumento significativo del numero di segnalazioni che, in alcuni casi, indicavano che DevTools rallentava l'applicazione al punto da non essere più utilizzabile. Di seguito puoi vedere un confronto affiancato del report chromium:1069425, che illustra il sovraccarico delle prestazioni dovuto semplicemente all'apertura di DevTools.
Come puoi vedere dal video, il rallentamento è nell'ordine di 5-10 volte, il che non è chiaramente accettabile. Il primo passaggio è stato capire dove andava tutto il tempo e cosa causava questo rallentamento massiccio quando DevTools era aperto. L'utilizzo delle perf Linux sul processo del renderer di Chrome ha rivelato la seguente distribuzione del tempo complessivo di esecuzione del renderer:
Ci aspettavamo di trovare qualcosa in relazione alla raccolta delle tracce dello stack, ma non avremmo mai immaginato che circa il 90% del tempo di esecuzione complessivo fosse dedicato alla simbolizzazione dei frame dello stack. Qui per simbolizzazione si intende l'atto di risolvere i nomi delle funzioni e le posizioni di origine concrete, ovvero i numeri di riga e di colonna negli script, dagli stack frame non elaborati.
Inferenza del nome del metodo
Ciò che è stato ancora più sorprendente è il fatto che quasi tutto il tempo viene dedicato alla funzione JSStackFrame::GetMethodName()
in V8, anche se da indagini precedenti sapevamo che JSStackFrame::GetMethodName()
non è estranea ai problemi di prestazioni. Questa funzione tenta di calcolare il nome del metodo per i frame considerati chiamate di metodo (frame che rappresentano chiamate di funzione del tipo obj.func()
anziché func()
). Un'occhiata rapida al codice ha rivelato che funziona eseguendo un attraversamento completo dell'oggetto e della relativa catena di prototipi e cercando
- proprietà di dati i cui
value
sono la chiusura difunc
oppure - proprietà di accesso in cui
get
oset
è uguale alla chiusurafunc
.
Ora, anche se da solo non sembra particolarmente economico, non sembra nemmeno che possa spiegare questo terribile rallentamento. Abbiamo quindi iniziato a esaminare l'esempio segnalato in chromium:1069425 e abbiamo scoperto che le tracce dello stack sono state raccolte per le attività asincrone e per i messaggi di log provenienti da classes.js
, un file JavaScript di 10 MiB. Un'analisi più approfondita ha rivelato che si trattava fondamentalmente di un runtime Java e di un codice dell'applicazione compilato in JavaScript. Le tracce dello stack contenevano diversi frame con metodi richiamati su un oggetto A
, quindi abbiamo pensato che potesse valere la pena capire con quale tipo di oggetto abbiamo a che fare.
A quanto pare, il compilatore Java to JavaScript ha generato un singolo oggetto con ben 82.203 funzioni. Era chiaro che stava iniziando a diventare interessante. Poi siamo tornati al JSStackFrame::GetMethodName()
del V8 per capire se c'è qualche frutto in uscita da poter raccogliere.
- Il funzionamento consiste prima nel cercare il
"name"
della funzione come proprietà dell'oggetto e, se viene trovato, controlla che il valore della proprietà corrisponda alla funzione. - Se la funzione non ha un nome o l'oggetto non ha una proprietà corrispondente, viene eseguita una ricerca inversa percorrendo tutte le proprietà dell'oggetto e dei relativi prototipi.
Nel nostro esempio, tutte le funzioni sono anonime e hanno proprietà "name"
vuote.
A.SDV = function() {
// ...
};
Il primo risultato è che la ricerca inversa è stata suddivisa in due passaggi (eseguiti per l'oggetto stesso e per ogni oggetto nella relativa catena di prototipi):
- Estrai i nomi di tutte le proprietà enumerabili e
- Esegui una ricerca di proprietà generica per ogni nome, verificando se il valore della proprietà risultante corrisponde alla chiusura che cercavamo.
Sembrava un frutto abbastanza basso, dal momento che per estrarre i nomi è necessario attraversare tutte le proprietà. Anziché eseguire due passaggi, O(N) per l'estrazione del nome e O(N log(N)) per i test, potremmo fare tutto in un unico passaggio e controllare direttamente i valori delle proprietà. In questo modo, l'intera funzione è diventata 2-10 volte più veloce.
Il secondo risultato è stato ancora più interessante. Sebbene le funzioni fossero tecnicamente anonime, il motore V8 aveva comunque registrato quello che chiamiamo un nome dedotto per queste funzioni. Per i valori letterali di funzione che vengono visualizzati sul lato destro dei compiti nel formato obj.foo = function() {...}
, l'analizzatore sintattico V8 memorizza "obj.foo"
come nome dedotto per il valore letterale della funzione. Nel nostro caso, quindi, sebbene non avessimo il nome corretto che potevamo semplicemente cercare, avevamo qualcosa di abbastanza simile: per l'esempio A.SDV = function() {...}
riportato sopra, avevamo "A.SDV"
come nome dedotto e potevamo ricavare il nome della proprietà dal nome dedotto cercando l'ultimo punto e poi cercando la proprietà "SDV"
nell'oggetto. È stato così in quasi tutti i casi, sostituendo un costoso full traversal con una singola ricerca di proprietà. Questi due miglioramenti sono stati inseriti nell'ambito di questo CL e hanno ridotto in modo significativo il rallentamento per l'esempio riportato in chromium:1069425.
Error.stack
Avremmo potuto chiudere qui. Ma c'era qualcosa di strano, dato che DevTools non utilizza mai il nome del metodo per gli frame dello stack. Infatti, la classe v8::StackFrame
nell'API C++ non espone nemmeno un modo per accedere al nome del metodo. Quindi ci è sembrato sbagliato che avremmo dovuto chiamare JSStackFrame::GetMethodName()
in primo luogo. L'unico posto in cui utilizziamo (ed esponiamo) il nome del metodo è invece l'API JavaScript dell'analisi dello stack. Per comprendere questo utilizzo, considera il seguente semplice esempio error-methodname.js
:
function foo() {
console.log((new Error).stack);
}
var object = {bar: foo};
object.bar();
Qui abbiamo una funzione foo
installata con il nome "bar"
su object
. L'esecuzione di questo snippet in Chromium produce il seguente output:
Error
at Object.foo [as bar] (error-methodname.js:2)
at error-methodname.js:6
Qui vediamo la ricerca del nome del metodo: il frame dello stack più alto mostra la chiamata alla funzione foo
su un'istanza di Object
tramite il metodo denominato bar
. Pertanto, la proprietà non standard error.stack
fa un uso intensivo di JSStackFrame::GetMethodName()
e, di fatto, i nostri test di prestazioni indicano anche che le nostre modifiche hanno reso le cose notevolmente più veloci.
Tornando all'argomento di Chrome DevTools, il fatto che il nome del metodo venga calcolato anche se error.stack
non viene utilizzato non sembra corretto. Ecco un po' di storia che ci è utile: in passato, V8 aveva due meccanismi distinti per raccogliere e rappresentare una traccia dello stack per le due diverse API descritte sopra (l'API v8::StackFrame
C++ e l'API di traccia dello stack JavaScript). Avere due modi diversi per eseguire (grosso modo) la stessa operazione era soggetto a errori e spesso portava a incoerenze e bug, quindi alla fine del 2018 abbiamo avviato un progetto per stabilire un unico collo di bottiglia per l'acquisizione dell'analisi dello stack.
Il progetto ebbe un grande successo e ridusse drasticamente il numero di problemi relativi alla raccolta delle analisi dello stack. Anche la maggior parte delle informazioni fornite tramite la proprietà non standard error.stack
era stata calcolata in modo lazy e solo quando era realmente necessaria, ma nell'ambito del refactoring abbiamo applicato lo stesso trucco agli oggetti v8::StackFrame
. Tutte le informazioni sullo stack frame vengono calcolate la prima volta che viene richiamato un metodo.
In genere, questo migliora le prestazioni, ma purtroppo si è rivelato in qualche modo contrario al modo in cui questi oggetti API C++ vengono utilizzati in Chromium e DevTools. In particolare, poiché abbiamo introdotto una nuova classe v8::internal::StackFrameInfo
, che conteneva tutte le informazioni su un frame di stack esposte tramite v8::StackFrame
o error.stack
, calcolavamo sempre il superset delle informazioni fornite da entrambe le API, il che significa che per gli utilizzi di v8::StackFrame
(e in particolare per DevTools) calcolavamo anche il nome del metodo, non appena venivano richieste informazioni su un frame di stack. A quanto pare, DevTools richiede sempre immediatamente informazioni su codice sorgente e script.
In base a questa consapevolezza, siamo stati in grado di eseguire il refactoring e semplificare drasticamente la rappresentazione del frame dello stack e di renderlo ancora più lazy, in modo che gli utilizzi in V8 e Chromium ora paghino solo il costo per il calcolo delle informazioni richieste. Ciò ha dato un enorme impulso alle prestazioni di DevTools e di altri casi d'uso di Chromium, che richiedono solo una frazione delle informazioni sui frame dello stack (essenzialmente solo il nome dello script e la posizione della sorgente sotto forma di offset di riga e colonna) e ha aperto la strada a ulteriori miglioramenti delle prestazioni.
Nomi delle funzioni
Una volta eliminati i refactoring sopra menzionati, il sovraccarico della simbolizzazione (il tempo trascorso in v8_inspector::V8Debugger::symbolize
) è stato ridotto a circa il 15% del tempo di esecuzione complessivo e abbiamo potuto capire più chiaramente dove V8 stava impiegando tempo durante la raccolta e la simbolizzazione dei frame dello stack per il consumo in DevTools.
La prima cosa che mi ha colpito è stato il costo cumulativo per il calcolo del numero di riga e colonna. La parte dispendiosa è in realtà il calcolo dell'offset dei caratteri all'interno dello script (in base all'offset del bytecode che otteniamo da V8) e abbiamo scoperto che, a causa del refactoring sopra riportato, l'abbiamo fatto due volte, una volta durante il calcolo del numero di riga e un'altra volta durante il calcolo del numero di colonna. La memorizzazione nella cache della posizione dell'origine nelle istanze v8::internal::StackFrameInfo
ha contribuito a risolvere rapidamente il problema ed eliminato completamente v8::internal::StackFrameInfo::GetColumnNumber
da tutti i profili.
Il risultato più interessante per noi è stato che v8::StackFrame::GetFunctionName
era sorprendentemente alto in tutti i profili che abbiamo esaminato. Scavando più a fondo ci siamo accorti che era inutilmente costoso calcolare il nome visualizzato per la funzione nello stack frame in DevTools.
- cercando prima la proprietà
"displayName"
non standard e, se questa ha restituito una proprietà dati con un valore di stringa, la useremo, - altrimenti viene cercata la proprietà
"name"
standard e viene verificato di nuovo se restituisce una proprietà dati il cui valore è una stringa. - e infine ricorrendo a un nome di debug interno dedotto dal parser V8 e memorizzato nel valore letterale della funzione.
La proprietà "displayName"
è stata aggiunta come soluzione alternativa per la proprietà "name"
nelle istanze Function
in quanto di sola lettura e non configurabile in JavaScript, ma non è mai stata standardizzata e non ha avuto un uso diffuso, poiché gli strumenti per sviluppatori del browser hanno aggiunto l'inferenza del nome della funzione che svolge il compito nel 99,9% dei casi. Inoltre, ES2015 ha reso configurabile la proprietà "name"
nelle istanze Function
, eliminando completamente la necessità di una proprietà "displayName"
speciale. Poiché la ricerca negativa per "displayName"
è piuttosto costosa e non realmente necessaria (ES2015 è stato rilasciato più di cinque anni fa), abbiamo deciso di rimuovere il supporto per la proprietà fn.displayName
non standard da V8 (e DevTools).
Una volta rimossa la ricerca negativa di "displayName"
, metà del costo di v8::StackFrame::GetFunctionName
è stata rimossa. L'altra metà va alla ricerca generica della proprietà "name"
. Fortunatamente, avevamo già implementato una logica per evitare ricerche costose della proprietà "name"
su istanze Function
(non toccate), che abbiamo introdotto nella versione 8 un po' di tempo fa per velocizzare Function.prototype.bind()
. Abbiamo portato i controlli necessari che ci consentono di saltare la costosa ricerca generica in primo luogo, con il risultato che v8::StackFrame::GetFunctionName
non viene più visualizzato in nessuno dei profili che abbiamo preso in considerazione.
Conclusione
Grazie ai miglioramenti sopra descritti, abbiamo ridotto in modo significativo l'overhead di DevTools in termini di tracce dello stack.
Sappiamo che esistono ancora vari possibili miglioramenti, ad esempio l'overhead quando si utilizzano i MutationObserver
è ancora evidente, come riportato in chromium:1077657, ma per il momento abbiamo risolto i principali problemi e potremmo tornare in futuro per semplificare ulteriormente il rendimento del debug.
Scaricare i canali di anteprima
Prendi in considerazione l'utilizzo di Chrome Canary, Dev o Beta come browser di sviluppo predefinito. Questi canali di anteprima ti consentono di accedere alle funzionalità più recenti di DevTools, di testare API di piattaforme web all'avanguardia e di trovare i problemi sul tuo sito prima che lo facciano gli utenti.
Contatta il team di Chrome DevTools
Utilizza le seguenti opzioni per discutere di nuove funzionalità, aggiornamenti o qualsiasi altro argomento relativo a DevTools.
- Inviaci feedback e richieste di funzionalità all'indirizzo crbug.com.
- Segnala un problema di DevTools utilizzando Altre opzioni > Guida > Segnala un problema di DevTools in DevTools.
- Invia un tweet all'account @ChromeDevTools.
- Lascia commenti sulle novità nei video di YouTube di DevTools o sui video di YouTube con i suggerimenti per gli strumenti per sviluppatori.