JavaScript de mémoire statique avec pools d'objets

Introduction

Vous recevez donc un e-mail vous indiquant les mauvaises performances de votre jeu ou application Web au bout d'un certain temps. Vous parcourez votre code, vous ne voyez rien de particulier tant que vous n'ouvrez pas les outils de performances de la mémoire de Chrome et voyez ceci:

Un instantané de votre chronologie de mémoire

L'un de vos collègues glousse, car il se rend compte que votre problème de performances est lié à la mémoire.

Dans la vue graphique de la mémoire, ce modèle en dents de scie indique clairement un problème de performances potentiellement critique. À mesure que votre utilisation de la mémoire augmente, vous constaterez que la zone du graphique s'accroît également dans la capture chronologique. Lorsque le graphique baisse soudainement, il s'agit d'une instance où le récupérateur de mémoire s'est exécuté et a nettoyé vos objets mémoire référencés.

Que signifient les dents de scie ?

Dans un graphique comme celui-ci, vous pouvez constater que de nombreux événements de récupération de mémoire sont en cours, ce qui peut nuire aux performances de vos applications Web. Cet article explique comment contrôler l'utilisation de la mémoire afin de réduire l'impact sur vos performances.

Coûts liés à la récupération de mémoire et aux performances

Le modèle de mémoire de JavaScript repose sur une technologie appelée collecteur de mémoire. Dans de nombreux langages, le programmeur est directement responsable de l'allocation et de la libération de la mémoire du tas de mémoire du système. En revanche, un système de récupération de mémoire gère cette tâche pour le compte du programmeur, ce qui signifie que les objets ne sont pas directement libérés de la mémoire lorsque le programmeur le déréférence, mais qu'il est plus tard lorsque les méthodes heuristiques de récupération de mémoire jugent utile. Ce processus de décision nécessite que la récupération de mémoire exécute une analyse statistique sur les objets actifs et inactifs, ce qui prend un certain temps.

La récupération de mémoire est souvent décrite comme le contraire de la gestion manuelle de la mémoire, qui nécessite que le programmeur spécifie les objets à désallouer et à renvoyer au système de mémoire.

Le processus de récupération de mémoire par une récupération de mémoire n'est pas libre. Il réduit généralement vos performances disponibles en prenant un bloc de temps pour effectuer son travail. En parallèle, le système décide lui-même du moment de l'exécution. Vous n'avez aucun contrôle sur cette action. Une impulsion de récupération de mémoire peut se produire à tout moment pendant l'exécution du code, ce qui l'interrompt jusqu'à ce qu'elle soit terminée. Vous ne connaissez généralement pas la durée de cette impulsion. L'exécution prend un certain temps, en fonction de la manière dont votre programme utilise la mémoire à un moment donné.

Les applications hautes performances s'appuient sur des limites de performances constantes pour garantir une expérience utilisateur fluide. Les systèmes de récupération de mémoire peuvent court-circuiter cet objectif, car ils peuvent s'exécuter à des moments aléatoires pendant des durées aléatoires, ce qui réduit le temps dont l'application a besoin pour atteindre ses objectifs de performances.

Réduisez la saturation de la mémoire et les taxes sur la récupération de mémoire

Comme indiqué, une récupération de mémoire peut prendre une impulsion lorsqu'une méthode heuristique détermine qu'il y a suffisamment d'objets inactifs pour obtenir un pouls. Ainsi, la clé pour réduire le temps que le récupérateur de mémoire prend à votre application consiste à éliminer autant de cas de création et de publication d'objets que possible. Ce processus de création/de libération d'objets est fréquemment appelé "perte de mémoire". Si vous pouvez réduire la saturation de la mémoire pendant toute la durée de vie de votre application, vous réduisez également le délai de récupération de mémoire avant son exécution. Cela signifie que vous devez supprimer / réduire le nombre d'objets créés et détruits. Dans les faits, vous devez arrêter d'allouer de la mémoire.

Ce processus déplacera votre graphique de mémoire de ce qui suit :

Un instantané de votre chronologie de mémoire

à ceci:

JavaScript de mémoire statique

Dans ce modèle, vous pouvez constater que le graphique n'a plus de motif en dents de scie, mais qu'il s'étend considérablement au début, puis augmente lentement au fil du temps. Si vous rencontrez des problèmes de performances dus à une saturation de la mémoire, voici le type de graphique que vous devriez créer.

Passer à JavaScript en mémoire statique

La technique JavaScript de mémoire statique consiste à préallouer au début de l'application toute la mémoire nécessaire à sa durée de vie et à gérer cette mémoire lors de l'exécution, car les objets ne sont plus nécessaires. Nous pouvons aborder cet objectif en quelques étapes simples:

  1. Instrumentez votre application pour déterminer le nombre maximal d'objets de mémoire active (par type) requis pour différents scénarios d'utilisation
  2. Réimplémentez votre code pour préallouer cette quantité maximale, puis extrayez/libérez-les manuellement au lieu d'aller dans la mémoire principale.

Dans les faits, pour atteindre la première étape, nous devons effectuer la deuxième étape. Commençons donc par là.

Pool d'objets

Pour faire simple, le pool d'objets consiste à conserver un ensemble d'objets inutilisés qui partagent un type. Lorsque vous avez besoin d'un nouvel objet pour votre code, au lieu d'en allouer un nouveau à partir du tas de mémoire du système, vous recyclez un des objets inutilisés du pool. Une fois que le code externe a terminé avec l'objet, au lieu de le libérer dans la mémoire principale, celui-ci est renvoyé au pool. Comme l'objet n'est jamais déréférencé (c'est-à-dire supprimé) du code, il ne sera pas récupéré. L'utilisation de pools d'objets redonne au programmeur le contrôle de la mémoire, ce qui réduit l'influence du récupérateur de mémoire sur les performances.

Étant donné qu'une application gère un ensemble hétérogène de types d'objets, une utilisation appropriée des pools d'objets nécessite que vous disposiez d'un pool par type qui connaît un taux de perte d'utilisateurs élevé pendant l'exécution de votre application.

var newEntity = gEntityObjectPool.allocate();
newEntity.pos = {x: 215, y: 88};

//..... do some stuff with the object that we need to do

gEntityObjectPool.free(newEntity); //free the object when we're done
newEntity = null; //free this object reference

Pour la grande majorité des applications, vous finirez par atteindre un seuil en termes d'allocation de nouveaux objets. Lors de plusieurs exécutions de votre application, vous devriez pouvoir vous faire une idée précise de cette limite supérieure et pouvoir préallouer ce nombre d'objets au début de l'application.

Pré-allocation d'objets

L'implémentation du pooling d'objets dans votre projet vous permettra d'obtenir un nombre maximal théorique d'objets requis pendant l'exécution de votre application. Après avoir passé en revue différents scénarios de test sur votre site, vous pouvez vous faire une idée des types d'exigences de mémoire nécessaires, cataloguer ces données quelque part et les analyser pour déterminer les limites supérieures de la mémoire requise pour votre application.

Ensuite, dans la version de livraison de votre application, vous pouvez définir la phase d'initialisation pour préremplir tous les pools d'objets jusqu'à un montant spécifié. Cette action transmettra toute l'initialisation des objets au premier plan de votre application, ce qui réduira le nombre d'allocations qui se produisent de manière dynamique pendant son exécution.

function init() {
  //preallocate all our pools. 
  //Note that we keep each pool homogeneous wrt object types
  gEntityObjectPool.preAllocate(256);
  gDomObjectPool.preAllocate(888);
}

Le montant que vous choisissez dépend en grande partie du comportement de votre application. Parfois, le maximum théorique n'est pas la meilleure option. Par exemple, le choix de la valeur moyenne maximale peut réduire l'espace mémoire utilisé pour les utilisateurs non expérimentés.

Loin d'une solution miracle

Il existe toute une classification des applications dans lesquelles les modèles de croissance de mémoire statique peuvent être gagnants. Renato Mangini, un collègue DevRel Chrome, le souligne toutefois, qu'il y a quelques inconvénients.

Conclusion

L'une des raisons pour lesquelles JavaScript est idéal pour le Web repose sur le fait qu'il s'agit d'un langage rapide, amusant et facile à utiliser. Cela s'explique principalement par le fait qu'il n'est pas limité par les restrictions de syntaxe et qu'il gère les problèmes de mémoire en votre nom. Vous pouvez vous contenter de coder et le laisser s'occuper des tâches sales. Cependant, pour les applications Web hautes performances telles que les jeux HTML5, la récupération de mémoire peut souvent épuiser une fréquence d'images indispensable, réduisant ainsi l'expérience de l'utilisateur final. Avec une instrumentation et une adoption prudentes des pools d'objets, vous pouvez réduire cette charge sur votre fréquence d'images et récupérer ce temps pour des choses plus impressionnantes.

Code source

Il existe de nombreuses implémentations de pools d'objets flottant sur le Web, donc je ne vais pas vous ennuyer avec une autre. Je vais plutôt vous rediriger vers ces ressources, qui présentent chacune des subtilités d'implémentation, ce qui est important, étant donné que chaque utilisation d'une application peut présenter des besoins d'implémentation spécifiques.

Références