Trovare perdite di memoria JavaScript con Chrome


163

Ho creato un test case molto semplice che crea una vista Backbone, collega un gestore a un evento e crea un'istanza di una classe definita dall'utente. Credo che facendo clic sul pulsante "Rimuovi" in questo esempio, tutto verrà ripulito e non dovrebbero esserci perdite di memoria.

Un jsfiddle per il codice è qui: http://jsfiddle.net/4QhR2/

// scope everything to a function
function main() {

    function MyWrapper() {
        this.element = null;
    }
    MyWrapper.prototype.set = function(elem) {
        this.element = elem;
    }
    MyWrapper.prototype.get = function() {
        return this.element;
    }

    var MyView = Backbone.View.extend({
        tagName : "div",
        id : "view",
        events : {
            "click #button" : "onButton",
        },    
        initialize : function(options) {        
            // done for demo purposes only, should be using templates
            this.html_text = "<input type='text' id='textbox' /><button id='button'>Remove</button>";        
            this.listenTo(this,"all",function(){console.log("Event: "+arguments[0]);});
        },
        render : function() {        
            this.$el.html(this.html_text);

            this.wrapper = new MyWrapper();
            this.wrapper.set(this.$("#textbox"));
            this.wrapper.get().val("placeholder");

            return this;
        },
        onButton : function() {
            // assume this gets .remove() called on subviews (if they existed)
            this.trigger("cleanup");
            this.remove();
        }
    });

    var view = new MyView();
    $("#content").append(view.render().el);
}

main();

Tuttavia, non sono chiaro come utilizzare il profiler di Google Chrome per verificare che ciò avvenga effettivamente. Ci sono un sacco di cose che appaiono nell'istantanea del profilatore heap e non ho idea di come decodificare ciò che è buono / cattivo. I tutorial che ho visto finora o semplicemente mi dicono di "usare il profiler di snapshot" o di darmi un manifesto estremamente dettagliato su come funziona l'intero profiler. È possibile utilizzare semplicemente il profiler come strumento o devo davvero capire come è stata progettata l'intera faccenda?

EDIT: tutorial come questi:

Correzione della perdita di memoria di Gmail

Utilizzo di DevTools

Sono rappresentativi di alcuni dei materiali più forti là fuori, da quello che ho visto. Tuttavia, oltre a introdurre il concetto di 3 Snapshot Technique , trovo che offrano molto poco in termini di conoscenza pratica (per un principiante come me). Il tutorial 'Uso di DevTools' non funziona attraverso un esempio reale, quindi la sua descrizione concettuale vaga e generale delle cose non è eccessivamente utile. Come nell'esempio "Gmail":

Quindi hai trovato una perdita. E adesso?

  • Esamina il percorso di mantenimento degli oggetti trapelati nella metà inferiore del pannello Profili

  • Se il sito di allocazione non può essere dedotto facilmente (ad esempio listener di eventi):

  • Strumentare il costruttore dell'oggetto di mantenimento tramite la console JS per salvare la traccia dello stack per le allocazioni

  • Usando la chiusura? Abilitare il flag esistente appropriato (ovvero goog.events.Listener.ENABLE_MONITORING) per impostare la proprietà creationStack durante la costruzione

Mi trovo più confuso dopo averlo letto, non meno. E, ancora una volta, mi sta solo dicendo di fare le cose, non come farle. Dal mio punto di vista, tutte le informazioni là fuori sono o troppo vaghe o avrebbero senso solo per qualcuno che ha già capito il processo.

Alcune di queste questioni più specifiche sono state sollevate nella risposta di @Jonathan Naguin di seguito.


2
Non so nulla sul test dell'utilizzo della memoria nei browser, ma nel caso in cui non l'avessi visto, l'articolo di Addy Osmani sull'ispettore web di Chrome potrebbe essere utile.
Paul D. Waite,

1
Grazie per il suggerimento, Paul. Tuttavia, quando eseguo un'istantanea prima di fare clic su Rimuovi, quindi un'altra dopo che è stata selezionata, quindi seleziono "oggetti allocati tra le istantanee 1 e 2" (come suggerito nel suo articolo), sono ancora presenti oltre 2000 oggetti. Ci sono 4 voci "HTMLButtonElement", per esempio, il che non ha senso per me. Davvero, non ho idea di cosa stia succedendo.
EleventyOne,

3
doh, non sembra particolarmente utile. Potrebbe essere che con un linguaggio spazzatura come JavaScript, non sei davvero intenzionato a verificare cosa stai facendo con la memoria a un livello granulare come il tuo test. Un modo migliore per verificare la presenza di perdite di memoria potrebbe essere quello di chiamare main10.000 volte anziché una volta e vedere se alla fine si ottiene molta più memoria in uso.
Paul D. Waite,

3
@ PaulD.Waite Sì, forse. Ma mi sembra che avrei ancora bisogno di un'analisi di livello granulare per determinare esattamente quale sia il problema, piuttosto che essere in grado di dire (o non dire): "Va bene, c'è un problema di memoria da qualche parte qui". E ho l'impressione che dovrei essere in grado di usare il loro profiler a un livello così granulare ... Non sono sicuro di come :(
EleventyOne,

Dovresti dare un'occhiata a youtube.com/watch?v=L3ugr9BJqIs
maja

Risposte:


205

Un buon flusso di lavoro per trovare perdite di memoria è la tecnica delle tre istantanee , utilizzata per la prima volta da Loreena Lee e dal team di Gmail per risolvere alcuni dei loro problemi di memoria. I passaggi sono, in generale:

  • Scatta un'istantanea dell'heap.
  • Fare cose.
  • Scatta un'altra istantanea dell'heap.
  • Ripeti le stesse cose.
  • Scatta un'altra istantanea dell'heap.
  • Filtra gli oggetti allocati tra le istantanee 1 e 2 nella vista "Riepilogo" dell'istantanea 3.

Per il tuo esempio, ho adattato il codice per mostrare questo processo (puoi trovarlo qui ) ritardando la creazione della vista Backbone fino all'evento click del pulsante Start. Adesso:

  • Esegui l'HTML (salvato localmente utilizzando questo indirizzo ) e scatta un'istantanea.
  • Fai clic su Avvia per creare la vista.
  • Scatta un'altra istantanea.
  • Fai clic su Rimuovi.
  • Scatta un'altra istantanea.
  • Filtra gli oggetti allocati tra le istantanee 1 e 2 nella vista "Riepilogo" dell'istantanea 3.

Ora sei pronto per trovare perdite di memoria!

Noterai nodi di alcuni colori diversi. I nodi rossi non hanno riferimenti diretti da Javascript a loro, ma sono vivi perché fanno parte di un albero DOM distaccato. Potrebbe esserci un nodo nell'albero a cui fa riferimento Javascript (forse come una chiusura o una variabile), ma per coincidenza impedisce che l'intero albero DOM venga raccolto.

inserisci qui la descrizione dell'immagine

I nodi gialli tuttavia hanno riferimenti diretti da Javascript. Cerca nodi gialli nello stesso albero DOM distaccato per individuare riferimenti dal tuo Javascript. Dovrebbe esserci una catena di proprietà che conduce dalla finestra DOM all'elemento.

Nel tuo particolare puoi vedere un elemento Div HTML contrassegnato come rosso. Se espandi l'elemento, vedrai che fa riferimento a una funzione "cache".

inserisci qui la descrizione dell'immagine

Seleziona la riga e nel tipo di console $ 0, vedrai la funzione e la posizione effettive:

>$0
function cache( key, value ) {
        // Use (key + " ") to avoid collision with native prototype properties (see Issue #157)
        if ( keys.push( key += " " ) > Expr.cacheLength ) {
            // Only keep the most recent entries
            delete cache[ keys.shift() ];
        }
        return (cache[ key ] = value);
    }                                                     jquery-2.0.2.js:1166

Qui è dove viene fatto riferimento al tuo elemento. Purtroppo non c'è molto da fare, è un meccanismo interno di jQuery. Ma, solo a scopo di test, vai alla funzione e modifica il metodo in:

function cache( key, value ) {
    return value;
}

Ora se ripeti il ​​processo non vedrai alcun nodo rosso :)

Documentazione:


8
Apprezzo il tuo sforzo. In effetti, la tecnica delle tre istantanee è regolarmente menzionata nei tutorial. Sfortunatamente, i dettagli sono spesso esclusi. Ad esempio, apprezzo l'introduzione della $0funzione nella console, che era una novità per me - ovviamente, non ho idea di cosa stia facendo o di come sapevi usarla ( $1sembra inutile mentre $2sembra fare la stessa cosa). In secondo luogo, come hai saputo evidenziare la riga #button in function cache()e non nessuna delle altre dozzine di righe? Infine, ci sono i nodi rossi in NodeListe HTMLInputElementanche, ma non li riesco a capire.
EleventyOne,

7
Come sapevi che la cacheriga conteneva informazioni mentre le altre no? Esistono numerosi rami che hanno una distanza inferiore a quella cache. E non sono sicuro di come tu sapessi che HTMLInputElementè figlio di HTMLDivElement. Lo vedo referenziato al suo interno ("native in HTMLDivElement"), ma fa anche riferimento a se stesso e due HTMLButtonElements, il che non ha senso per me. Apprezzo certamente che tu abbia identificato la risposta per questo esempio, ma non avrei davvero idea di come generalizzare questo ad altri problemi.
EleventyOne,

2
È strano, stavo usando il tuo esempio e ho ottenuto un risultato diverso da te (dal tuo screenshot). Tuttavia, apprezzo molto tutto il tuo aiuto. Penso di avere abbastanza per ora, e quando avrò un esempio di vita reale di cui ho bisogno di un aiuto specifico, creerò una nuova domanda qui. Grazie ancora.
EleventyOne,

2
Spiegazione a $ 0 può essere trovata qui: developer.chrome.com/devtools/docs/commandline-api#0-4
Sukrit Gupta

4
Cosa Filter objects allocated between Snapshots 1 and 2 in Snapshot 3's "Summary" view.significa?
K - La tossicità in SO sta crescendo.

8

Ecco un suggerimento sulla creazione di profili di memoria di un jsfiddle: utilizzare il seguente URL per isolare il risultato jsfiddle, rimuove tutto il framework jsfiddle e carica solo il risultato.

http://jsfiddle.net/4QhR2/show/

Non sono mai stato in grado di capire come utilizzare la linea temporale e il profiler per rintracciare le perdite di memoria, fino a quando non ho letto la seguente documentazione. Dopo aver letto la sezione intitolata "Tracker allocazione oggetti" sono stato in grado di utilizzare lo strumento "Registra allocazioni heap" e tenere traccia di alcuni nodi DOM distaccati.

Ho risolto il problema passando dall'associazione di eventi jQuery all'utilizzo della delega di eventi Backbone. Comprendo che le nuove versioni di Backbone separeranno automaticamente gli eventi per te, se chiami View.remove(). Esegui tu stesso alcune demo, sono configurate con perdite di memoria che puoi identificare. Sentiti libero di fare domande qui se ancora non lo capisci dopo aver studiato questa documentazione.

https://developers.google.com/chrome-developer-tools/docs/javascript-memory-profiling


6

Fondamentalmente è necessario esaminare il numero di oggetti all'interno dell'istantanea dell'heap. Se il numero di oggetti aumenta tra due istantanee e hai eliminato gli oggetti, allora hai una perdita di memoria. Il mio consiglio è di cercare gestori di eventi nel tuo codice che non si staccino.


3
Ad esempio, se guardo un'istantanea heap del jsfiddle, prima di fare clic su "Rimuovi", sono presenti oltre 100.000 oggetti. Dove dovrei cercare gli oggetti che il mio codice jsfiddle ha effettivamente creato? Ho pensato che Window/http://jsfiddle.net/4QhR2/showpotesse essere utile, ma sono solo infinite funzioni. Non ho idea di cosa stia succedendo lì dentro.
EleventyOne,

@EleventyOne: non userei jsFiddle. Perché non creare un file sul proprio computer per il test?
Blue Skies,

1
@BlueSkies Ho creato un jsfiddle in modo che le persone qui possano lavorare dalla stessa base di codice. Tuttavia, quando creo un file sul mio computer per il test, nell'istantanea dell'heap sono presenti oltre 50.000 oggetti.
EleventyOne,

@EleventyOne Un'istantanea heap non ti dà un'idea se hai una perdita di memoria o meno. Ne hai bisogno di almeno due.
Konstantin Dinev,

2
Infatti. Stavo sottolineando quanto sia difficile sapere cosa cercare quando sono presenti migliaia di oggetti.
EleventyOne,


3

Puoi anche guardare la scheda Timeline negli strumenti di sviluppo. Registra l'utilizzo della tua app e tieni d'occhio il nodo DOM e il conteggio dei listener di eventi.

Se il grafico della memoria indica effettivamente una perdita di memoria, è possibile utilizzare il profiler per capire cosa sta perdendo.



2

Seguo il consiglio di fare un'istantanea dell'heap, sono eccellenti per rilevare perdite di memoria, Chrome fa un ottimo lavoro di snapshot.

Nel mio progetto di ricerca per la mia laurea stavo costruendo un'applicazione web interattiva che doveva generare molti dati accumulati in "livelli", molti di questi livelli sarebbero stati "cancellati" nell'interfaccia utente ma per qualche ragione la memoria non era essendo stato deallocato, usando lo strumento snapshot sono stato in grado di determinare che JQuery aveva mantenuto un riferimento sull'oggetto (la fonte era quando stavo cercando di innescare un .load()evento che manteneva il riferimento nonostante fosse uscito dall'ambito). Avere queste informazioni a portata di mano ha salvato da solo il mio progetto, è uno strumento molto utile quando usi le librerie di altre persone e hai questo problema di riferimenti persistenti che impediscono al GC di fare il suo lavoro.

EDIT: è anche utile pianificare in anticipo quali azioni eseguirai per ridurre al minimo il tempo impiegato per lo snapshot, ipotizzare cosa potrebbe causare il problema e testare ogni scenario, realizzando snapshot prima e dopo.


0

Un paio di note importanti per quanto riguarda l'identificazione delle perdite di memoria utilizzando gli strumenti per sviluppatori di Chrome:

1) Chrome stesso ha perdite di memoria per alcuni elementi come i campi password e numeri. https://bugs.chromium.org/p/chromium/issues/detail?id=967438 . Evita di usarli mentre esegui il debug mentre inquinano l'istantanea dell'heap durante la ricerca di elementi separati.

2) Evitare di registrare nulla sulla console del browser. Chrome non raccoglierà inutilmente gli oggetti scritti sulla console, influendo quindi sul risultato. Puoi sopprimere l'output posizionando il seguente codice all'inizio del tuo script / pagina:

console.log = function() {};
console.warn = console.log;
console.error = console.log;

3) Utilizzare gli snapshot dell'heap e cercare "staccare" per identificare elementi DOM distaccati. Passando il mouse sugli oggetti, si ottiene l'accesso a tutte le proprietà tra cui id e outerHTML che possono aiutare a identificare ogni elemento. Schermata di JS Heap Snapshot con dettagli sull'elemento DOM distaccato Se gli elementi staccati sono ancora troppo generici per essere riconosciuti, assegnare loro ID univoci utilizzando la console del browser prima di eseguire il test, ad esempio:

var divs = document.querySelectorAll("div");
for (var i = 0 ; i < divs.length ; i++)
{
    divs[i].id = divs[i].id || "AutoId_" + i;
}
divs = null; // Free memory

Ora, quando identifichi un elemento distaccato con, diciamo id = "AutoId_49", ricarica la tua pagina, esegui di nuovo lo snippet sopra e trova l'elemento con id = "AutoId_49" usando l'ispettore DOM o document.querySelector (..) . Naturalmente questo funziona solo se il contenuto della tua pagina è prevedibile.

Come eseguo i miei test per identificare le perdite di memoria

1) Carica pagina (con uscita console soppressa!)

2) Fare cose sulla pagina che potrebbero causare perdite di memoria

3) Utilizza gli Strumenti per gli sviluppatori per creare un'istantanea dell'heap e cercare "staccare"

4) Passa il mouse sugli elementi per identificarli dalle loro proprietà id o outerHTML


Inoltre, è sempre una buona idea disabilitare la minimizzazione / ugualizzazione poiché rende più difficile il debug nel browser.
Jimmy Thomsen,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.