Outil Memory · Arcade sandbox

Allocation sampling

Un sandbox rétro inspiré d’Asteroids. Il a au moins un memory leak et sature vite le CPU sur des machines plus modestes. Pourtant, le jeu semble avoir trente ans.

On va regarder où le jeu dépense sa mémoire, avant que ses petits excès ne deviennent un vrai problème.

Point de départ

Ouvrez le lab arcade

Ouvrez le workspace 02-arcade-profiling et lancez le sandbox. On termine ici le tour des outils Memory ; la page suivante passe aux bases du CPU profiling.

Le sandbox arcade rétro en action : vaisseau, ennemis, tirs et particules dans l’espace.
Un jeu discret, des allocations beaucoup moins discrètes.

Outil Memory

Allocation sampling

Dans Memory, choisissez Allocation sampling. DevTools rattache les allocations aux fonctions qui les ont déclenchées.

Les types de profiling du panneau Memory de Chrome DevTools, avec Allocation sampling sélectionné.
Ce profile n’est pas au byte près, mais il reste assez léger pour jouer quelques instants sans fausser complètement la partie.

Allocation sampling regarde un échantillon des allocations plutôt que de compter chaque object. Les chiffres ne sont donc pas exacts au byte près, mais ils montrent très vite quelles fonctions créent le plus de bazar.

  1. Lancez l’enregistrement.
  2. Pendant 15 à 20 secondes, jouez normalement : tirez, détruisez quelques mines et déclenchez trois attaques spéciales avec la touche X. Trichez avec O pour recharger le condensateur et reproduire l’action.
  3. Arrêtez l’enregistrement.
Résultat d’un Allocation sampling dans Heavy (Bottom Up), montrant notamment une allocation attribuée à spawnEnemy.
Après quelques secondes de jeu, le résultat peut ressembler à ceci. Qu’est-ce que je regarde ?

Qu’est-ce que je regarde ???

La vue répond à une seule question : « dans quelles fonctions DevTools a-t-il vu le plus d’allocations ? » Ce n’est ni le décompte des GameObjects vivants, ni la liste complète de tout ce qui s’est passé. Chaque ligne est un échantillon, regroupé par JavaScript execution stack. Ici, l’échantillon remonte jusqu’à spawnEnemy : une mine a été créée sur ce chemin.

Premier cas utile : les explosions de mines. Chaque mine détruite crée 32 particles, donc beaucoup d’allocations d’un coup. Explorez les piles qui apparaissent autour de l’explosion et essayez de relier ce que vous voyez à cette action du jeu.

Le second cas utile est l’attaque spéciale avec la touche X : chaque pression crée 30 particles d’un coup. Trichez avec la touche O pour recharger le condensateur, puis répétez l’action. Elles traversent ensuite l’espace sans jamais être détruites. Cherchez la pile qui remonte vers SpecialAttack.fire, puis suivez-la jusqu’au new SpecialEnergyParticle(...). On obtient ainsi une piste de code qui alloue beaucoup, sans prétendre avoir prouvé une fuite.

Pour établir les trois memory leaks, choisissez plutôt Allocations on timeline ou comparez deux heap snapshots : ces outils permettent de voir les Bullet, SpecialEnergyParticle ou ExplosionParticle qui restent vivants après l’action. Ici, Allocation sampling sert à trouver où regarder ; les autres mesures servent à prouver ce qui reste.

Le HUD du jeu affiche aussi Objets retenus : les tirs manqués, particles de l’attaque spéciale et débris d’explosion fondus mais jamais détruits. C’est un contrôle pratique du scénario, pas un remplacement de la mesure DevTools.

  1. Dans Heavy (Bottom Up), commencez par ce qui occupe le haut de la liste.
  2. Remontez le JavaScript execution stack pour voir d’où viennent les allocations : particules, collisions, sprites, ou juste une mauvaise idée dans la boucle de jeu.

À vous de jouer

Pratiquez ce que vous avez appris

Trois memory leaks se sont glissés dans le jeu. Rejouez le même petit scénario, servez-vous de l’outil qui vous paraît le plus utile, puis regardez ce qui change.

Petit repère : dans Phaser, un tir, une mine ou une particule est un Game Object. Pour le retirer de la scène et de la physique, appelez destroy() sur cet objet.

Essayez d’abord. Les spoilers disent où regarder, mais ne remplacent ni le profile ni le contrôle après le correctif. Si vous ne trouvez pas, appuyez sur D pour afficher une ligne de debug supplémentaire qui devrait vous mettre sur la piste.

Leak 1 — les débris d’explosion fondent, mais ne disparaissent pas

Chaque mine détruite laisse 32 particles physiques. Elles deviennent transparentes, puis continuent hors écran avec leur body.

02-arcade-profiling/src/entities/ExplosionParticle.js
if (this.fadeRemaining <= 0) {
  this.setAlpha(0);
  // this.destroy(); manque ici
  return;
}
Heap snapshot montrant de nombreux Body Phaser retenus par des ExplosionParticle.
Dans le heap snapshot, les Body restent retenus par les ExplosionParticle, même après la disparition visuelle de l’effet.
Leak 2 — les tirs manqués traversent le vide sans fin

Un projectile qui ne touche rien ne quitte jamais le groupe physique. Il continue hors écran, avec son body.

02-arcade-profiling/src/entities/Bullet.js
constructor(scene) {
  super(scene, 0, 0, ASSET_KEYS.space, 'blaster');
  scene.add.existing(this);
  scene.physics.add.existing(this);
  this.lifespan = BULLETS.lifespan;
}

fire(ship) {
  this.lifespan = BULLETS.lifespan;
  this.setPosition(ship.x, ship.y);
  this.body.reset(ship.x, ship.y);
  const angle = Phaser.Math.DegToRad(ship.body.rotation);
  this.scene.physics.velocityFromRotation(angle, BULLETS.speed, this.body.velocity);
}

update(_, delta) {
  this.lifespan -= delta;
  if (this.lifespan <= 0) {
    this.setVisible(false); // le tir est seulement masqué
    // this.destroy(); correctif attendu
  }
}
Leak 3 — l’attaque spéciale traverse l’espace sans fin

Les 30 particles de chaque attaque restent dans le groupe physique, même lorsqu’elles sont très loin du jeu.

02-arcade-profiling/src/entities/SpecialEnergyParticle.js
constructor(scene) {
  super(scene, 0, 0, ASSET_KEYS.space, 'blue');
  scene.add.existing(this);
  scene.physics.add.existing(this);
  this.lifespan = 0;
}

fire(ship, angle) {
  this.lifespan = SPECIAL_ATTACK.particleLifespan;
  this.setPosition(ship.x, ship.y);
  this.body.reset(ship.x, ship.y);
  this.scene.physics.velocityFromRotation(
    angle, SPECIAL_ATTACK.particleSpeed, this.body.velocity
  );
}

update(_, delta) {
  this.lifespan -= delta;
  if (this.lifespan <= 0) {
    this.setVisible(false); // l’effet est seulement masqué
    // this.destroy(); correctif attendu
  }
}

Pour aller plus loin

La solution propre

Ce n’est pas un cours de game development. Mais pour une version propre, on évite de créer puis détruire sans arrêt des images et des objets simples ; avec de la physique, le coût est encore plus sensible. Des pools précréent des objets dormants, puis les activent ou les désactivent au besoin : moins d’allocations et moins de risques de memory leaks.

Le dossier 02-arcade-profiling/_clean-code contient cette version avec des pools d’objets pour les trois cas que vous venez de corriger.