Fuites mémoires en JavaScript : comment s'en débarrasser (2/2)

Cet article est la suite de l’article “les 4 types de fuites mémoire”
c

Le comportement peu intuitif des Garbage collectors

Bien que les Garbage collectors soient très pratiques, ils viennent avec leur lots de compromis. L’un de ces compromis étant leur non déterminisme. En d’autres termes, les Garbage collectors sont imprévisibles. Il n’est généralement pas possible de déterminer avec certitude quand une Garbage collection sera effectuée. Ce qui implique que dans certains cas, le programme utilise plus de mémoire que nécéssaire. Dans d’autres cas, on peut remarquer de courtes interruptions dans les applications sensibles. Bien que non déterministe signifie que l’on ne peut jamais être certain de quand la collecte sera effectuée, la plupart des implémentation de GC partagent un pattern commun d’effectuer la collecte durant l’allocation. Lorsqu’aucune allocation n’est faite, la plupart GS restent au repos.
Considérons le scénario suivant :

  1. Un ensemble conséquent d’allocations est effectué
  2. La plupart de ces éléments (ou tous) sont marqués comme inaccessibles (imaginez qu’on null une référence qui pointe sur un cache dont on a plus besoin)
  3. Plus aucune allocation n’est faite

Dans ce scénario, la plupart des GCs ne fera plus de collecte. Autrement dit, même s’il y a des éléments inaccessibles disponibles pour la collecte, ils ne sont pas réclamés par le collector. Ce ne sont pas des fuites à proprement parler mais le résultat est une utilisation mémoire supérieure à à la normale.
Google fournit un excellent exemple de ce comportement dans leurs JavaScript Memory Profiling docs, example #2.

Présentation des outils de Memory Profiling de Chrome

Chrome fournit un bon ensemble d’outils pour diagnostiquer l’utilisation mémoire du code JavaScript. il y a principalement deux vues liées à la mémoire: la timeline view et la profile view.

Timeline view

La timeline view est essentielle pour découvrir des paternes mémoire inhabituels dans notre code. Lorsque nous cherchons une fuite importante, les saut périodiques qui ne réduisent pas d’autant qu’ils ont grandit après la collecte doivent vous alarmer. Dans cette capture, on voit ce à quoi ressemble la croissance régulière d’un objet qui fuit. Même après la grosse collecte finale, le volume total de mémoire utilisé est plus grand à la fin qu’au début. Le nombre de Node aussi. Autant d’indices de fuites DOM quelque part dans le code.

Profiles view

C’est cette vue que vous allez passer beaucoup de temps à regarder. La profiles view vous permet d’obtenir un instantané et de comparer les instantanés de l’utilisation mémoire de votre code JavaScript.
Cela vous permet d’enregistrer les allocation dans le temps. Dans toutes les result view, il y a différents types de listes, mais les plus pertinentes pour notre tache sont la summary list et la comparaison list.
La summary view nous donne une vue d’ensemble des différents types d’objets alloués et leur taille agrégée : shallow size (la somme de tous les objets d’un type spécifique) et retained size (la shallow size plus la taille des autres objets retenus à cause de cet objet). Cela donne aussi une notion de à l’éloignement de cet objet à sa racine GC (la distance).
La comparison list nous donne la même information mais nous permet de comparer les différents snapshots. C’est particulièrement utile pour trouver les fuites.
Exemple: trouver des fuites en utilisant Chrome
Il y a principalement deux types de fuites : les fuites qui causent des augmentations périodiques d’utilisation mémoire et les fuites qui arrivent une fois et ne causent pas d’autres augmentations de la consommation mémoire. Pour des raisons évidentes, il est plus facile de trouver les fuites lorsqu’elles sont périodiques. Elles sont aussi les plus problématiques, si la mémoire augmente dans le temps, les fuites de ce type risquent de ralentir le navigateur ou risquent de provoquer l’arrêt du script. Les fuites non périodiques peuvent être facile à trouver lorsqu’elles sont suffisamment grandes pour être remarquées parmi les autres allocations. Ce n’est généralement pas le cas, donc elles passent inaperçues. Dans un sens, les fuites qui arrivent juste une fois pourraient être classés parmi les questions d’optimisation. Ceci dit, les fuites périodiques sont des bugs et doivent être réparées.
Pour illustrer, nous prenons un exemple dans la doc de Chrome. Le code est copié ci dessous :
var x = [];
function createSomeNodes() {
var div,
i = 100,
frag = document.createDocumentFragment();
for (;i > 0; i--) {
div = document.createElement("div");
div.appendChild(document.createTextNode(i + " - "+ new Date().toTimeString()));
frag.appendChild(div);
}
document.getElementById("nodes").appendChild(frag);
}
function grow() {
x.push(new Array(1000000).join('x'));
createSomeNodes();
setTimeout(grow,1000);
}
Quand grow est appelé, il va commencer par créer des noeuds DIV et les attacher au DOM. Il va aussi allouer une grande array et lui ajouter une array référencée par une variable globale. Cela va causer une augmentation régulière de la mémoire qui peut-être trouvée en utilisant les outils mentionnés plus haut.
On observe généralement un pattern d’utilisation mémoire oscillant dans les langages à base de Garbage Collection. C’est ce qui est attendu si le code tourne sur une boucle produisant les allocations, ce qui est généralement le cas. Nous allons chercher des augmentations périodiques de mémoire qui ne retombent pas aux niveaux précédents après la collecte.
Premièrement, voir si la mémoire augmente périodiquement.
La timeline View est géniale pour ça. Ouvrez l’exemple dans Chrome, ouvrez les Dev Tools, allez à la timeline, sélectionnez memory et cliquez sur le bouton record. Puis allez sur la page et cliquez sur The Button pour démarrer la fuite mémoire. Attendez un peu puis arrêtez l’enregistrement et regardez le résultat:

Fuite mémoire dans la timeline view

Cet exemple va continuer à fuiter de la mémoire à chaque seconde. Après avoir arrêté l’enregistrement, ajoutez un breakpoint dans la fonction grow pour empêcher le script de forcer Chrome à fermer la page. Il y a 2 gros indices dans cette image qui montre que nous avons de la mémoire qui fuit. Les graph des Nodes (ligne verte) et JS heap (ligne bleu). Les noeuds augmentent régulièrement et ne redescendent jamais. C’est un gros signal d’alerte.
Le JS Heap montre aussi une augmentation régulière de l’utilisation mémoire. C’est plus difficile à voir à cause des effets du Garbage Collector. Vous pouvez observer un pattern de croissance initiale de la mémoire, suivi d’une grosse réduction, suivi d’une croissance, puis un pic, suivi d’une autre réduction. La clé dans ce cas repose sur le fait qu’après chaque réduction de l’utilisation mémoire, la taille de la heap reste supérieure à celle de la fois précédente. Ce qui veut dire que bien que le garbage collector récupère avec succès beaucoup de mémoire, une partie est régulièrement perdue.
Nous sommes désormais certain qu’il y a une fuite. Trouvons la.
d

Prenez deux snapshots

Pour trouver la fuite, nous allons maintenant aller dans la section profile de Chrome Dev Tools. pour garder l’utilisation mémoire à un niveau gerable, rechargez la page avant cette étape. Nous allons utiliser la fonction Take Heap Snapshot.
Rechargez la page et prenez un Heap Snapshot juste après qu’elle ait fini de charger. Nous allons utiliser ce snapshot comme référence. Maintenant, cliquez à nouveau sur The Button, attendez quelques secondes, et prenez un nouveau snapshot. Après avoir pris le snapshot, c’est une bonne idée de mettre un breakpoint dans le script pour empêcher la fuite de consommer plus de mémoire.

Heap Snapshots

Il y a deux façon de regarder les allocations entre deux snapshots. Soit vous cliquez sur Summary et allez ensuite sur la droite et sélectionnez Objects allocated between Snapshot 1 and Snapshot 2, ou vous cliquez sur Comparison à la place de Summary. Dans les deux cas, nous verrons une liste d’objets qui ont été alloués entre les deux snapshots.
Dans ce cas, il est assez facile de trouver les fuites: elles sont grandes. Regardez le Size Delta des (string) constructor. 8MBs pour 58 nouveaux objets. Cela a l’ai suspect: les nouveaux objets sont alloués mais pas libérés et 8MBs sont consommés.
Si nous ouvrons la liste des allocations pour le (string) constructor nous allons remarquer qu’il y a quelques grosses allocations parmi plein de petites. Les grosses retiennent immédiatement notre attention. Si on sélectionne n’importe laquelle d’entre elles, on trouvera quelque chose d’intéressant avec dans la section Retainers juste en dessous.

Retainers pour l’objet sélectionné

Nous voyons que l’allocation sélectionnée fait parti d’une array. A son tour, l’array est référencée par la variable x dans l’objet global window. Cela nous donne le chemin complet de notre gros objet à sa racine no collectable (window). Nous avons trouvé notre fuite potentielle et où elle est référencée.
Jusqu’ici tout va bien. Mais notre exemple était facile : les grosses allocation comme dans l’exemple ne sont pas la norme. Par chance, notre exemple fuite aussi des noeuds DOM, qui sont plus petits. Il est facile de trouver ces noeuds en utilisant le snapshot ci dessus, mais dans les sites plus gros, les choses deviennent plus compliquées . Les version récentes de Chrome fournissent un outil supplémentaire qui est bien adapté à notre job: la fonction Record Heap Allocations.

Enregistrer les heap allocation pour trouver les fuites

Désactivez le breakpoint que vous avez défini auparavant, laissez le script tourner et revenez à la section Profile de Chrome Dev Tools. Maintenant, appuyez sur Record Heap Allocations. Pendant que l’outil fonctionne, vous remarquerez des pics bleus dans le graph du haut. ça représente des allocations. Chaque seconde, une grosse allocation est produite par le code. Laissez le tourner quelques secondes puis arrêtez le (n’oubliez pas d’ajouter un breakpoint pour empêcher Chrome de consommer plus de mémoire).
Heap allocations enregistrées.
Dans cette image, vous pouvez voir la killer feature de cet outil : sélectionner une portion de la timeline pour voir quelles allocations ont été faire pendant cette durée. Nous définissons la sélection au plus près des grands pics. Seuls trois constructor apparaissent dans la liste. L’un d’entre eux est celui qui est lié à notre grosse fuite ((string)), le suivant est lié aux allocations DOM, et le dernier est le Text constructor (le constructor des noeuds feuilles de DOM contenant du texe).
Sélectionnez un des HTMLDivElement constructors depuis la liste puis cliquez sur pick Allocation stack.
Selected element in heap allocation results
BAM ! Nous savons maintenant ou cet élément a été alloué (grow -> createSomeNodes). Si on regarde avec attention chaque pic dans le graph, on remarquera que le constructeur HTMLDivElement est beaucoup appelé. Si on retourne au Snapshot comparison view, on remarquera que ce constructeur révèle beaucoup d’allocation mais pas de suppression. En d’autres termes, il alloue régulièrement de la mémoire sans permettre au GC d’en récupérer. Ce sont tous les symptômes d’une fuite et en plus, nous savons exactement ou ces objets sont alloués (the createSomeNodes function). Maintenant, il est temps de revenir au code, l’étudier et réparer les fuites.

  • Une autre feature utile:

Dans la heap allocations result view, nous pouvons sélectionner Allocation view au lieu de Summary.
Cette vue nous donne une liste de fonctions et les allocations mémoire liée. Nous pouvons immédiatement voir grow et createSomeNodes sortir du lot. Quand on sélectionne grow, on jette un oeil sur les constructeurs des objets associés qui sont appelés par lui. Nous remarquons (string), HTMLDivElement et Text que nous savons déjà être les constructors des objets à l’origine des fuites.
La combinaison de ces outils peut grandement aider à trouver les fuites. Jouez avec, faites tourner différents profiling dans vos sites de production (dans l’idéal du code, non-minimized ou obfuscated). Regardez si vous pouvez trouver des fuites ou des objets qui sont conservés plus qu’ils ne devraient (astuce: ils sont plus dur à trouver).
Pour utiliser ces features, allez dans Dev Tools -> Settings et activez “record heap allocation stack traces”. Il est nécéssaire de faire ça avant l’enregistrement.

  • Pour aller plus loin:

Memory Management – Mozilla Developer Network
JScript Memory Leaks – Douglas Crockford (old, in relation to Internet Explorer 6 leaks)
JavaScript Memory Profiling – Chrome Developer Docs
Memory Diagnosis – Google Developers
An Interesting Kind of JavaScript Memory Leak – Meteor blog
Grokking V8 closures
Conclusion
Les fuites mémoire peuvent arriver et arrivent dans les langages garbage collected comme JavaScript. Elles peuvent vivre cachées pendant un moment et à la longue, faire des ravages. Pour cette raison, les outils de profilage mémoire sont essentiels pour trouver les fuites mémoires. Le profilage devrait faire parti des cycles de développement, particulièrement pour les application de taille moyenne à grande taille.Commencez dès maintenant pour donner à vos utilisateurs la meilleur expérience possible.
Bon débug !
Retrouvez le 1er article à ce sujet ici !
[separator type=”” size=”” icon=”star”] [actionbox color=”default” title=”” description=”JS-REPUBLIC est une société de services spécialisée dans le développement JavaScript. Nous sommes centre de formation agréé. Retrouvez toutes nos formations techniques sur notre site partenaire dédié au Training” btn_label=”Nos formations” btn_link=”http://training.ux-republic.com” btn_color=”primary” btn_size=”big” btn_icon=”star” btn_external=”1″]