Recherche de fuites de mémoire JavaScript avec Chrome

163

J'ai créé un cas de test très simple qui crée une vue Backbone, attache un gestionnaire à un événement et instancie une classe définie par l'utilisateur. Je crois qu'en cliquant sur le bouton "Supprimer" dans cet exemple, tout sera nettoyé et il ne devrait y avoir aucune fuite de mémoire.

Un jsfiddle pour le code est ici: 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();

Cependant, je ne sais pas comment utiliser le profileur de Google Chrome pour vérifier que c'est bien le cas. Il y a des milliards de choses qui apparaissent sur l'instantané du profileur de tas, et je n'ai aucune idée de comment décoder ce qui est bon / mauvais. Les tutoriels que j'ai vus à ce sujet jusqu'à présent me disent simplement d '"utiliser le profileur d'instantané" ou me donnent un manifeste extrêmement détaillé sur le fonctionnement de l'ensemble du profileur. Est-il possible d'utiliser simplement le profileur comme un outil, ou dois-je vraiment comprendre comment tout a été conçu?

EDIT: Tutoriels comme ceux-ci:

Correction des fuites de mémoire Gmail

Utilisation de DevTools

Sont représentatifs de certains des matériaux les plus solides, d'après ce que j'ai vu. Cependant, au-delà de l'introduction du concept de la technique des 3 instantanés , je trouve qu'ils offrent très peu de connaissances pratiques (pour un débutant comme moi). Le didacticiel «Utilisation de DevTools» ne fonctionne pas à travers un exemple réel, donc sa description conceptuelle vague et générale des choses n'est pas trop utile. Quant à l'exemple "Gmail":

Vous avez donc trouvé une fuite. Maintenant quoi?

  • Examinez le chemin de rétention des objets ayant fui dans la moitié inférieure du panneau Profils

  • Si le site d'allocation ne peut pas être facilement déduit (c'est-à-dire les écouteurs d'événements):

  • Instrumenter le constructeur de l'objet de rétention via la console JS pour enregistrer la trace de pile pour les allocations

  • Utilisation de la fermeture? Activez l'indicateur existant approprié (c'est-à-dire goog.events.Listener.ENABLE_MONITORING) pour définir la propriété creationStack pendant la construction

Je me trouve plus confus après avoir lu cela, pas moins. Et, encore une fois, cela me dit simplement de faire les choses, pas comment les faire. De mon point de vue, toutes les informations disponibles sont trop vagues ou n'auraient de sens que pour quelqu'un qui a déjà compris le processus.

Certains de ces problèmes plus spécifiques ont été soulevés dans la réponse de @Jonathan Naguin ci-dessous.

EleventyOne
la source
2
Je ne sais rien sur le test de l'utilisation de la mémoire dans les navigateurs, mais au cas où vous ne l'auriez pas vu, l'article d'Addy Osmani sur l'inspecteur Web Chrome pourrait être utile.
Paul D.Waite
1
Merci pour la suggestion, Paul. Cependant, lorsque je prends un instantané avant de cliquer sur Supprimer, puis un autre après avoir cliqué, puis que je sélectionne `` objets alloués entre les instantanés 1 et 2 '' (comme suggéré dans son article), il y a encore plus de 2000 objets présents. Il y a 4 entrées 'HTMLButtonElement', par exemple, ce qui n'a aucun sens pour moi. Vraiment, je n'ai aucune idée de ce qui se passe.
EleventyOne
3
doh, cela ne semble pas particulièrement utile. Il se peut qu'avec un langage récupéré comme JavaScript, vous n'êtes pas vraiment censé vérifier ce que vous faites avec la mémoire à un niveau aussi granulaire que votre test. Une meilleure façon de vérifier les fuites de mémoire pourrait être d'appeler main10 000 fois au lieu d'une seule et voir si vous vous retrouvez avec beaucoup plus de mémoire utilisée à la fin.
Paul D.Waite
3
@ PaulD.Waite Ouais, peut-être. Mais il me semble que j'aurais encore besoin d'une analyse de niveau granulaire pour déterminer exactement quel est le problème, plutôt que de simplement pouvoir dire (ou ne pas dire): "D'accord, il y a un problème de mémoire quelque part ici". Et j'ai l'impression que je devrais être en mesure d'utiliser leur profileur à un niveau aussi granulaire ... Je ne sais pas comment :(
EleventyOne
Vous devriez jeter un oeil à youtube.com/watch?v=L3ugr9BJqIs
maja

Réponses:

205

Un bon flux de travail pour trouver des fuites de mémoire est la technique des trois instantanés , d'abord utilisée par Loreena Lee et l'équipe Gmail pour résoudre certains de leurs problèmes de mémoire. Les étapes sont, en général:

  • Prenez un instantané du tas.
  • Faire des trucs.
  • Prenez un autre instantané du tas.
  • Répétez la même chose.
  • Prenez un autre instantané du tas.
  • Filtrez les objets alloués entre les instantanés 1 et 2 dans la vue "Résumé" de l'instantané 3.

Pour votre exemple, j'ai adapté le code pour montrer ce processus (vous pouvez le trouver ici ) retardant la création de la vue Backbone jusqu'à l'événement de clic du bouton Démarrer. Maintenant:

  • Exécutez le HTML (enregistré localement à l'aide de cette adresse ) et prenez un instantané.
  • Cliquez sur Démarrer pour créer la vue.
  • Prenez un autre instantané.
  • Cliquez sur supprimer.
  • Prenez un autre instantané.
  • Filtrez les objets alloués entre les instantanés 1 et 2 dans la vue "Résumé" de l'instantané 3.

Vous êtes maintenant prêt à trouver des fuites de mémoire!

Vous remarquerez des nœuds de quelques couleurs différentes. Les nœuds rouges n'ont pas de références directes de Javascript vers eux, mais sont vivants car ils font partie d'une arborescence DOM détachée. Il peut y avoir un nœud dans l'arborescence référencé à partir de Javascript (peut-être en tant que fermeture ou variable) mais empêche par coïncidence l'ensemble de l'arbre DOM d'être récupéré.

entrez la description de l'image ici

Les nœuds jaunes ont cependant des références directes de Javascript. Recherchez les nœuds jaunes dans la même arborescence DOM détachée pour localiser les références de votre Javascript. Il devrait y avoir une chaîne de propriétés menant de la fenêtre DOM à l'élément.

Dans votre particulier, vous pouvez voir un élément HTML Div marqué en rouge. Si vous développez l'élément, vous verrez qu'il est référencé par une fonction "cache".

entrez la description de l'image ici

Sélectionnez la ligne et dans votre console tapez $ 0, vous verrez la fonction et l'emplacement réels:

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

C'est là que votre élément est référencé. Malheureusement, vous ne pouvez pas faire grand chose, c'est un mécanisme interne de jQuery. Mais, juste à des fins de test, accédez à la fonction et changez la méthode en:

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

Maintenant, si vous répétez le processus, vous ne verrez aucun nœud rouge :)

Documentation:

Jonathan Naguin
la source
8
J'apprécie ton effort. En effet, la technique des trois clichés est régulièrement évoquée dans les tutoriels. Malheureusement, les détails sont souvent laissés de côté. Par exemple, j'apprécie l'introduction de la $0fonction dans la console, c'était nouveau pour moi - bien sûr, je n'ai aucune idée de ce que cela fait ou comment vous saviez l'utiliser (cela $1semble inutile alors que $2semble faire la même chose). Deuxièmement, comment avez-vous su mettre en surbrillance la ligne #button in function cache()et aucune des dizaines d'autres lignes? Enfin, il y a des nœuds rouges dedans NodeListet HTMLInputElementaussi, mais je ne peux pas les comprendre.
EleventyOne
7
Comment saviez-vous que la cacheligne contenait des informations alors que les autres n'en contenaient pas? Il existe de nombreuses branches qui ont une distance inférieure à celle- cachelà. Et je ne sais pas comment tu savais que c'était HTMLInputElementun enfant de HTMLDivElement. Je le vois référencé à l'intérieur ("native in HTMLDivElement"), mais il se réfère également à lui-même et à deux HTMLButtonElements, ce qui n'a pas de sens pour moi. J'apprécie certainement que vous ayez identifié la réponse pour cet exemple, mais je ne saurais vraiment pas comment généraliser cela à d'autres questions.
EleventyOne
2
C'est étrange, j'utilisais votre exemple et j'ai obtenu un résultat différent de celui que vous avez fait (d'après votre capture d'écran). Néanmoins, j'apprécie grandement toute votre aide. Je pense que j'en ai assez pour le moment, et quand j'ai un exemple réel pour lequel j'ai besoin d'une aide spécifique, je vais créer une nouvelle question ici. Merci encore.
EleventyOne
2
Une explication à $ 0 peut être trouvée ici: developer.chrome.com/devtools/docs/commandline-api#0-4
Sukrit Gupta
4
Que veut Filter objects allocated between Snapshots 1 and 2 in Snapshot 3's "Summary" view.dire?
K - La toxicité du SO augmente.
8

Voici une astuce sur le profilage de la mémoire d'un jsfiddle: Utilisez l'URL suivante pour isoler votre résultat jsfiddle, cela supprime tout le framework jsfiddle et ne charge que votre résultat.

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

Je n'ai jamais pu comprendre comment utiliser la chronologie et le profileur pour détecter les fuites de mémoire, jusqu'à ce que je lis la documentation suivante. Après avoir lu la section intitulée «Object allocation tracker», j'ai pu utiliser l'outil «Record Heap Allocations» et suivre certains nœuds DOM détachés.

J'ai résolu le problème en passant de la liaison d'événements jQuery à l'utilisation de la délégation d'événements Backbone. Je crois comprendre que les nouvelles versions de Backbone dissocieront automatiquement les événements pour vous si vous appelez View.remove(). Exécutez certaines des démos vous-même, elles sont configurées avec des fuites de mémoire que vous devez identifier. N'hésitez pas à poser des questions ici si vous ne l'avez toujours pas après avoir étudié cette documentation.

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

Rick Suggs
la source
6

En gros, vous devez regarder le nombre d'objets dans votre instantané de tas. Si le nombre d'objets augmente entre deux instantanés et que vous avez supprimé des objets, vous avez une fuite de mémoire. Mon conseil est de rechercher dans votre code des gestionnaires d'événements qui ne se détachent pas.

Konstantin Dinev
la source
3
Par exemple, si je regarde un instantané de tas de jsfiddle, avant de cliquer sur «Supprimer», il y a bien plus de 100 000 objets présents. Où chercherais-je les objets que le code de mon jsfiddle a réellement créés? J'ai pensé que cela Window/http://jsfiddle.net/4QhR2/showpourrait être utile, mais ce n'est que des fonctions infinies. Je n'ai aucune idée de ce qui se passe là-dedans.
EleventyOne
@EleventyOne: Je n'utiliserais pas jsFiddle. Pourquoi ne pas simplement créer un fichier sur votre propre ordinateur pour le tester?
Blue Skies
1
@BlueSkies J'ai créé un jsfiddle pour que les gens d'ici puissent travailler à partir de la même base de code. Néanmoins, lorsque je crée un fichier sur mon propre ordinateur pour le test, il y a encore plus de 50 000 objets présents dans l'instantané du tas.
EleventyOne
@EleventyOne Un instantané de tas ne vous donne pas une idée de si vous avez une fuite de mémoire ou non. Il vous en faut au moins deux.
Konstantin Dinev
2
En effet. Je soulignais à quel point il est difficile de savoir quoi chercher quand il y a des milliers d'objets présents.
EleventyOne
3

Vous pouvez également consulter l'onglet Chronologie dans les outils de développement. Enregistrez l'utilisation de votre application et gardez un œil sur le nombre d'écouteurs de nœuds DOM et d'événements.

Si le graphique de mémoire indique effectivement une fuite de mémoire, vous pouvez utiliser le profileur pour déterminer ce qui fuit.

Robert Falkén
la source
2

J'appuie le conseil de prendre un instantané de tas, ils sont excellents pour détecter les fuites de mémoire, chrome fait un excellent travail de capture instantanée.

Dans mon projet de recherche pour mon diplôme, je construisais une application Web interactive qui devait générer beaucoup de données accumulées en `` couches '', beaucoup de ces couches seraient `` supprimées '' dans l'interface utilisateur, mais pour une raison quelconque, la mémoire ne l'était pas. étant désalloué, à l'aide de l'outil de capture instantanée, j'ai pu déterminer que JQuery avait conservé une référence sur l'objet (la source était lorsque j'essayais de déclencher un .load()événement qui gardait la référence malgré le fait que je sois hors de portée). Avoir ces informations à portée de main a sauvé mon projet à lui seul, c'est un outil très utile lorsque vous utilisez les bibliothèques d'autres personnes et que vous avez ce problème de références persistantes qui empêchent le GC de faire son travail.

EDIT: Il est également utile de planifier à l'avance les actions que vous allez effectuer pour minimiser le temps passé à prendre des instantanés, émettre des hypothèses sur ce qui pourrait causer le problème et tester chaque scénario, en créant des instantanés avant et après.

ProgrammerInProgress
la source
0

Quelques remarques importantes concernant l'identification des fuites de mémoire à l'aide des outils de développement Chrome:

1) Chrome lui-même a des fuites de mémoire pour certains éléments tels que les champs de mot de passe et de nombre. https://bugs.chromium.org/p/chromium/issues/detail?id=967438 . Évitez de les utiliser lors du débogage car ils polissent votre instantané de tas lors de la recherche d'éléments détachés.

2) Évitez d'enregistrer quoi que ce soit dans la console du navigateur. Chrome ne récupérera pas les objets écrits sur la console, ce qui affectera votre résultat. Vous pouvez supprimer la sortie en plaçant le code suivant au début de votre script / page:

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

3) Utilisez des instantanés de tas et recherchez "détacher" pour identifier les éléments DOM détachés. En survolant des objets, vous avez accès à toutes les propriétés, y compris id et externalHTML, qui peuvent aider à identifier chaque élément. Capture d'écran de JS Heap Snapshot avec des détails sur l'élément DOM détaché Si les éléments détachés sont encore trop génériques pour être reconnus, attribuez-leur des ID uniques à l'aide de la console du navigateur avant d'exécuter votre test, par exemple:

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

Maintenant, lorsque vous identifiez un élément détaché avec, disons id = "AutoId_49", rechargez votre page, exécutez à nouveau l'extrait de code ci-dessus et recherchez l'élément avec id = "AutoId_49" en utilisant l'inspecteur DOM ou document.querySelector (..) . Naturellement, cela ne fonctionne que si le contenu de votre page est prévisible.

Comment j'exécute mes tests pour identifier les fuites de mémoire

1) Charger la page (avec la sortie de la console supprimée!)

2) Faites des choses sur la page qui pourraient entraîner des fuites de mémoire

3) Utilisez les outils de développement pour prendre un instantané du tas et rechercher «détacher»

4) Survolez les éléments pour les identifier à partir de leurs propriétés id ou externalHTML

Jimmy Thomsen
la source
De plus, c'est toujours une bonne idée de désactiver la minification / uglification car cela rend le débogage dans le navigateur plus difficile.
Jimmy Thomsen le