170 votes

Trouver des fuites de mémoire JavaScript avec Chrome

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 pas y avoir de fuites de mémoire.

Un jsfiddle pour le code est ici : http://jsfiddle.net/4QhR2/

// tout est contenu dans une fonction
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) {        
            // fait à des fins de démonstration uniquement, devrait utiliser des templates
            this.html_text = "Supprimer";        
            this.listenTo(this,"all",function(){console.log("Événement: "+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() {
            // supposons que .remove() est appelé sur les sous-vues (si elles existaient)
            this.trigger("cleanup");
            this.remove();
        }
    });

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

main();

Cependant, je ne suis pas certain de savoir comment utiliser le profileur de Google Chrome pour vérifier que c'est bien le cas. Il y a une multitude de choses qui apparaissent dans l'instantané du profileur de la heap, et je n'ai aucune idée de comment décoder ce qui est bon/mauvais. Les tutoriels que j'ai vus à ce sujet se contentent soit de me dire d'utiliser le profileur d'instantanés, soit de me donner un manifeste extrêmement détaillé sur le fonctionnement de tout le profileur. Est-il possible d'utiliser le profileur comme un outil, ou dois-je vraiment comprendre comment l'ensemble a été conçu ?

EDIT : Des tutoriels comme ceux-ci :

Corriger une fuite de mémoire dans Gmail

Utilisation de DevTools

Sont représentatifs de certains des contenus les plus solides disponibles, d'après ce que j'ai vu. Cependant, à part introduire le 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 tutoriel 'Utilisation de DevTools' ne travaille pas sur un exemple concret, donc sa description vague et conceptuelle générale des choses n'est pas très utile. Quant à l'exemple de 'Gmail' :

Donc vous avez trouvé une fuite. Et maintenant ?

  • Examinez le chemin de rétention des objets en fuite dans la moitié inférieure du panneau Profiles

  • Si le site d'allocation ne peut pas être facilement déduit (par exemple les écouteurs d'événements) :

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

  • Utilisez Closure ? Activez le drapeau existant approprié (par exemple goog.events.Listener.ENABLE_MONITORING) pour définir la propriété creationStack pendant la construction

Je me retrouve plus confus après avoir lu cela, pas moins. Et, une fois de plus, on me dit simplement de faire des choses, sans m'expliquer comment les faire. De mon point de vue, toutes les informations disponibles sont soit trop vagues, soit ne seraient compréhensibles que par quelqu'un qui aurait déjà compris le processus.

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

2 votes

Je ne sais rien sur le test d'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.

1 votes

Merci pour la suggestion, Paul. Cependant, lorsque je prends un instantané avant de cliquer sur Supprimer, puis un autre après l'avoir cliqué, et 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 par exemple 4 entrées 'HTMLButtonElement', ce qui n'a pas de sens pour moi. Vraiment, je n'ai aucune idée de ce qui se passe.

3 votes

Doh, cela ne semble pas particulièrement utile. Il se peut qu'avec un langage à ramasse-miettes comme JavaScript, vous ne soyez pas vraiment censé vérifier ce que vous faites avec la mémoire à un niveau aussi granulaire que celui de votre test. Une meilleure façon de vérifier les fuites de mémoire pourrait être d'appeler main 10 000 fois au lieu d'une seule, et de voir si vous finissez avec beaucoup plus de mémoire utilisée à la fin.

214voto

Jonathan Naguin Points 7315

Un bon workflow pour trouver les 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.
  • Faites des choses.
  • Prenez un autre instantané du tas.
  • Répétez les mêmes choses.
  • 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) en 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 ou utilisant 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.

Maintenant vous êtes prêt à trouver des fuites de mémoire !

Vous remarquerez des noeuds de quelques couleurs différentes. Les noeuds rouges n'ont pas de références directes depuis JavaScript vers eux, mais sont vivants car ils font partie d'un arbre DOM détaché. Il peut y avoir un noeud dans l'arbre référencé depuis JavaScript (peut-être comme une fermeture ou une variable) mais empêche le garbage collection de l'ensemble de l'arbre DOM de manière fortuite.

saisir la description de l'image ici

Les noeuds jaunes ont cependant des références directes depuis JavaScript. Recherchez des noeuds jaunes dans le même arbre DOM détaché pour localiser les références depuis votre JavaScript. Il devrait y avoir une chaîne de propriétés menant de la fenêtre DOM à l'élément.

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

saisir la description de l'image ici

Sélectionnez la ligne et dans votre console tapez $0, vous verrez la fonction et son emplacement :

>$0
function cache( key, value ) {
        // Utilisez (key + " ") pour éviter une collision avec les propriétés natives des prototypes (voir l'issue #157)
        if ( keys.push( key += " " ) > Expr.cacheLength ) {
            // Ne conserver que les entrées les plus récentes
            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, il n'y a pas grand-chose que vous pouvez faire, c'est un mécanisme interne de jQuery. Mais, juste à des fins de test, allez dans 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 noeud rouge :)

Documentation:

8 votes

Je apprécie vos efforts. En effet, la technique des trois instantanés est régulièrement mentionnée dans les tutoriels. Malheureusement, les détails sont souvent omis. Par exemple, je apprécie l'introduction de la fonction $0 dans la console, qui était nouvelle pour moi - bien sûr, je n'ai aucune idée de ce que cela fait ou comment vous saviez comment l'utiliser ( $1 semble inutile tandis que $2 semble faire la même chose). Deuxièmement, comment avez-vous su mettre en évidence la ligne #button dans la fonction cache() et aucune des dizaines d'autres lignes ? Enfin, il y a des nœuds rouges dans NodeList et HTMLInputElement aussi, mais je ne peux pas les comprendre.

0 votes

@EleventyOne Le NodeList et HTMLInputElement sont des enfants du noeud HTMLDivElement, c'est pourquoi ils sont également en rouge. Deuxièmement, j'ai sélectionné la ligne cache car elle contient des informations que les autres n'ont pas, et, habituellement, je commence par un noeud ayant une faible distance par rapport à l'objet fenêtre, il est plus facile de suivre l'arbre des éléments retenus (dans ce cas, le mot cache a également aidé)

7 votes

Comment avez-vous su que la ligne de cache contenait des informations tandis que les autres ne le faisaient pas ? Il y a de nombreuses branches qui présentent une distance inférieure à celle de cache. Et je ne suis pas sûr de comment vous saviez que HTMLInputElement est un enfant de HTMLDivElement. Je le vois référencé à l'intérieur de celle-ci ("natif dans HTMLDivElement"), mais il se référence également lui-même et deux HTMLButtonElement, ce qui n'a pas de sens pour moi. Je vous suis reconnaissant d'avoir identifié la réponse pour cet exemple, mais je ne saurais pas du tout comment généraliser cela à d'autres problèmes.

8voto

ricksuggs Points 774

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

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

Je n'ai jamais réussi à comprendre comment utiliser la Timeline et le Profiler pour traquer les fuites de mémoire, jusqu'à ce que je lise la documentation suivante. Après avoir lu la section intitulée 'Suivi des allocations d'objets', j'ai pu utiliser l'outil 'Enregistrer les allocations de la Heap' et suivre quelques noeuds 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. Il est de mon entendement que les versions plus récentes de Backbone annuleront automatiquement les événements pour vous si vous appelez View.remove(). Exécutez certains des démos vous-même, ils sont configurés avec des fuites de mémoire pour que vous les identifiiez. N'hésitez pas à poser des questions ici si vous ne comprenez toujours pas après avoir étudié cette documentation.

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

6voto

Fondamentalement, 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, alors vous avez une fuite de mémoire. Mon conseil est de rechercher les gestionnaires d'événements dans votre code qui ne sont pas détachés.

3 votes

Par exemple, si je regarde un instantané du tas de la jsfiddle, avant de cliquer sur 'Supprimer', il y a beaucoup plus de 100 000 objets présents. Où chercherais-je les objets que le code de ma jsfiddle a réellement créés? Je pensais que le Window/http://jsfiddle.net/4QhR2/show pourrait être utile, mais ce ne sont que des fonctions sans fin. Je n'ai aucune idée de ce qui se passe là-dedans.

0 votes

@EleventyOne: Je n'utiliserais pas jsFiddle. Pourquoi ne pas simplement créer un fichier sur votre propre ordinateur pour tester?

1 votes

@BlueSkies J'ai créé un jsfiddle pour que les gens ici puissent travailler à partir du même code source. Néanmoins, lorsque je crée un fichier sur mon propre ordinateur pour tester, il y a toujours plus de 50 000 objets présents dans le snapshot du heap.

3voto

bennidi Points 734

Vous voudrez peut-être aussi lire :

http://addyosmani.com/blog/taming-the-unicorn-easing-javascript-memory-profiling-in-devtools/

Cela explique l'utilisation des outils de développement Chrome et donne quelques conseils étape par étape sur la manière de confirmer et localiser une fuite de mémoire en utilisant la comparaison des instantanés de la mémoire heap et les différentes vues des instantanés de la mémoire heap disponibles.

2voto

Je suis d'accord avec le conseil de prendre un instantané de la mémoire, ils sont excellents pour détecter les fuites de mémoire, chrome fait un excellent travail de capture d'instantané.

Dans mon projet de recherche pour mon diplôme, je construisais une application web interactive qui devait générer beaucoup de données construites en 'couches', dont beaucoup de ces couches seraient 'supprimées' dans l'interface utilisateur mais pour une raison quelconque la mémoire n'était pas libérée, en utilisant l'outil instantané j'ai pu déterminer que JQuery gardait une référence sur l'objet (la source était lorsque j'essayais de déclencher un événement .load() qui maintenait la référence malgré le dépassement de la portée). Avoir cette information à 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 empêchant le GC de faire son travail.

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

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X