Gestione efficace della memoria su scala Gmail

John McCutchan
John McCutchan
Loreena Lee
Loreena Lee

Introduzione

Sebbene JavaScript utilizzi la garbage collection per la gestione automatica della memoria, non sostituisce una gestione efficace della memoria nelle applicazioni. Le applicazioni JavaScript soffrono degli stessi problemi relativi alla memoria delle applicazioni native, come le perdite di memoria e il gonfiore, ma devono anche gestire le pause della garbage collection. Le applicazioni su larga scala come Gmail riscontrano gli stessi problemi che si trovano ad affrontare le applicazioni più piccole. Continua a leggere per scoprire in che modo il team di Gmail ha utilizzato Chrome DevTools per identificare, isolare e risolvere i problemi di memoria.

Sessione Google I/O 2013

Abbiamo presentato questo materiale alla conferenza Google I/O 2013. Guarda il seguente video:

Gmail, si è verificato un problema...

Il team di Gmail stava affrontando un problema grave. Gli aneddoti sulle schede di Gmail che consumavano più gigabyte di memoria su laptop e desktop con risorse limitate sono stati ascoltati con maggiore frequenza, spesso fino alla conclusione che era stato disattivato l'intero browser. Storie di CPU bloccate al 100%, app che non rispondono e schede tristi di Chrome ("È morto, Jim."). Il team era incerto sul modo in cui iniziare a diagnosticare il problema, per non parlare di come risolverlo. Non avevano idea di quanto fosse diffuso il problema e gli strumenti a disposizione non si adattavano alle applicazioni di grandi dimensioni. Il team ha unito le forze con i team di Chrome e insieme hanno sviluppato nuove tecniche per individuare i problemi di memoria, migliorato gli strumenti esistenti e attivato la raccolta dei dati della memoria sul campo. Prima di passare agli strumenti, però, vediamo le nozioni di base della gestione della memoria di JavaScript.

Nozioni di base sulla gestione della memoria

Prima di poter gestire in modo efficace la memoria in JavaScript, è necessario comprendere le nozioni di base. Questa sezione tratterà i tipi primitivi, il grafico degli oggetti e fornirà le definizioni del sovraccarico della memoria in generale e di una perdita di memoria in JavaScript. La memoria in JavaScript può essere concettualizzata come un grafico e, per questo motivo, la teoria del grafico gioca un ruolo nella gestione della memoria JavaScript e nell'heap Profiler.

Tipi primitivi

JavaScript ha tre tipi primitivi:

  1. Numero (ad es. 4, 3.14159)
  2. Booleano (vero o falso)
  3. Stringa ("Hello World")

Questi tipi primitivi non possono fare riferimento ad altri valori. Nel grafico degli oggetti questi valori sono sempre nodi foglia o di terminazione, ovvero non hanno mai un bordo in uscita.

Esiste un solo tipo di container: l'oggetto. In JavaScript, l'oggetto è un array associati. Un oggetto non vuoto è un nodo interno con bordi in uscita verso altri valori (nodi).

Cosa sono gli array?

Un array in JavaScript è un oggetto con chiavi numeriche. Questa è una semplificazione, perché i runtime JavaScript ottimizzano gli oggetti tipo array e li rappresentano in background come array.

Terminologia

  1. Valore: un'istanza di tipo primitivo, Oggetto, Array e così via.
  2. Variabile: un nome che fa riferimento a un valore.
  3. Proprietà: un nome in un oggetto che fa riferimento a un valore.

Grafico oggetto

Tutti i valori in JavaScript fanno parte del grafico degli oggetti. Il grafico inizia con le radici, ad esempio l'oggetto finestra. La gestione della durata dei certificati radice GC non è sotto il tuo controllo, in quanto vengono create dal browser ed eliminate quando la pagina viene scaricata. Le variabili globali sono in realtà proprietà della finestra.

Grafico oggetto

Quando un valore diventa spazzatura?

Un valore diventa garbage quando non c'è alcun percorso da una radice al valore. In altre parole, partendo dalle origini ed esaminando in modo esaustivo tutte le proprietà e le variabili degli oggetti attive nel frame frame, un valore non può essere raggiunto, è diventato inutili.

Grafico dei rifiuti

Che cos'è una perdita di memoria in JavaScript?

Una perdita di memoria in JavaScript si verifica più comunemente quando sono presenti nodi DOM che non sono raggiungibili dall'albero DOM della pagina, ma a cui un oggetto JavaScript fa ancora riferimento. Anche se i browser moderni stanno rendendo sempre più difficile creare inavvertitamente fughe di dati, ma è comunque più facile di quanto si possa pensare. Supponiamo che tu aggiunga un elemento all'albero DOM in questo modo:

email.message = document.createElement("div");
displayList.appendChild(email.message);

In seguito, rimuovi l'elemento dall'elenco di visualizzazione:

displayList.removeAllChildren();

Finché esiste email, l'elemento DOM a cui fa riferimento il messaggio non verrà rimosso, anche se ora è scollegato dall'albero DOM della pagina.

Che cos'è Bloat?

La pagina si escresce quando utilizzi più memoria del necessario per ottimizzare la velocità delle pagine. Indirettamente, anche le perdite di memoria causano gonfiore, ma questo non è stato progettato. Una cache dell'applicazione senza limiti di dimensione è un'origine comune di sovraccarico della memoria. Inoltre, la pagina può essere sovraccaricata di dati dell'host, ad esempio i dati dei pixel caricati dalle immagini.

Che cos'è la raccolta dei rifiuti?

La garbage collection è il modo in cui la memoria viene recuperata in JavaScript. È il browser a decidere quando eseguire questa operazione. Durante una raccolta, l'esecuzione di tutti gli script sulla pagina viene sospesa mentre i valori attivi vengono rilevati da un attraversamento del grafico degli oggetti a partire dalle radici GC. Tutti i valori che non sono raggiungibili vengono classificati come rifiuti. La memoria per i valori garbage viene recuperata dal gestore della memoria.

Garbage Collector V8 in dettaglio

Per comprendere meglio come avviene la garbage collection, diamo un'occhiata in dettaglio al garbage collection V8. V8 utilizza un raccoglitore generazionale. La memoria è divisa in due generazioni: la giovane e la vecchia. L'assegnazione e la raccolta nelle giovani generazioni sono rapide e frequenti. L'allocazione e la raccolta all'interno della vecchia generazione sono più lente e meno frequenti.

Raccoglitore generazionale

V8 utilizza un raccoglitore a due generazioni. L'età di un valore è definita come il numero di byte allocati da quando è stato allocato. In pratica, l'età di un valore è spesso approssimata in base al numero di collezioni di giovani generazioni a cui sono sopravvissute. Una volta che un valore è sufficientemente vecchio, viene mantenuto nella vecchia generazione.

In pratica, i valori appena assegnati non durano a lungo. Uno studio sui programmi Smalltalk ha dimostrato che solo il 7% dei valori sopravvive dopo una raccolta di giovani generazioni. Studi simili sui tempi di esecuzione hanno rilevato che in media il 90% e il 70% dei valori appena assegnati non sono mai mantenuti nella vecchia generazione.

Giovane generazione

L'heap della giovane generazione in V8 è diviso in due spazi, che prendono il nome da e a. La memoria viene allocata dallo spazio. L'allocazione è molto rapida, fino a quando lo spazio è pieno; a quel punto, si scatena una raccolta delle giovani generazioni. La raccolta delle giovani generazioni scambia prima lo spazio da e allo spazio, quella vecchia allo spazio (ora lo spazio) viene analizzata e tutti i valori reali vengono copiati nello spazio o conservati nella vecchia generazione. Una raccolta tipica delle nuove generazioni durerà nell'ordine di 10 millisecondi (ms).

Intuitivamente, dovresti capire che ogni allocazione effettuata dalla tua applicazione comporta l'esaurimento dello spazio e una pausa di GC. Gli sviluppatori di giochi devono prendere nota: per garantire una durata frame di 16 ms (necessaria a raggiungere 60 frame al secondo), l'applicazione non deve fare allocazioni zero, poiché una singola raccolta di giovani generazioni consuma la maggior parte del tempo di frame.

Heap di giovani generazioni

Vecchia generazione

L'heap di generazione precedente in V8 utilizza un algoritmo Mark-compact per la raccolta. Le allocazioni della generazione precedente si verificano ogni volta che un valore viene mantenuto dalla giovane generazione alla vecchia generazione. Ogni volta che si effettua una raccolta di vecchia generazione, si effettua anche una raccolta di nuova generazione. L'applicazione verrà messa in pausa nell'ordine di secondi. In pratica, questo approccio è accettabile perché le raccolte della vecchia generazione non sono frequenti.

Riepilogo GC V8

La gestione automatica della memoria con la garbage collection è ottima per la produttività degli sviluppatori, ma ogni volta che assegni un valore, ti avvicini sempre di più a una pausa della garbage collection. Le pause della garbage collection possono rovinare il design della tua applicazione grazie all'introduzione di jank. Ora che hai compreso come JavaScript gestisce la memoria, puoi fare le scelte giuste per la tua applicazione.

Correggere i problemi di Gmail

Nell'ultimo anno, Chrome DevTools ha introdotto numerose funzionalità e correzioni di bug, rendendole più potenti che mai. Inoltre, il browser stesso ha apportato una modifica chiave all'API performance.memory consentendo a Gmail e a qualsiasi altra applicazione di raccogliere statistiche sulla memoria sul campo. Grazie a questi incredibili strumenti, quello che un tempo sembrava impossibile, ora è diventato un gioco entusiasmante per rintracciare i colpevoli.

Strumenti e tecniche

API Field Data e performance.memory

A partire da Chrome 22, l'API performance.memory è attiva per impostazione predefinita. Per le applicazioni a lunga esecuzione come Gmail, i dati di utenti reali sono inestimabili. Queste informazioni ci permettono di distinguere tra utenti esperti, ovvero coloro che trascorrono 8-16 ore al giorno su Gmail, ricevendo centinaia di messaggi al giorno, da più utenti medi che trascorrono alcuni minuti al giorno in Gmail e ricevono circa una dozzina di messaggi a settimana.

Questa API restituisce tre tipi di dati:

  1. jsHeapSizeLimit - La quantità di memoria (in byte) a cui è limitato l'heap JavaScript.
  2. totalJSHeapSize: la quantità di memoria (in byte) allocata dall'heap JavaScript, incluso lo spazio libero.
  3. usedJSHeapSize: la quantità di memoria (in byte) attualmente in uso.

Una cosa da tenere presente è che l'API restituisce valori di memoria per l'intero processo di Chrome. Anche se non è la modalità predefinita, in alcuni casi Chrome potrebbe aprire più schede nello stesso processo di rendering. Ciò significa che i valori restituiti da performance.memory potrebbero contenere l'utilizzo di memoria di altre schede del browser oltre a quella contenente la tua app.

Misurazione della memoria su larga scala

Gmail ha instrumentato JavaScript per utilizzare l'API performance.memory per raccogliere informazioni sulla memoria circa una volta ogni 30 minuti. Poiché molti utenti di Gmail lasciano l'app in funzione per giorni, il team è stato in grado di monitorare la crescita della memoria nel tempo e le statistiche generali sulla memoria utilizzata. Dopo pochi giorni dall'implementazione di Gmail per raccogliere informazioni sulla memoria da un campione casuale di utenti, il team ha potuto raccogliere dati sufficienti per comprendere quanto fossero diffusi i problemi di memoria tra gli utenti medi. Hanno impostato una base di riferimento e utilizzato il flusso di dati in entrata per tenere traccia dei progressi verso l'obiettivo di ridurre il consumo di memoria. Alla fine, questi dati verranno utilizzati anche per rilevare eventuali regressioni della memoria.

Oltre a scopi di tracciamento, le misurazioni sul campo forniscono anche una visione approfondita della correlazione tra l'utilizzo della memoria e le prestazioni delle applicazioni. Contrariamente alla convinzione diffusa che "più memoria si traduce in un miglioramento delle prestazioni", il team di Gmail ha scoperto che maggiore è l'utilizzo della memoria, maggiori sono le latenze più lunghe per le azioni comuni di Gmail. Armati di questa rivelazione, sono stati più motivati che mai a limitare il consumo di memoria.

Misurazione della memoria su larga scala

Identificazione di un problema di memoria con la sequenza temporale di DevTools

Il primo passo per risolvere qualsiasi problema di prestazioni è dimostrare che il problema esiste, creare un test riproducibile ed effettuare una misurazione di base del problema. Senza un programma riproducibile, non è possibile misurare il problema in modo affidabile. Senza una misurazione di riferimento non sai di quanto hai migliorato il rendimento.

Il riquadro Spostamenti di DevTools è il candidato ideale per dimostrare l'esistenza del problema. Fornisce una panoramica completa di dove viene trascorso il tempo durante il caricamento e l'interazione con l'app web o la pagina. Tutti gli eventi, dal caricamento delle risorse all'analisi di JavaScript, al calcolo degli stili, alle pause della garbage collection e alla rivisualizzazione, sono tracciati su una sequenza temporale. Al fine di esaminare i problemi di memoria, il riquadro Spostamenti dispone anche di una modalità Memoria che tiene traccia della memoria totale allocata, del numero di nodi DOM, del numero di oggetti finestra e del numero di listener di eventi allocati.

Dimostrazione dell'esistenza di un problema

Inizia identificando una sequenza di azioni che sospetti stiano perdendo memoria. Inizia a registrare la sequenza temporale ed esegui la sequenza di azioni. Usa il pulsante del cestino in basso per forzare una garbage collection completa. Se, dopo alcune iterazioni, vedi un grafico a forma di dente di sega, significa che stai allocando molti oggetti risalenti a breve tempo. Tuttavia, se non si prevede che la sequenza di azioni produca memoria mantenuta e il numero dei nodi DOM non torna alla base di partenza in cui hai iniziato, hai una buona ragione di sospettare che si sia verificata una perdita.

Grafico a dente di sega

Dopo aver verificato che il problema esiste, puoi ricevere assistenza per identificare l'origine del problema da DevTools Heap Profiler.

Ricerca di fughe di memoria con il Profiler Heap DevTools

Il riquadro Profiler fornisce sia un profiler CPU sia un profiler heap. La profilazione heap consente di acquisire un'istantanea del grafico degli oggetti. Prima di scattare un'istantanea, sia la giovane che la vecchia generazione vengono raccolte ai rifiuti. In altre parole, vengono visualizzati solo i valori attivi al momento dello snapshot.

Il profiler heap presenta troppe funzionalità da trattare sufficientemente in questo articolo, ma la documentazione dettagliata è disponibile sul sito Chrome for Developers. Ci concentreremo sul profiler dell'allocazione dell'heap.

Utilizzo del Profiler di allocazione heap

Il profiler per l'allocazione dell'heap combina le informazioni dettagliate dell'istantanea del Profiler dell'heap con l'aggiornamento e il monitoraggio incrementali del riquadro della cronologia. Apri il riquadro Profili, avvia un profilo Registra allocazioni heap, esegui una sequenza di azioni, quindi interrompi la registrazione per l'analisi. Il profiler di allocazione acquisisce periodicamente istantanee dell'heap per tutta la registrazione (ogni 50 ms) e un'istantanea finale al termine della registrazione.

Profiler allocazione heap

Le barre in alto indicano quando vengono trovati nuovi oggetti nell'heap. L'altezza di ogni barra corrisponde alle dimensioni degli oggetti assegnati di recente e il colore delle barre indica se tali oggetti sono ancora attivi nell'istantanea heap finale: le barre blu indicano gli oggetti ancora attivi alla fine della sequenza temporale, le barre grigie indicano gli oggetti assegnati durante la sequenza temporale ma che sono stati raccolti in seguito.

Nell'esempio precedente, un'azione è stata eseguita 10 volte. Il programma di esempio memorizza nella cache cinque oggetti, quindi sono previste le ultime cinque barre blu. ma la barra blu all'estrema sinistra indica un potenziale problema. Puoi quindi utilizzare i cursori nella sequenza temporale in alto per aumentare lo zoom su quell'istantanea e vedere gli oggetti recentemente allocati in quel punto. Se fai clic su un oggetto specifico nell'heap, verrà visualizzato l'albero di conservazione nella parte inferiore dell'istantanea dell'heap. L'esame del percorso di conservazione dell'oggetto dovrebbe fornire informazioni sufficienti per capire perché l'oggetto non è stato raccolto e potrai apportare le modifiche necessarie al codice per rimuovere il riferimento non necessario.

Risolvere problemi di memoria di Gmail

Utilizzando gli strumenti e le tecniche illustrate sopra, il team di Gmail è stato in grado di identificare alcune categorie di bug: cache illimitate, array infiniti di callback in attesa di qualcosa che non accade mai e i listener di eventi conservano involontariamente i propri obiettivi. Risolvendo questi problemi, l'utilizzo complessivo della memoria di Gmail è stato ridotto drasticamente. Nel 99% degli utenti, gli utenti utilizzavano l'80% di memoria in meno rispetto a prima e il consumo medio di memoria degli utenti mediani è calato di quasi il 50%.

Utilizzo memoria di Gmail

Poiché Gmail utilizzava meno memoria, la latenza di pausa GC è stata ridotta, aumentando l'esperienza utente complessiva.

Nota anche che il team di Gmail che ha raccolto statistiche sull'utilizzo della memoria è riuscito a scoprire regressioni della garbage collection all'interno di Chrome. Nello specifico, sono stati scoperti due bug di frammentazione quando i dati della memoria di Gmail hanno iniziato a mostrare un significativo aumento del divario tra la memoria totale allocata e la memoria in tempo reale.

Invito all'azione

Ponetevi queste domande:

  1. Quanta memoria sta usando la mia app? È possibile che tu stia utilizzando troppa memoria e, in contraddizione con quanto comunemente si pensa, le prestazioni complessive dell'applicazione sono negative. È difficile sapere esattamente qual è il numero giusto, ma assicurati di verificare che l'eventuale memorizzazione nella cache aggiuntiva utilizzata dalla pagina abbia un impatto misurabile sul rendimento.
  2. La mia pagina è priva di perdite? Se la pagina ha delle perdite di memoria, ciò può influire non solo sulle prestazioni della pagina, ma anche su altre schede. Utilizza il tracker degli oggetti per identificare meglio eventuali perdite.
  3. Con quale frequenza viene eseguito il GC della mia pagina? Puoi visualizzare qualsiasi messa in pausa di GC utilizzando il riquadro Spostamenti negli Strumenti per sviluppatori di Chrome. Se la tua pagina esegue spesso il test GC, è probabile che la tua pagina venga allocata troppo spesso, abbandonando la memoria delle giovani generazioni.

Conclusione

Siamo partiti in crisi. Abbiamo esaminato le nozioni di base fondamentali della gestione della memoria in particolare in JavaScript e V8. Hai imparato a usare gli strumenti, inclusa la nuova funzione di rilevamento degli oggetti disponibile nelle ultime build di Chrome. Il team di Gmail, grazie a queste conoscenze, ha risolto il problema di utilizzo della memoria e ha registrato un miglioramento delle prestazioni. Puoi fare lo stesso con le tue app web.