Lutte contre les fuites de mémoire dans les applications Web

Lorsque nous sommes passés du développement de sites Web, dont les pages sont formées sur le serveur, à la création d'applications Web à page unique rendues sur le client, nous avons adopté certaines règles du jeu. L'un d'eux est la gestion précise des ressources sur l'appareil de l'utilisateur. Cela signifie - ne bloquez pas le flux principal, ne "tournez" pas le ventilateur de l'ordinateur portable, ne mettez pas la batterie du téléphone. Nous avons échangé une amélioration de l'interactivité des projets Web, et le fait que leur comportement devenait plus semblable au comportement des applications ordinaires, une nouvelle classe de problèmes qui n'existait pas dans le monde du rendu de serveur.



Un de ces problèmes est les fuites de mémoire. Une application d'une page mal conçue peut facilement engloutir des mégaoctets ou même des gigaoctets de mémoire. Il est capable de prendre de plus en plus de ressources même lorsqu'il se trouve tranquillement sur l'onglet d'arrière-plan. La page d'une telle application, après avoir capturé une quantité exorbitante de ressources, peut commencer à "ralentir" considérablement. De plus, le navigateur peut simplement fermer l'onglet et dire à l'utilisateur: «Quelque chose s'est mal passé».


Quelque chose a mal tourné

Bien sûr, les sites rendus sur le serveur peuvent également souffrir d'un problème de fuite de mémoire. Mais ici, nous parlons de la mémoire du serveur. Dans le même temps, il est hautement improbable que de telles applications provoquent une fuite de mémoire sur le client, car le navigateur efface la mémoire après chaque transition utilisateur entre les pages.

Le sujet des fuites de mémoire n'est pas bien couvert dans les publications de développement Web. Et malgré cela, je suis presque sûr que la plupart des applications à page unique non triviales souffrent de fuites de mémoire - à moins que les équipes qui les traitent ne disposent d'outils fiables pour détecter et résoudre ce problème. Le point ici est qu'en JavaScript, il est extrêmement facile d'allouer au hasard une certaine quantité de mémoire, puis d'oublier de libérer cette mémoire.

L'auteur de l'article, dont nous publions la traduction aujourd'hui, va partager avec les lecteurs son expérience dans la lutte contre les fuites de mémoire dans les applications web, et souhaite également donner des exemples de leur détection efficace.

Pourquoi est-il si peu écrit à ce sujet?


Tout d'abord, je veux expliquer pourquoi si peu de choses sont écrites sur les fuites de mémoire. Je suppose que vous pouvez trouver ici plusieurs raisons:

  • Manque de plaintes des utilisateurs: la plupart des utilisateurs ne sont pas occupés à surveiller de près le gestionnaire de tâches lorsqu'ils naviguent sur le Web. En règle générale, le développeur ne rencontre pas de plaintes des utilisateurs jusqu'à ce que la fuite de mémoire soit si grave qu'elle entraîne l'incapacité de fonctionner ou de ralentir l'application.
  • : Chrome - , . .
  • : .
  • : «» . , , , , -.


Les bibliothèques et les cadres modernes de développement d'applications Web, tels que React, Vue et Svelte, utilisent le modèle de composant de l'application. Dans ce modèle, la façon la plus courante de provoquer une fuite de mémoire est quelque chose comme ceci:

window.addEventListener('message', this.onMessage.bind(this));

C'est tout. C'est tout ce qu'il faut pour «équiper» un projet d'une fuite mémoire. Pour ce faire, il suffit d'appeler la méthode addEventListener d' un objet global (comme window, ou <body>, ou quelque chose de similaire), puis, lors du démontage du composant, oubliez de supprimer l'écouteur d'événements à l'aide de la méthode removeEventListener .

Mais les conséquences sont encore pires, car la fuite de l'ensemble du composant se produit. Cela est dû au fait que la méthode est this.onMessageattachée à this. Avec ce composant, une fuite de ses composants enfants se produit. Il est très probable que tous les nœuds DOM associés à ce composant fuiront. En conséquence, la situation peut devenir incontrôlable très rapidement, entraînant de très graves conséquences.

Voici comment résoudre ce problème:

//   
this.onMessage = this.onMessage.bind(this);
window.addEventListener('message', this.onMessage);
 
//   
window.removeEventListener('message', this.onMessage);

Situations dans lesquelles les fuites de mémoire se produisent le plus souvent


L'expérience me dit que les fuites de mémoire se produisent le plus souvent lors de l'utilisation des API suivantes:

  1. Méthode addEventListener. C'est là que les fuites de mémoire se produisent le plus souvent. Pour résoudre le problème, il suffit d'appeler au bon moment removeEventListener.
  2. setTimeout setInterval. , (, 30 ), , , , , clearTimeout clearInterval. , setTimeout, «» , , setInterval-. , setTimeout .
  3. API IntersectionObserver, ResizeObserver, MutationObserver . , , . . - , , , , , disconnect . , DOM , , -. -, . — <body>, document, header footer, .
  4. Promise-, , . , , — , . , , «» , . «» .then()-.
  5. Référentiels représentés par des objets globaux. Lorsque vous utilisez quelque chose comme Redux pour contrôler l'état d'une application , le magasin d'état est représenté par un objet global. Par conséquent, si vous traitez un tel stockage avec négligence, les données inutiles ne seront pas supprimées de celui-ci, ce qui entraînera une augmentation constante de sa taille.
  6. Croissance DOM infinie. Si la page implémente un défilement sans fin sans utiliser de virtualisation , cela signifie que le nombre de nœuds DOM sur cette page peut augmenter de manière illimitée.

Ci-dessus, nous avons examiné les situations dans lesquelles les fuites de mémoire se produisent le plus souvent, mais, bien sûr, il existe de nombreux autres cas qui causent le problème qui nous intéresse.

Identification des fuites de mémoire


Nous sommes maintenant passés au défi d'identifier les fuites de mémoire. Pour commencer, je ne pense pas que l'un des outils existants soit très approprié pour cela. J'ai essayé les outils d'analyse de mémoire de Firefox, j'ai essayé les outils de Edge et IE. Testé même Windows Performance Analyzer. Mais les meilleurs de ces outils restent les outils de développement Chrome. Certes, dans ces outils, il existe de nombreux "coins vifs", qui valent la peine d'être connus.

Parmi les outils fournis par le développeur Chrome, nous sommes particulièrement intéressés par le profileur à Heap snapshotpartir de l'onglet Memory, qui vous permet de créer des instantanés de tas. Il existe d'autres outils pour analyser la mémoire dans Chrome, mais je n'ai pas pu en tirer des avantages particuliers pour détecter les fuites de mémoire.


L'outil d'instantané du tas vous permet de prendre des instantanés de la mémoire du flux principal, des travailleurs Web ou des éléments iframe.

Si la fenêtre de l'outil Chrome ressemble à celle illustrée dans la figure précédente, lorsque vous cliquez sur le boutonTake snapshot, les informations sur tous les objets dans la mémoire de la machine virtuelle sélectionnée sont capturées JavaScript de la page étudiée. Cela inclut les objets référencés danswindow, les objets référencés par les rappels utilisés dans l'appelsetInterval, etc. Un instantané de la mémoire peut être perçu comme un «moment figé» du travail de l'entité enquêtée, représentant des informations sur toute la mémoire utilisée par cette entité.

Une fois la photo prise, nous passons à l'étape suivante de la recherche de fuites. Elle consiste à reproduire un scénario dans lequel, selon le développeur, une fuite de mémoire peut se produire. Par exemple, il ouvre et ferme une certaine fenêtre modale. Une fois la fenêtre similaire fermée, il est prévu que la quantité de mémoire allouée revienne au niveau qui existait avant l'ouverture de la fenêtre. Par conséquent, ils prennent une autre photo, puis la comparent avec la photo prise précédemment. En fait, la comparaison d'images est la caractéristique la plus importante qui nous intéresse Heap snapshot.


Nous prenons le premier instantané, puis nous entreprenons des actions qui peuvent provoquer une fuite de mémoire, puis nous prenons un autre instantané. S'il n'y a pas de fuite, la taille de la mémoire allouée sera égale. C'est

vrai,Heap snapshotc'est loin d'être un outil idéal. Il a certaines limites à connaître:

  1. Même si vous cliquez sur le petit bouton du panneau Memoryqui démarre la récupération de place ( Collect garbage), pour être sûr que la mémoire est vraiment effacée, vous devrez peut-être prendre plusieurs photos consécutives. J'ai généralement trois coups de feu. Ici, il convient de se concentrer sur la taille totale de chaque image - elle devrait finalement se stabiliser.
  2. -, -, iframe, , , . , JavaScript. — , , .
  3. «». .

À ce stade, si votre application est assez complexe, vous remarquerez peut-être beaucoup d'objets «qui fuient» lorsque vous comparez des instantanés. Ici, la situation est quelque peu compliquée, car ce qui peut être confondu avec une fuite de mémoire n'est pas toujours le cas. Une grande partie de ce qui est suspect est juste des processus normaux pour travailler avec des objets. La mémoire occupée par certains objets est effacée pour placer d'autres objets dans cette mémoire, quelque chose est vidé dans le cache et de sorte que la mémoire correspondante ne soit pas effacée immédiatement, etc.

Nous nous frayons un chemin à travers le bruit de l'information


J'ai trouvé que la meilleure façon de percer le bruit de l'information est de répéter les actions qui sont censées provoquer une fuite de mémoire. Par exemple, au lieu d'ouvrir et de fermer la fenêtre modale une seule fois après la capture du premier plan, cela peut être fait 7 fois. Pourquoi 7? Oui, ne serait-ce que parce que 7 est un nombre premier notable. Ensuite, vous devez prendre une deuxième photo et, en la comparant avec la première, savoir si un certain objet a «fui» 7 fois (ou 14 fois ou 21 fois).


Comparez les instantanés de tas. Veuillez noter que nous comparons l'image n ° 3 avec l'image n ° 6. Le fait est que j'ai pris trois clichés de suite pour que Chrome ait plus de sessions de collecte des ordures. De plus, notez que certains objets ont «fui» 7 fois.

Une autre astuce utile est qu'au tout début de l'étude, avant de créer la première image, effectuez la procédure une fois, pendant laquelle, comme prévu, fuite de mémoire. Ceci est particulièrement recommandé si le fractionnement de code est utilisé dans le projet. Dans un tel cas, il est très probable que lors de la première exécution de l'action suspecte, les modules JavaScript nécessaires seront chargés, ce qui affectera la quantité de mémoire allouée.

Vous pouvez maintenant vous demander pourquoi vous devez porter une attention particulière au nombre d'objets et non à la quantité totale de mémoire. Ici, nous pouvons dire que nous nous efforçons intuitivement de réduire la quantité de mémoire "qui fuit". À cet égard, vous pourriez penser que vous devriez surveiller la quantité totale de mémoire utilisée. Mais cette approche, pour une raison importante, ne nous convient pas particulièrement bien.

Si quelque chose «fuit», cela arrive parce que (en racontant Joe Armstrong ) vous avez besoin d'une banane, mais vous vous retrouvez avec une banane, le gorille qui la tient, et aussi, en plus, toute la jungle. Si nous nous concentrons sur la quantité totale de mémoire, ce sera la même chose que «mesurer» la jungle, et non la banane qui nous intéresse.


Gorille mangeant une banane.

Revenons maintenant à l'exemple ci-dessus avecaddEventListener. Une source de fuite est un écouteur d'événements qui fait référence à une fonction. Et cette fonction, à son tour, fait référence à un composant qui, éventuellement, stocke des liens vers un tas de bonnes choses comme des tableaux, des chaînes et des objets.

Si vous analysez la différence entre les images, en triant les entités selon la quantité de mémoire qu'elles occupent, cela vous permettra de voir de nombreux tableaux, lignes, objets, dont la plupart ne sont probablement pas liés à la fuite. Et après tout, nous devons trouver l'écouteur d'événements à partir duquel tout a commencé. Lui, par rapport à ce à quoi il fait référence, prend très peu de mémoire. Afin de réparer la fuite, vous devez trouver une banane, pas la jungle.

Par conséquent, si vous triez les enregistrements en fonction du nombre d'objets «divulgués», vous remarquerez 7 écouteurs d'événements. Et peut-être 7 composants, et 14 sous-composants, et peut-être quelque chose d'autre comme ça. Ce nombre 7 devrait se démarquer de la situation dans son ensemble, car il s'agit néanmoins d'un nombre plutôt notable et inhabituel. Dans ce cas, peu importe le nombre de répétitions de l'action suspecte. Lors de l'examen des images, si les soupçons sont justifiés, il sera enregistré tout autant d'objets «ayant fui». C'est ainsi que vous pouvez identifier rapidement la source d'une fuite de mémoire.

Analyse de l'arborescence des liens


L'outil de création d'instantanés offre la possibilité d'afficher des «chaînes de liens» qui vous aident à savoir quels objets sont référencés par d'autres objets. C'est ce qui permet à l'application de fonctionner. En analysant de telles «chaînes» ou «arbres» de liens, vous pouvez savoir exactement où la mémoire a été allouée pour l'objet «qui fuit».


La chaîne de liens vous permet de savoir quel objet fait référence à l'objet "qui fuit". Lors de la lecture de ces chaînes, il est nécessaire de tenir compte du fait que les objets qui y sont situés ci-dessous se réfèrent aux objets situés au-dessus.

Dans l'exemple ci-dessus, il existe une variable appeléesomeObjectréférencée dans la fermeture (context) référencée par l'écouteur d'événement. Si vous cliquez sur le lien menant au code source, un texte assez compréhensible du programme sera affiché:

class SomeObject () { /* ... */ }
 
const someObject = new SomeObject();
const onMessage = () => { /* ... */ };
window.addEventListener('message', onMessage);

Si nous comparons ce code avec la figure précédente, il s'avère que contextla figure est une fermeture onMessagequi fait référence someObject. Ceci est un exemple artificiel . Les vraies fuites de mémoire peuvent être beaucoup moins évidentes.

Il convient de noter que l'outil d'instantané de tas présente certaines limites:

  1. Si vous enregistrez un fichier d'instantané, puis le téléchargez à nouveau, les liens vers les fichiers avec du code sont perdus. Autrement dit, après avoir téléchargé un instantané, il ne sera pas possible de savoir que le code de fermeture de l'écouteur d'événements se trouve à la ligne 22 du fichier foo.js. Étant donné que ces informations sont extrêmement importantes, il est presque inutile d'enregistrer des fichiers d'instantané de tas ou, par exemple, de les transférer à quelqu'un.
  2. WeakMap, Chrome , . , , , , . WeakMap — .
  3. Chrome , . , , , , . , object, EventListener. object — , , , «» 7 .

Ceci est une description de ma stratégie de base pour identifier les fuites de mémoire. J'ai utilisé avec succès cette technique pour détecter des dizaines de fuites.

Certes, je dois dire que ce guide pour trouver les fuites de mémoire ne couvre qu'une petite partie de ce qui se passe dans la réalité. Ce n'est que le début du travail. De plus, vous devez être en mesure de gérer l'installation des points d'arrêt, la journalisation, les tests de corrections pour déterminer s'ils résolvent le problème. Et, malheureusement, tout cela, en substance, se traduit par un sérieux investissement de temps.

Analyse automatisée des fuites de mémoire


Je veux commencer cette section avec le fait que je n'ai pas pu trouver une bonne approche pour automatiser la détection des fuites de mémoire. Chrome a sa propre API performance.memory , mais pour des raisons de confidentialité, il ne vous permet pas de collecter des données suffisamment détaillées. Par conséquent, cette API ne peut pas être utilisée en production pour détecter les fuites. Le groupe de travail sur les performances Web du W3C a précédemment discuté des outils de mémoire, mais ses membres doivent encore se mettre d'accord sur une nouvelle norme conçue pour remplacer cette API.

Dans les environnements de test, vous pouvez augmenter la granularité de la sortie des données performance.memoryà l'aide de l'indicateur Chrome --enable-precise-memory-info. Les instantanés de tas peuvent toujours être créés à l'aide de la propre équipe du Chromedriver: takeHeapSnapshot . Cette équipe a les mêmes limites que nous avons déjà discutées. Il est probable que si vous utilisez cette commande, pour les raisons décrites ci-dessus, il est logique de l'appeler trois fois, puis de ne prendre que ce qui a été reçu à la suite de son dernier appel.

Étant donné que les écouteurs d'événements sont la source la plus courante de fuites de mémoire, je vais parler d'une autre technique de détection des fuites que j'utilise. Elle consiste à créer des patchs singe pour l'API addEventListeneret removeEventListenerà compter les liens pour vérifier que leur nombre revient à zéro. Voici un exemple de la façon dont cela se fait.

Dans les outils de développement Chrome, vous pouvez également utiliser l'API native getEventListeners pour savoir quels écouteurs d'événements sont associés à un élément particulier. Cette commande n'est cependant disponible que dans la barre d'outils du développeur.

Je veux ajouter que Matthias Binens m'a parlé d'une autre API d'outils Chrome utile. Ce sont des queryObjects . Avec lui, vous pouvez obtenir des informations sur tous les objets créés à l'aide d'un certain constructeur. Voici de bonnes informations sur ce sujet concernant l'automatisation de la détection des fuites de mémoire dans Puppeteer.

Sommaire


La recherche et la correction des fuites de mémoire dans les applications Web en sont encore à leurs balbutiements. Ici, j'ai parlé de certaines techniques qui, dans mon cas, fonctionnaient bien. Mais il faut reconnaître que l'application de ces techniques est encore lourde de difficultés et de temps.

Comme pour tout problème de performance, comme on dit, une pincée à l'avance vaut une livre. Peut-être que quelqu'un jugera utile de préparer les tests de synthèse appropriés plutôt que d'analyser la fuite une fois qu'elle s'est déjà produite. Et si ce n'est pas une fuite, mais plusieurs, alors l'analyse du problème peut se transformer en quelque chose comme éplucher des oignons: après qu'un problème est résolu, un autre est découvert, puis ce processus se répète (et tout ce temps, comme pour les oignons , larmes aux yeux). Les révisions de code peuvent également aider à identifier les modèles de fuite courants. Mais ceci - si vous savez - où chercher.

JavaScript est un langage qui permet de travailler en toute sécurité avec la mémoire. Par conséquent, il existe une certaine ironie dans la facilité avec laquelle les fuites de mémoire se produisent dans les applications Web. Certes, cela est dû en partie aux caractéristiques des interfaces utilisateur des périphériques. Vous devez écouter de nombreux événements: événements de souris, événements de défilement, événements de clavier. L'application de tous ces modèles peut facilement entraîner des fuites de mémoire. Mais, en veillant à ce que nos applications Web utilisent la mémoire avec parcimonie, nous pouvons augmenter leurs performances et les protéger contre les «plantages». De plus, nous démontrons ainsi le respect des limites de ressources des appareils utilisateurs.

Chers lecteurs! Avez-vous rencontré des fuites de mémoire dans vos projets Web?


All Articles