Introduction
Vous recevez donc un e-mail vous informant des mauvaises performances de votre jeu ou de votre application Web au bout d'un certain temps. Vous fouillez votre code, vous ne voyez rien de différent jusqu'à ce que vous ouvriez les outils de performances de la mémoire de Chrome et que vous voyiez ceci:
L'un de vos collègues sourit, car il comprend que vous rencontrez un problème de performances lié à la mémoire.
Dans la vue du graphique de la mémoire, ce modèle en dents de scie est très révélateur d'un problème de performances potentiellement critique. À mesure que l'utilisation de la mémoire augmente, la zone du graphique augmente également dans la capture de la chronologie. Lorsque le graphique baisse soudainement, il s'agit d'une instance où le collecteur de déchets a exécuté et nettoyé vos objets de mémoire référencés.
Dans un graphique comme celui-ci, vous pouvez voir que de nombreux événements de récupération de mémoire se produisent, ce qui peut nuire aux performances de vos applications Web. Cet article explique comment contrôler l'utilisation de la mémoire et réduire son impact sur les 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 mémoire à partir du tas de mémoire du système. Toutefois, un système de récupération de mémoire gère cette tâche pour le compte du programmeur. Cela signifie que les objets ne sont pas directement libérés de la mémoire lorsque celui-ci les déréférence, mais plus tard lorsque l'heuristique de la récupération de mémoire décide qu'il serait utile de le faire. Ce processus de décision nécessite que le RM exécute une analyse statistique des objets actifs et inactifs, ce qui prend un certain temps à effectuer.
La récupération de mémoire est souvent présentée comme le contraire de la gestion manuelle de la mémoire, qui oblige le programmeur à spécifier les objets à désallouer et à renvoyer au système de mémoire
Le processus par lequel un GC récupère de la mémoire n'est pas gratuit. Il réduit généralement vos performances disponibles en prenant un bloc de temps pour effectuer son travail. De plus, le système lui-même décide quand l'exécuter. Vous n'avez aucun contrôle sur cette action. Un signal GC peut se produire à tout moment pendant l'exécution du code, ce qui bloque l'exécution du code jusqu'à ce qu'elle soit terminée. La durée de cette impulsion vous est généralement inconnue ; son exécution prendra 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 cohérentes pour garantir une expérience fluide aux utilisateurs. Les systèmes de récupération de mémoire peuvent court-circuiter cet objectif, car ils peuvent s'exécuter à des heures et des durées aléatoires, réduisant le temps disponible dont l'application a besoin pour atteindre ses objectifs de performances.
Réduire la perte de mémoire, réduire les taxes de récupération de mémoire
Comme indiqué, un signal GC se produit une fois qu'un ensemble d'heuristiques détermine qu'il existe suffisamment d'objets inactifs pour qu'un signal soit utile. Par conséquent, la clé pour réduire le temps que le garbage collector prend à votre application consiste à éliminer autant de cas de création et de libération d'objets excessive que possible. Ce processus de création/libération d'objets fréquents est appelé "saturation de la mémoire". Si vous pouvez réduire la saturation de la mémoire pendant la durée de vie de votre application, vous réduisez également le temps que la récupération de mémoire prend sur votre exécution. Cela signifie que vous devez supprimer/réduire le nombre d'objets créés et détruits. En d'autres termes, vous devez arrêter d'allouer de la mémoire.
Ce processus déplace votre graphique de mémoire de la manière suivante :
en ceci :
Dans ce modèle, vous pouvez voir que le graphique n'a plus un motif en forme de dent de scie, mais qu'il grossit beaucoup au début, puis augmente progressivement au fil du temps. Si vous rencontrez des problèmes de performances en raison d'une rotation de mémoire, c'est le type de graphique que vous devez créer.
Passage à du code JavaScript à mémoire statique
La mémoire statique JavaScript est une technique qui consiste à préallouer, au début de votre application, toute la mémoire nécessaire pendant toute sa durée de vie, et à gérer cette mémoire pendant l'exécution lorsque les objets ne sont plus nécessaires. Pour atteindre cet objectif, procédez comme suit :
- Instrumentez votre application pour déterminer le nombre maximal d'objets de mémoire actifs (par type) requis pour une gamme de scénarios d'utilisation.
- Réimplémentez votre code pour préallouer cette quantité maximale, puis récupérez-les/libérez-les manuellement au lieu d'accéder à la mémoire principale.
En réalité, pour accomplir la première tâche, nous devons faire un peu de la deuxième. Commençons par là.
Pool d'objets
En termes simples, 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, plutôt que d'en allouer un à partir du tas de mémoire du système, vous recyclez l'un des objets inutilisés du pool. Une fois que le code externe a été utilisé avec l'objet, au lieu de le libérer dans la mémoire principale, il est renvoyé au pool. Étant donné que l'objet n'est jamais déréférencé (ou supprimé) du code, il ne sera pas collecté par le garbage collector. L'utilisation de pools d'objets redonne au programmeur le contrôle de la mémoire, ce qui réduit l'influence du garbage collector sur les performances.
Étant donné qu'une application gère un ensemble hétérogène de types d'objets, une utilisation correcte des pools d'objets nécessite de disposer d'un pool par type d'objet qui connaît un taux de rotation é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 niveau d'allocation d'objets. Au fil de plusieurs exécutions de votre application, vous devriez pouvoir déterminer cette limite supérieure avec précision et préallouer ce nombre d'objets au début de votre application.
Pré-allocation d'objets
En implémentant le pooling d'objets dans votre projet, vous obtiendrez un maximum théorique du nombre d'objets requis pendant l'exécution de votre application. Une fois que vous avez exécuté votre site dans différents scénarios de test, vous pouvez avoir une bonne idée des types de mémoire requis. Vous pouvez ensuite cataloguer ces données et les analyser pour comprendre quelles sont les limites supérieures des exigences de mémoire 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 selon un montant spécifié. Cette action placera l'initialisation de tous les objets au début de votre application et réduira le nombre d'allocations qui se produisent de manière dynamique lors de 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, choisir la valeur moyenne maximale peut réduire l'espace mémoire utilisé par les utilisateurs occasionnels.
Loin d'être une solution miracle
Il existe toute une classification d'applications pour lesquelles les modèles de croissance de la mémoire statique peuvent être bénéfiques. Cependant, comme le souligne Renato Mangini, spécialiste Chrome DevRel, cette approche présente quelques inconvénients.
Conclusion
L'une des raisons pour lesquelles JavaScript est idéal pour le Web est qu'il s'agit d'un langage rapide, amusant et facile à prendre en main. Cela s'explique principalement par la faible barrière des restrictions de syntaxe et la gestion des problèmes de mémoire en votre nom. Vous pouvez coder sans vous soucier du code et le laisser s'occuper du reste. Toutefois, pour les applications Web hautes performances, comme les jeux HTML5, le GC peut souvent réduire la fréquence d'images, ce qui réduit l'expérience de l'utilisateur final. Grâce à une instrumentation minutieuse et à l'adoption de pools d'objets, vous pouvez réduire cette charge sur votre fréquence d'images et récupérer ce temps pour d'autres tâches plus intéressantes.
Code source
Il existe de nombreuses implémentations de pools d'objets sur le Web. Je ne vais donc pas vous ennuyer avec une autre. Je vais plutôt vous diriger vers ces éléments, chacun d'eux ayant des nuances d'implémentation spécifiques. Cela est important, car chaque utilisation d'une application peut avoir des besoins d'implémentation spécifiques.
- Pool d'objets de Gamecore.js
- Pools d'objets de Beej
- Pool d'objets super simple d'Emehrkay
- Le pool d'objets de Steven Lambert axé sur le jeu
- Configuration de l'objectPool de RenderEngine