Gérer efficacement la mémoire à l'échelle de Gmail

John McCutchan
John McCutchan
Loreena Lee
Loreena Lee

Introduction

Bien que JavaScript utilise la récupération de mémoire pour la gestion automatique de la mémoire, il ne remplace pas une gestion efficace de la mémoire dans les applications. Les applications JavaScript présentent les mêmes problèmes de mémoire que les applications natives, tels que les fuites et les données volumineuses, mais elles doivent également gérer les mises en pause de la récupération de mémoire. Les applications à grande échelle telles que Gmail rencontrent les mêmes problèmes que les applications plus petites. Poursuivez votre lecture pour découvrir comment l'équipe Gmail a utilisé les outils pour les développeurs Chrome pour identifier, isoler et résoudre ses problèmes de mémoire.

Session Google I/O 2013

Nous avons présenté ces ressources lors de la conférence Google I/O 2013. Regardez la vidéo ci-dessous:

Gmail, nous avons un problème...

L'équipe Gmail rencontrait un problème sérieux. De plus en plus souvent, on entendait parler d'onglets Gmail consommant plusieurs gigaoctets de mémoire sur des ordinateurs portables et de bureau à ressources limitées, souvent avec l'intention de ne plus utiliser l'intégralité du navigateur. Histoires de processeurs épinglés à 100%, d'applications qui ne répondent pas et d'onglets Chrome tristes ("Il est mort, Jim"). L'équipe ne savait pas comment commencer à diagnostiquer le problème, et encore moins à le résoudre. Elle n'avait aucune idée de l'étendue du problème et les outils disponibles n'étaient pas adaptés aux applications volumineuses. L'équipe a collaboré avec les équipes Chrome pour développer de nouvelles techniques permettant de trier les problèmes de mémoire, d'améliorer les outils existants et de permettre la collecte de données de mémoire sur le terrain. Avant d'aborder ces outils, examinons les bases de la gestion de la mémoire JavaScript.

Principes de base de la gestion de la mémoire

Avant de pouvoir gérer efficacement la mémoire en JavaScript, vous devez comprendre les principes de base. Cette section aborde les types primitifs et le graphique d'objets. Elle fournit également des définitions des problèmes de mémoire gonflée en général et des fuites de mémoire en JavaScript. La mémoire en JavaScript peut être conceptualisée sous forme de graphique. De ce fait, la théorie des graphes joue un rôle dans la gestion de la mémoire JavaScript et dans le Profileur de tas de mémoire.

Types primitifs

JavaScript comporte trois types primitifs:

  1. Nombre (ex. : 4, 3.14159)
  2. Booléen (vrai ou faux)
  3. Chaîne ("Hello World")

Ces types primitifs ne peuvent faire référence à aucune autre valeur. Dans le graphe d'objet, ces valeurs sont toujours des nœuds feuilles ou de terminaison, ce qui signifie qu'elles n'ont jamais d'arête sortante.

Il n'existe qu'un seul type de conteneur: l'objet. En JavaScript, l'objet est un tableau associatif. Un objet non vide est un nœud interne avec des arêtes sortantes vers d'autres valeurs (nœuds).

Qu'en est-il des tableaux ?

Dans JavaScript, un tableau est en fait un objet possédant des clés numériques. Il s'agit d'une simplification, car les environnements d'exécution JavaScript optimisent les objets de type Array et les représentent en arrière-plan sous forme de tableaux.

Terminologie

  1. Valeur : instance d'un type primitif, d'un objet, d'un tableau, etc.
  2. Variable : nom qui fait référence à une valeur.
  3. Propriété : nom dans un objet qui fait référence à une valeur.

Graphique sur les objets

Toutes les valeurs en JavaScript font partie du graphique d'objets. Le graphique commence par les racines, par exemple l'objet window. Vous ne pouvez pas gérer la durée de vie des racines de récupération de mémoire, car elles sont créées par le navigateur et détruites lorsque la page est déchargée. Les variables globales sont en réalité des propriétés de la fenêtre.

Graphique sur un objet

À quel moment une valeur devient-elle vide ?

Une valeur devient instable lorsqu'il n'y a pas de chemin entre la racine et la valeur. En d'autres termes, en commençant par la racine et en recherchant de manière exhaustive toutes les propriétés et variables d'objet actives dans le bloc de pile, une valeur ne peut pas être atteinte, elle est devenue obsolète.

Graphique de mémoire

Qu'est-ce qu'une fuite de mémoire en JavaScript ?

Une fuite de mémoire en JavaScript se produit le plus souvent lorsque des nœuds DOM ne sont pas accessibles à partir de l'arborescence DOM de la page, mais qui sont tout de même référencés par un objet JavaScript. Même si les navigateurs récents rendent de plus en plus difficile les fuites accidentelles, ce sont encore plus faciles qu'il n'y paraît. Imaginons que vous ajoutiez un élément à l'arborescence DOM comme suit:

email.message = document.createElement("div");
displayList.appendChild(email.message);

Vous supprimez ensuite l'élément de la liste d'affichage:

displayList.removeAllChildren();

Tant que email existe, l'élément DOM référencé par le message n'est pas supprimé, même s'il est désormais dissocié de l'arborescence DOM de la page.

Qu'est-ce que Ballat ?

Votre page est gonflée lorsque vous utilisez plus de mémoire que nécessaire pour optimiser sa vitesse. Indirectement, les fuites de mémoire provoquent également des surcharges, mais ce n'est pas par intention. Un cache d'application dont la taille n'est pas limitée est une source courante de surcharge de mémoire. Votre page peut également être surchargée en raison des données de l'hôte (données de pixels chargées à partir d'images, par exemple).

Qu'est-ce que la récupération de mémoire ?

La récupération de mémoire correspond à la récupération de mémoire en JavaScript. Le navigateur décide quand cela se produit. Au cours d'une collecte, toutes les exécutions de scripts de votre page sont suspendues, tandis que les valeurs actives sont détectées par un balayage du graphe d'objets à partir du répertoire racine de récupération de mémoire. Toutes les valeurs qui ne sont pas accessibles sont classées comme indésirables. La mémoire pour les valeurs de mémoire est récupérée par le gestionnaire de mémoire.

Détails du récupérateur de mémoire V8

Pour mieux comprendre le fonctionnement de la récupération de mémoire, examinons en détail le récupérateur de mémoire V8. V8 utilise un collecteur générationnel. La mémoire est divisée en deux générations: la plus jeune et la plus âgée. Chez la jeune génération, l'allocation et la collecte sont rapides et fréquentes. L'allocation et la collecte au sein de l'ancienne génération sont plus lentes et moins fréquentes.

Collecteur générationnel

V8 utilise un collecteur de deux générations. L'ancienneté d'une valeur est définie comme le nombre d'octets alloués depuis qu'elle a été allouée. En pratique, l'âge d'une valeur est souvent estimé par le nombre de collections de jeunes générations dans lesquelles elle a survécu. Une fois qu'une valeur est suffisamment ancienne, elle est cédée à l'ancienne génération.

En pratique, les valeurs nouvellement allouées ne durent pas longtemps. Une étude des programmes Smalltalk a montré que seulement 7% des valeurs subsistent après une collecte auprès d'une jeune génération. Des études similaires portant sur plusieurs environnements d'exécution ont montré qu'en moyenne, entre 90% et 70% des valeurs nouvellement allouées ne sont jamais affectées à l'ancienne génération.

Jeune génération

Dans V8, le tas de mémoire des jeunes générations est divisé en deux espaces nommés "from" et "to". La mémoire est allouée à partir de l'espace. L'allocation est très rapide, jusqu'à ce que l'espace soit complet, puis une collecte d'images jeune est déclenchée. La collection de la jeune génération échange d'abord l'ancien vers l'espace, l'ancien vers l'espace (désormais l'espace depuis l'espace) et toutes les valeurs actives sont copiées dans l'espace ou cédées à l'ancienne génération. La durée d'une collecte type "jeune génération" prend environ 10 millisecondes (ms).

Intuitivement, vous devez comprendre que chaque allocation de votre application vous rapproche de l'épuisement de l'espace et entraîne une pause de récupération de mémoire. Remarque aux développeurs de jeux: pour garantir un temps de rendu de 16 ms (requis pour atteindre 60 images par seconde), votre application ne doit effectuer aucune allocation, car une seule collection de jeunes générations consomme la majeure partie du temps de rendu.

Tas de mémoire de nouvelle génération

Ancienne génération

Le tas de mémoire de l'ancienne génération dans V8 utilise un algorithme Mark-Compact pour la collecte. Les allocations d'ancienne génération ont lieu chaque fois qu'une valeur est cédée de la jeune génération à l'ancienne. À chaque fois qu'une collection d'ancienne génération est créée, une collection jeune est également présente. Votre application sera mise en pause après quelques secondes. En pratique, cela est acceptable, car les collections d'ancienne génération sont peu fréquentes.

Récapitulatif de la récupération de mémoire V8

La gestion automatique de la mémoire avec la récupération de mémoire est idéale pour la productivité des développeurs, mais chaque fois que vous attribuez une valeur, vous vous rapprochez d'une suspension de la récupération de mémoire. Les suspensions de la récupération de mémoire peuvent altérer l'apparence de votre application en introduisant des à-coups. Maintenant que vous comprenez comment JavaScript gère la mémoire, vous pouvez faire les bons choix pour votre application.

Corriger les problèmes dans Gmail

Au cours de l'année écoulée, de nombreuses fonctionnalités et corrections de bugs ont été intégrées aux outils pour les développeurs Chrome, ce qui les rend plus puissants que jamais. De plus, le navigateur lui-même a apporté une modification clé à l'API performance.memory permettant à Gmail et à toute autre application de collecter des statistiques sur la mémoire à partir du champ. Grâce à ces incroyables outils, ce qui semblait auparavant impossible est vite devenu un jeu passionnant pour traquer les coupables.

Outils et techniques

Données de champ et API performance.memory

Depuis Chrome 22, l'API performance.memory est activée par défaut. Pour les applications de longue durée telles que Gmail, les données d'utilisateurs réels sont inestimables. Ces informations nous permettent de distinguer les utilisateurs expérimentés (ceux qui passent entre 8 et 16 heures par jour sur Gmail et qui reçoivent des centaines de messages par jour) des utilisateurs moyens qui passent quelques minutes par jour sur Gmail pour recevoir une dizaine de messages par semaine.

Cette API renvoie trois types de données:

  1. jsHeapSizeLimit - Quantité de mémoire (en octets) à laquelle le tas de mémoire JavaScript est limité.
  2. totalJSHeapSize - Quantité de mémoire (en octets) allouée par le tas de mémoire JavaScript, y compris l'espace libre.
  3. useJSHeapSize : quantité de mémoire (en octets) actuellement utilisée.

N'oubliez pas que l'API renvoie des valeurs de mémoire pour l'ensemble du processus Chrome. Bien qu'il ne s'agisse pas du mode par défaut, Chrome peut, dans certaines circonstances, ouvrir plusieurs onglets au cours du même processus de moteur de rendu. Cela signifie que les valeurs renvoyées par performance.memory peuvent contenir l'espace mémoire utilisé par d'autres onglets du navigateur, en plus de celui contenant votre application.

Mesurer la mémoire à grande échelle

Gmail a instrumenté son code JavaScript pour utiliser l'API performance.memory afin de collecter des informations sur la mémoire environ toutes les 30 minutes. Étant donné que de nombreux utilisateurs de Gmail laissent l'application en service plusieurs jours d'affilée, l'équipe a pu suivre l'augmentation de la mémoire au fil du temps ainsi que les statistiques globales d'utilisation de la mémoire. Quelques jours après avoir utilisé Gmail pour collecter des informations sur la mémoire à partir d'un échantillonnage aléatoire d'utilisateurs, l'équipe disposait de suffisamment de données pour comprendre l'étendue des problèmes de mémoire parmi les utilisateurs moyens. Elle a défini une référence et utilisé le flux de données entrantes pour suivre la progression vers l'objectif de réduction de la consommation de mémoire. À terme, ces données seront également utilisées pour détecter les régressions de mémoire.

Au-delà du suivi, les mesures sur le terrain fournissent un bon éclairage sur la corrélation entre l'espace mémoire utilisé et les performances des applications. Contrairement à la croyance selon laquelle "plus il y a de mémoire pour améliorer les performances", l'équipe Gmail a constaté que plus l'encombrement de la mémoire était important, plus les temps de latence étaient importants pour les actions courantes dans Gmail. Forts de cette révélation, ils étaient plus enclins que jamais à maîtriser leur consommation de mémoire.

Mesurer la mémoire à grande échelle

Identifier un problème de mémoire dans la chronologie des outils de développement

La première étape de la résolution d'un problème de performance consiste à prouver que le problème existe, à créer un test reproductible et à effectuer une mesure de référence du problème. Sans programme reproductible, vous ne pouvez pas mesurer le problème de manière fiable. Sans mesure de référence, vous ne savez pas dans quelle mesure vous avez amélioré vos performances.

Le panneau "Timeline" des outils de développement est l'outil idéal pour prouver l'existence du problème. Elle offre un aperçu complet du temps passé à charger votre application ou votre page Web et à interagir avec elle. Tous les événements, du chargement des ressources à l'analyse du code JavaScript, en passant par le calcul des styles, les pauses de récupération de mémoire et le repeinture, sont représentés sur une chronologie. Afin d'examiner les problèmes de mémoire, le panneau "Timeline" dispose également d'un mode Mémoire qui suit la mémoire totale allouée, le nombre de nœuds DOM, le nombre d'objets de fenêtre et le nombre d'écouteurs d'événements alloués.

Prouver qu'un problème existe

Commencez par identifier une séquence d'actions qui, selon vous, provoquent une fuite de mémoire. Commencez à enregistrer la chronologie et effectuez la séquence d'actions. Utilisez le bouton de la corbeille en bas pour forcer une récupération complète de la mémoire. Si, après quelques itérations, vous voyez un graphique en forme de dents de scie, vous allouez de nombreux objets ayant une durée de vie rapide. Toutefois, si la séquence d'actions ne devrait pas entraîner de retenue de mémoire et que le nombre de nœuds DOM ne retombe pas à la valeur de référence où vous avez commencé, vous avez de bonnes raisons de penser qu'il y a une fuite.

Graphique en forme de dents de scie

Après avoir confirmé l'existence du problème, vous pouvez obtenir de l'aide pour identifier sa source dans le Profileur de tas de mémoire des outils de développement.

Rechercher des fuites de mémoire à l'aide du Profileur de tas de mémoire des outils de développement

Le panneau "Profiler" fournit à la fois un profileur de processeur et un profileur de tas de mémoire. Le profilage de tas de mémoire s'effectue en prenant un instantané du graphique d'objet. Avant la prise d'un instantané, les générations jeune et ancienne font l'objet d'une récupération de mémoire. En d'autres termes, vous ne verrez que les valeurs actives au moment de la prise de l'instantané.

Le profileur de tas de mémoire contient trop de fonctionnalités pour être traitées suffisamment dans cet article, mais une documentation détaillée est disponible sur le site des développeurs Chrome. Nous allons nous concentrer ici sur le profileur d'allocation de tas de mémoire.

Utiliser le profileur d'allocation de tas de mémoire

Le profileur d'allocation de tas de mémoire associe les informations détaillées de l'instantané du profileur de segments de mémoire à la mise à jour et au suivi incrémentiels du panneau "Timeline". Ouvrez le panneau "Profiles", démarrez un profil Record Heap Allocations (Enregistrer des allocations de tas de mémoire), effectuez une séquence d'actions, puis arrêtez l'enregistrement pour analyse. Le profileur d'allocation prend des instantanés de tas de mémoire régulièrement tout au long de l'enregistrement (à une fréquence de 50 ms) et un instantané final à la fin de l'enregistrement.

Profileur d'allocation de segments de mémoire

Les barres situées dans la partie supérieure indiquent les nouveaux objets détectés dans le tas de mémoire. La hauteur de chaque barre correspond à la taille des objets récemment alloués, et la couleur des barres indique si ces objets sont toujours actifs ou non dans l'instantané du tas de mémoire final: les barres bleues indiquent les objets toujours actifs à la fin de la chronologie, les barres grises indiquent les objets qui ont été alloués pendant la chronologie, mais qui ont depuis été récupérés.

Dans l'exemple ci-dessus, une action a été effectuée 10 fois. L'exemple de programme met en cache cinq objets. Les cinq dernières barres bleues sont donc attendues. Mais la barre bleue la plus à gauche indique un problème potentiel. Vous pouvez ensuite utiliser les curseurs de la timeline ci-dessus pour effectuer un zoom avant sur cet instantané et voir les objets qui ont été récemment alloués à ce moment-là. Cliquez sur un objet spécifique dans le tas de mémoire pour afficher son arborescence de conservation dans la partie inférieure de l'instantané du tas de mémoire. L'examen du chemin d'accès à l'objet devrait vous donner suffisamment d'informations pour comprendre pourquoi l'objet n'a pas été collecté. Vous pourrez également apporter les modifications de code nécessaires pour supprimer la référence inutile.

Résoudre un problème de mémoire dans Gmail

À l'aide des outils et techniques décrits ci-dessus, l'équipe Gmail a pu identifier quelques catégories de bugs: caches illimités, nombre croissant de rappels en attente d'événements qui ne se produisent jamais réellement et écouteur d'événements conservant involontairement leurs cibles. La résolution de ces problèmes a permis de réduire considérablement l'utilisation globale de la mémoire de Gmail. Dans ces 99% de cas, les utilisateurs ont utilisé 80% de mémoire en moins qu'auparavant et la consommation de mémoire des utilisateurs médians a chuté de près de 50%.

Utilisation de la mémoire Gmail

Gmail utilisant moins de mémoire, la latence de la récupération de mémoire a été réduite, ce qui améliore l'expérience utilisateur globale.

Notez également qu'en collectant des statistiques sur l'utilisation de la mémoire, l'équipe Gmail a pu détecter des régressions de récupération de mémoire dans Chrome. Plus précisément, deux bugs de fragmentation ont été découverts lorsque les données de mémoire de Gmail ont commencé à montrer une augmentation spectaculaire de l'écart entre la mémoire totale allouée et la mémoire active.

Incitation à l'action

Posez-vous les questions suivantes:

  1. Quelle quantité de mémoire mon application utilise-t-elle ? Il est possible que vous utilisiez trop de mémoire, ce qui, contrairement à la croyance courante, a un impact négatif net sur les performances globales des applications. Il est difficile de savoir exactement quel est le nombre exact, mais veillez à vérifier que toute mise en cache supplémentaire utilisée par votre page a un impact mesurable sur les performances.
  2. Ma page est-elle sans fuite ? Si votre page présente des fuites de mémoire, cela peut non seulement avoir un impact sur les performances de votre page, mais aussi sur celles des autres onglets. Utilisez l'outil de suivi des objets pour identifier les fuites.
  3. À quelle fréquence ma page récupère-t-elle du contenu ? Toutes les pauses de récupération de mémoire peuvent s'afficher dans le panneau de la chronologie des outils pour les développeurs Chrome. Si votre page utilise fréquemment des récupérations de mémoire, il est probable que ce soit trop souvent, ce qui épuise votre mémoire de jeune génération.

Conclusion

Nous avons commencé dans une crise. Nous avons abordé les bases de la gestion de la mémoire dans JavaScript et V8 en particulier. Vous avez appris à utiliser ces outils, y compris la nouvelle fonctionnalité de suivi des objets disponible dans les dernières versions de Chrome. Grâce à ces connaissances, l'équipe Gmail a résolu son problème d'utilisation de la mémoire et constaté une amélioration des performances. Vous pouvez faire de même avec vos applications Web !