Finden von JavaScript-Speicherlecks mit Chrome

163

Ich habe einen sehr einfachen Testfall erstellt, der eine Backbone-Ansicht erstellt, einen Handler an ein Ereignis anfügt und eine benutzerdefinierte Klasse instanziiert. Ich glaube, dass durch Klicken auf die Schaltfläche "Entfernen" in diesem Beispiel alles bereinigt wird und keine Speicherlecks auftreten sollten.

Eine jsfiddle für den Code finden Sie hier: 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();

Ich bin mir jedoch nicht sicher, wie ich mit dem Profiler von Google Chrome überprüfen soll, ob dies tatsächlich der Fall ist. Es gibt eine Unmenge Dinge, die im Snapshot des Heap-Profilers angezeigt werden, und ich habe keine Ahnung, wie ich entschlüsseln kann, was gut / schlecht ist. In den Tutorials, die ich bisher gesehen habe, werde ich entweder nur aufgefordert, "den Snapshot-Profiler zu verwenden" oder mir ein äußerst detailliertes Manifest zur Funktionsweise des gesamten Profilers zu geben. Ist es möglich, den Profiler nur als Werkzeug zu verwenden, oder muss ich wirklich verstehen, wie das Ganze entwickelt wurde?

BEARBEITEN: Tutorials wie diese:

Behebung von Google Mail-Speicherlecks

Verwenden von DevTools

Sind repräsentativ für einige der stärkeren Materialien da draußen, von dem, was ich gesehen habe. Abgesehen von der Einführung des Konzepts der 3-Schnappschuss-Technik bieten sie jedoch nur sehr wenig praktisches Wissen (für einen Anfänger wie mich). Das Tutorial "Verwenden von DevTools" funktioniert nicht anhand eines realen Beispiels, daher ist die vage und allgemeine konzeptionelle Beschreibung der Dinge nicht besonders hilfreich. Wie für das Beispiel "Google Mail":

Sie haben also ein Leck gefunden. Was jetzt?

  • Untersuchen Sie den Haltepfad von durchgesickerten Objekten in der unteren Hälfte des Bedienfelds „Profile“

  • Wenn die Zuweisungsstelle nicht leicht abgeleitet werden kann (dh Ereignis-Listener):

  • Instrumentieren Sie den Konstruktor des Aufbewahrungsobjekts über die JS-Konsole, um die Stapelverfolgung für Zuordnungen zu speichern

  • Verwenden Sie Closure? Aktivieren Sie das entsprechende vorhandene Flag (z. B. goog.events.Listener.ENABLE_MONITORING), um die Eigenschaft createdStack während der Erstellung festzulegen

Nachdem ich das gelesen habe, bin ich verwirrter, nicht weniger. Und wieder sagt es mir nur, dass ich Dinge tun soll , nicht wie ich sie tun soll. Aus meiner Sicht sind alle Informationen entweder zu vage oder würden nur für jemanden Sinn machen, der den Prozess bereits verstanden hat.

Einige dieser spezifischeren Probleme wurden in der Antwort von @Jonathan Naguin unten angesprochen .

EleventyOne
quelle
2
Ich weiß nichts über das Testen der Speichernutzung in Browsern, aber falls Sie es nicht gesehen haben, der Artikel von Addy Osmani über den Chrome Web Inspector hilfreich sein.
Paul D. Waite
1
Danke für den Vorschlag, Paul. Wenn ich jedoch einen Schnappschuss mache, bevor ich auf Entfernen klicke, und dann einen anderen, nachdem ich darauf geklickt habe, und dann 'Objekte zwischen den Schnappschüssen 1 und 2' auswähle (wie in seinem Artikel vorgeschlagen), sind immer noch über 2000 Objekte vorhanden. Es gibt zum Beispiel 4 'HTMLButtonElement'-Einträge, was für mich keinen Sinn ergibt. Ich habe wirklich keine Ahnung, was los ist.
EleventyOne
3
doh, das klingt nicht besonders hilfreich. Es kann sein, dass Sie mit einer durch Müll gesammelten Sprache wie JavaScript nicht wirklich dazu gedacht sind, zu überprüfen, was Sie mit dem Speicher auf einer Ebene tun, die so detailliert ist wie Ihr Test. Eine bessere Möglichkeit, nach Speicherlecks zu suchen, besteht darin, main10.000 statt einmal aufzurufen und zu prüfen , ob am Ende viel mehr Speicher verwendet wird.
Paul D. Waite
3
@ PaulD.Waite Ja, vielleicht. Aber es scheint mir, dass ich immer noch eine detaillierte Analyse benötige, um genau zu bestimmen, um was es sich handelt, anstatt nur sagen (oder nicht sagen) zu können: "Okay, hier gibt es irgendwo ein Speicherproblem." Und ich habe den Eindruck, dass ich in der Lage sein sollte, ihren Profiler auf einer so
detaillierten
Sie sollten einen Blick auf youtube.com/watch?v=L3ugr9BJqIs
maja

Antworten:

205

Ein guter Workflow zum Auffinden von Speicherlecks ist die Drei-Schnappschuss- Technik, die zuerst von Loreena Lee und dem Google Mail-Team verwendet wurde, um einige ihrer Speicherprobleme zu lösen. Die Schritte sind im Allgemeinen:

  • Machen Sie einen Heap-Schnappschuss.
  • Sachen machen.
  • Machen Sie einen weiteren Heap-Schnappschuss.
  • Wiederholen Sie das gleiche Zeug.
  • Machen Sie einen weiteren Heap-Schnappschuss.
  • Filtern Sie Objekte, die zwischen Snapshots 1 und 2 in der Ansicht "Zusammenfassung" von Snapshot 3 zugeordnet sind.

Für Ihr Beispiel habe ich den Code angepasst, um diesen Prozess zu zeigen (Sie finden ihn hier ) und die Erstellung der Backbone-Ansicht bis zum Klickereignis der Schaltfläche Start verzögert. Jetzt:

  • Führen Sie den HTML-Code aus (lokal gespeichert, um diesen zu verwenden Adresse ) und machen Sie einen Schnappschuss.
  • Klicken Sie auf Start, um die Ansicht zu erstellen.
  • Machen Sie einen weiteren Schnappschuss.
  • Klicken Sie auf Entfernen.
  • Machen Sie einen weiteren Schnappschuss.
  • Filtern Sie Objekte, die zwischen Snapshots 1 und 2 in der Ansicht "Zusammenfassung" von Snapshot 3 zugeordnet sind.

Jetzt sind Sie bereit, Speicherlecks zu finden!

Sie werden Knoten in einigen verschiedenen Farben bemerken. Rote Knoten haben keine direkten Verweise von Javascript auf sie, sind aber lebendig, weil sie Teil eines getrennten DOM-Baums sind. Es kann einen Knoten in dem Baum geben, auf den aus Javascript verwiesen wird (möglicherweise als Abschluss oder Variable), der jedoch zufällig verhindert, dass der gesamte DOM-Baum durch Müll gesammelt wird.

Geben Sie hier die Bildbeschreibung ein

Gelbe Knoten haben jedoch direkte Verweise aus Javascript. Suchen Sie im gleichen getrennten DOM-Baum nach gelben Knoten, um Referenzen aus Ihrem Javascript zu finden. Es sollte eine Eigenschaftskette geben, die vom DOM-Fenster zum Element führt.

In Ihrem speziellen Bereich sehen Sie ein HTML Div-Element, das als rot markiert ist. Wenn Sie das Element erweitern, sehen Sie, dass es von einer "Cache" -Funktion referenziert wird.

Geben Sie hier die Bildbeschreibung ein

Wählen Sie die Zeile aus und in Ihrem Konsolentyp $ 0 sehen Sie die tatsächliche Funktion und Position:

>$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

Hier wird auf Ihr Element verwiesen. Leider können Sie nicht viel tun, es ist ein interner Mechanismus von jQuery. Gehen Sie jedoch nur zu Testzwecken in die Funktion und ändern Sie die Methode in:

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

Wenn Sie den Vorgang wiederholen, sehen Sie keinen roten Knoten :)

Dokumentation:

Jonathan Naguin
quelle
8
Ich schätze deine Bemühungen. In der Tat wird die Drei-Schnappschuss-Technik regelmäßig in Tutorials erwähnt. Leider werden die Details oft ausgelassen. Ich schätze zum Beispiel die Einführung der $0Funktion in der Konsole, die für mich neu war - natürlich habe ich keine Ahnung, was das tut oder woher Sie wissen, dass Sie es verwenden ( $1scheint nutzlos zu sein, während $2es dasselbe zu tun scheint). Zweitens, woher wussten Sie, dass Sie die Zeile #button in function cache()und nicht eine der anderen Dutzenden von Zeilen hervorheben sollten ? Schließlich gibt es rote Knoten in NodeListund HTMLInputElementauch, aber ich kann sie nicht herausfinden.
EleventyOne
7
Woher wussten Sie, dass die cacheZeile Informationen enthielt, während die anderen dies nicht taten? Es gibt zahlreiche Zweige, die einen geringeren Abstand haben als der cacheeine. Und ich bin mir nicht sicher, woher du wusstest, dass das HTMLInputElementein Kind von ist HTMLDivElement. Ich sehe, dass darin darauf verwiesen wird ("native in HTMLDivElement"), aber es verweist auch auf sich selbst und zwei HTMLButtonElements, was für mich keinen Sinn ergibt. Ich würde es sicherlich begrüßen, wenn Sie die Antwort für dieses Beispiel identifizieren würden, aber ich hätte wirklich keine Ahnung, wie ich dies auf andere Probleme verallgemeinern könnte.
EleventyOne
2
Das ist seltsam, ich habe Ihr Beispiel verwendet und ein anderes Ergebnis erzielt als Sie (aus Ihrem Screenshot). Trotzdem schätze ich Ihre Hilfe sehr. Ich glaube, ich habe jetzt genug, und wenn ich ein reales Beispiel habe, bei dem ich spezielle Hilfe benötige, werde ich hier eine neue Frage stellen. Danke noch einmal.
EleventyOne
2
Eine Erklärung zu $ ​​0 finden Sie hier: developer.chrome.com/devtools/docs/commandline-api#0-4
Sukrit Gupta
4
Was heißt Filter objects allocated between Snapshots 1 and 2 in Snapshot 3's "Summary" view.das
K - Die Toxizität in SO nimmt zu.
8

Hier ist ein Tipp zur Speicherprofilerstellung einer jsfiddle: Verwenden Sie die folgende URL, um Ihr jsfiddle-Ergebnis zu isolieren. Es entfernt das gesamte jsfiddle-Framework und lädt nur Ihr Ergebnis.

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

Ich konnte nie herausfinden, wie ich mithilfe der Zeitleiste und des Profilers Speicherlecks aufspüren kann, bis ich die folgende Dokumentation gelesen habe. Nachdem ich den Abschnitt mit dem Titel "Objektzuordnungs-Tracker" gelesen hatte, konnte ich das Tool "Heap-Zuordnungen aufzeichnen" verwenden und einige getrennte DOM-Knoten verfolgen.

Ich habe das Problem behoben, indem ich von der jQuery-Ereignisbindung zur Verwendung der Backbone-Ereignisdelegierung gewechselt bin. Nach meinem Verständnis werden neuere Versionen von Backbone die Ereignisse automatisch aufheben, wenn Sie anrufen View.remove(). Führen Sie einige der Demos selbst aus. Sie sind mit Speicherlecks ausgestattet, die Sie identifizieren können. Sie können hier gerne Fragen stellen, wenn Sie diese nach dem Studium dieser Dokumentation immer noch nicht erhalten.

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

Rick Suggs
quelle
6

Grundsätzlich müssen Sie sich die Anzahl der Objekte in Ihrem Heap-Snapshot ansehen. Wenn die Anzahl der Objekte zwischen zwei Schnappschüssen zunimmt und Sie Objekte entsorgt haben, liegt ein Speicherverlust vor. Mein Rat ist, in Ihrem Code nach Ereignishandlern zu suchen, die nicht getrennt werden.

Konstantin Dinev
quelle
3
Wenn ich mir zum Beispiel einen Heap-Schnappschuss der jsfiddle ansehe, bevor ich auf "Entfernen" klicke, sind weit mehr als 100.000 Objekte vorhanden. Wo würde ich nach den Objekten suchen, die der Code meiner jsfiddle tatsächlich erstellt hat? Ich dachte, das Window/http://jsfiddle.net/4QhR2/showkönnte nützlich sein, aber es sind nur endlose Funktionen. Ich habe keine Ahnung, was da drin los ist.
EleventyOne
@EleventyOne: Ich würde jsFiddle nicht verwenden. Warum nicht einfach eine Datei zum Testen auf Ihrem eigenen Computer erstellen?
Blue Skies
1
@BlueSkies Ich habe eine jsfiddle erstellt, damit die Leute hier mit derselben Codebasis arbeiten können. Wenn ich jedoch zum Testen eine Datei auf meinem eigenen Computer erstelle, sind immer noch mehr als 50.000 Objekte im Heap-Snapshot vorhanden.
EleventyOne
@EleventyOne Ein Heap-Snapshot gibt Ihnen keine Vorstellung davon, ob Sie einen Speicherverlust haben oder nicht. Du brauchst mindestens zwei.
Konstantin Dinev
2
Tatsächlich. Ich habe hervorgehoben, wie schwierig es ist, zu wissen, wonach zu suchen ist, wenn Tausende von Objekten vorhanden sind.
EleventyOne
3

Sie können auch die Registerkarte Zeitleiste in den Entwicklertools anzeigen. Notieren Sie die Nutzung Ihrer App und behalten Sie die Anzahl der DOM-Knoten und Ereignis-Listener im Auge.

Wenn das Speicherdiagramm tatsächlich einen Speicherverlust anzeigen würde, können Sie den Profiler verwenden, um herauszufinden, was undicht ist.

Robert Falkén
quelle
2

Ich stimme dem Rat zu, einen Heap-Schnappschuss zu machen. Sie sind hervorragend zum Erkennen von Speicherlecks geeignet. Chrome leistet hervorragende Arbeit beim Schnappschuss.

In meinem Forschungsprojekt für meinen Abschluss habe ich eine interaktive Webanwendung erstellt, die viele Daten generieren musste, die in "Ebenen" aufgebaut waren. Viele dieser Ebenen wurden in der Benutzeroberfläche "gelöscht", aber aus irgendeinem Grund war der Speicher nicht vorhanden Durch die Freigabe konnte ich mithilfe des Snapshot-Tools feststellen, dass JQuery eine Referenz auf das Objekt gespeichert hatte (die Quelle war, als ich versuchte, ein .load()Ereignis auszulösen, bei dem die Referenz beibehalten wurde, obwohl der Gültigkeitsbereich überschritten wurde). Wenn diese Informationen im Alleingang zur Verfügung stehen und mein Projekt gespeichert ist, ist dies ein äußerst nützliches Tool, wenn Sie die Bibliotheken anderer Personen verwenden und das Problem bestehender Referenzen besteht, die den GC daran hindern, seine Arbeit zu erledigen.

BEARBEITEN: Es ist auch nützlich, im Voraus zu planen, welche Aktionen Sie ausführen werden, um den Zeitaufwand für das Aufnehmen von Schnappschüssen zu minimieren, Hypothesen aufzustellen, was das Problem verursachen könnte, und jedes Szenario zu testen und vorher und nachher Schnappschüsse zu erstellen.

ProgrammerInProgress
quelle
0

Einige wichtige Hinweise zum Erkennen von Speicherlecks mithilfe von Chrome Developer-Tools:

1) Chrome selbst weist Speicherlecks für bestimmte Elemente wie Kennwort- und Nummernfelder auf. https://bugs.chromium.org/p/chromium/issues/detail?id=967438 . Vermeiden Sie es, diese beim Debuggen zu verwenden, da sie Ihren Heap-Snapshot bei der Suche nach getrennten Elementen beschädigen.

2) Vermeiden Sie es, etwas in der Browserkonsole zu protokollieren . Chrome sammelt keine Objekte, die in die Konsole geschrieben wurden, und wirkt sich somit auf Ihr Ergebnis aus. Sie können die Ausgabe unterdrücken, indem Sie den folgenden Code am Anfang Ihres Skripts / Ihrer Seite einfügen:

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

3) Verwenden Sie Heap-Snapshots und suchen Sie nach "Trennen", um getrennte DOM-Elemente zu identifizieren. Durch Bewegen des Mauszeigers erhalten Sie Zugriff auf alle Eigenschaften, einschließlich ID und OuterHTML, mit deren Hilfe Sie jedes Element identifizieren können. Screenshot von JS Heap Snapshot mit Details zum getrennten DOM-Element Wenn die getrennten Elemente immer noch zu allgemein sind, um sie zu erkennen, weisen Sie ihnen vor dem Ausführen des Tests über die Browserkonsole eindeutige IDs zu, z.

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

Wenn Sie nun ein getrenntes Element mit identifizieren, sagen wir id = "AutoId_49", laden Sie Ihre Seite neu, führen Sie das obige Snippet erneut aus und suchen Sie das Element mit id = "AutoId_49" mithilfe des DOM-Inspektors oder document.querySelector (..) . Dies funktioniert natürlich nur, wenn Ihr Seiteninhalt vorhersehbar ist.

Wie ich meine Tests durchführe, um Speicherlecks zu identifizieren

1) Seite laden (mit unterdrückter Konsolenausgabe!)

2) Machen Sie Dinge auf der Seite, die zu Speicherlecks führen können

3) Verwenden Sie die Entwicklertools, um einen Heap-Snapshot zu erstellen und nach "Trennen" zu suchen.

4) Hover Elemente sie von ihrer identifizieren ID oder Outerhtml Eigenschaften

Jimmy Thomsen
quelle
Außerdem ist es immer eine gute Idee, das Minimieren / Uglifizieren zu deaktivieren, da dies das Debuggen im Browser erschwert.
Jimmy Thomsen