Profiler votre jeu WebGL avec l'indicateur about:tracing

Lilli Thompson
Lilli Thompson

Si vous ne pouvez pas le mesurer, vous ne pouvez pas l'améliorer.

Lord Kelvin

Pour accélérer l'exécution de vos jeux HTML5, vous devez d'abord identifier les goulots d'étranglement des performances, mais cela peut s'avérer difficile. Évaluer les données sur les images par seconde (FPS) est un bon début, mais pour avoir une vue d'ensemble, vous devez saisir les nuances des activités Chrome.

L'outil about:tracing vous fournit les insights qui vous permettent d'éviter les solutions rapides visant à améliorer les performances, mais qui sont essentiellement des suppositions bien intentionnées. Vous gagnerez ainsi beaucoup de temps et d'énergie, et vous obtiendrez une image plus claire de ce que Chrome fait avec chaque frame. Vous pourrez ensuite utiliser ces informations pour optimiser votre jeu.

Bonjour about:tracing

L'outil about:tracing de Chrome vous permet d'accéder à toutes les activités de Chrome sur une période donnée avec une granularité si élevée que vous pourriez être submergé au premier abord. De nombreuses fonctions de Chrome sont instrumentées pour le traçage dès la sortie de la boîte. Vous pouvez donc utiliser about:tracing pour suivre vos performances sans avoir à effectuer d'instrumentation manuelle. (voir une section ultérieure sur l'instrumentation manuelle de votre code JavaScript)

Pour afficher la vue de traçage, saisissez simplement "about:tracing" dans la barre d'adresse de Chrome.

Omnibox Chrome
Saisissez "about:tracing" dans l'omnibox de Chrome.

Dans l'outil de traçage, vous pouvez démarrer l'enregistrement, exécuter votre jeu pendant quelques secondes, puis afficher les données de traçage. Voici un exemple de ce à quoi les données peuvent ressembler:

Résultat de traçage simple
Résultat de traçage simple

Oui, c'est vrai. Voyons comment le lire.

Chaque ligne représente un processus en cours de profilage, l'axe de gauche à droite indique le temps, et chaque zone colorée correspond à un appel de fonction instrumenté. Il existe des lignes pour plusieurs types de ressources. Les plus intéressants pour le profilage de jeu sont CrGpuMain, qui montre ce que fait l'unité de traitement graphique (GPU), et CrRendererMain. Chaque trace contient des lignes CrRendererMain pour chaque onglet ouvert pendant la période de traçage (y compris l'onglet about:tracing lui-même).

Lorsque vous lisez les données de trace, votre première tâche consiste à déterminer quelle ligne CrRendererMain correspond à votre jeu.

Résultat de traçage simple mis en évidence
Résultat du traçage simple mis en évidence

Dans cet exemple, les deux candidats sont: 2216 et 6516. Malheureusement, il n'existe actuellement aucun moyen élégant de sélectionner votre application, sauf à rechercher la ligne qui effectue de nombreuses mises à jour périodiques (ou, si vous avez instrumenté manuellement votre code avec des points de trace, à rechercher la ligne qui contient vos données de trace). Dans cet exemple, il semble que 6516 exécute une boucle principale en fonction de la fréquence des mises à jour. Si vous fermez tous les autres onglets avant de démarrer la traçabilité, vous trouverez plus facilement le CrRendererMain approprié. Toutefois, il peut toujours y avoir des lignes CrRendererMain pour d'autres processus que votre jeu.

Trouver votre cadre

Une fois que vous avez trouvé la ligne appropriée dans l'outil de traçage de votre jeu, l'étape suivante consiste à trouver la boucle principale. La boucle principale ressemble à un motif répétitif dans les données de traçage. Vous pouvez parcourir les données de traçage à l'aide des touches W, A, S et D: A et D pour vous déplacer vers la gauche ou la droite (avant et arrière dans le temps) et W et S pour faire un zoom avant ou arrière sur les données. Votre boucle principale devrait être un modèle qui se répète toutes les 16 millisecondes si votre jeu s'exécute à 60 Hz.

Il semble que trois cadres d'exécution
Ressemble à trois cadres d'exécution

Une fois que vous avez identifié le "heartbeat" de votre jeu, vous pouvez examiner précisément ce que fait votre code à chaque frame. Utilisez les touches W, A, S et D pour faire un zoom avant jusqu'à ce que vous puissiez lire le texte dans les zones de fonction.

Dans un frame d'exécution
Plongée dans un frame d'exécution

Cet ensemble de cases affiche une série d'appels de fonction, chaque appel étant représenté par une case colorée. Chaque fonction a été appelée par la zone située au-dessus. Dans ce cas, vous pouvez voir que MessageLoop::RunTask a appelé RenderWidget::OnSwapBuffersComplete, qui a à son tour appelé RenderWidget::DoDeferredUpdate, et ainsi de suite. En lisant ces données, vous pouvez obtenir une vue complète de ce qui appelle quoi et de la durée de chaque exécution.

Mais c'est là que les choses se compliquent. Les informations exposées par about:tracing sont les appels de fonction bruts du code source de Chrome. Vous pouvez faire des suppositions éclairées sur ce que fait chaque fonction à partir de son nom, mais les informations ne sont pas vraiment conviviales. Il est utile de voir le flux global de votre frame, mais vous avez besoin de quelque chose de plus lisible pour comprendre ce qui se passe.

Ajouter des balises de suivi

Heureusement, il existe un moyen simple d'ajouter une instrumentation manuelle à votre code pour créer des données de trace: console.time et console.timeEnd.

console.time("update");
update
();
console
.timeEnd("update");
console
.time("render");
update
();
console
.timeEnd("render");

Le code ci-dessus crée des cases dans le nom de la vue de traçage avec les balises spécifiées. Si vous exécutez à nouveau l'application, vous verrez des cases "update" (Mettre à jour) et "render" (Afficher) qui indiquent le temps écoulé entre les appels de début et de fin pour chaque balise.

Tags ajoutés manuellement
Tags ajoutés manuellement

Vous pouvez ainsi créer des données de traçage lisibles par l'humain pour suivre les points chauds de votre code.

GPU ou CPU ?

Avec les graphiques accélérés par matériel, l'une des questions les plus importantes que vous pouvez vous poser lors du profilage est la suivante: ce code est-il lié au GPU ou au CPU ? À chaque frame, vous effectuez un travail de rendu sur le GPU et une logique sur le CPU. Pour comprendre ce qui ralentit votre jeu, vous devez voir comment le travail est réparti entre les deux ressources.

Commencez par trouver la ligne CrGPUMain dans la vue de traçage, qui indique si le GPU est occupé à un moment donné.

Traces du GPU et du CPU

Vous pouvez constater que chaque frame de votre jeu génère du travail de processeur dans CrRendererMain et sur le GPU. La trace ci-dessus montre un cas d'utilisation très simple où le processeur et le GPU sont inactifs pendant la majeure partie de chaque frame de 16 ms.

La vue de traçage est vraiment utile lorsque le jeu s'exécute lentement et que vous ne savez pas quelle ressource est saturée. L'examen de la relation entre les lignes GPU et CPU est essentiel pour le débogage. Prenez le même exemple qu'avant, mais ajoutez un peu de travail supplémentaire dans la boucle de mise à jour.

console.time("update");
doExtraWork
();
update
(Math.min(50, now - time));
console
.timeEnd("update");

console
.time("render");
render
();
console
.timeEnd("render");

Une trace semblable à celle-ci s'affiche:

Traces du GPU et du CPU

Que nous apprend cette trace ? On peut voir que le frame illustré passe d'environ 2 270 ms à 2 320 ms, ce qui signifie que chaque frame prend environ 50 ms (une fréquence d'images de 20 Hz). Vous pouvez voir des fragments de cases colorées représentant la fonction de rendu à côté de la case de mise à jour, mais le frame est entièrement dominé par la mise à jour elle-même.

Contrairement à ce qui se passe sur le CPU, vous pouvez voir que le GPU est toujours inactif pendant la majeure partie de chaque frame. Pour optimiser ce code, vous pouvez rechercher des opérations qui peuvent être effectuées dans le code de nuanceur et les déplacer vers le GPU afin d'utiliser au mieux les ressources.

Que se passe-t-il lorsque le code du nuanceur lui-même est lent et que le GPU est surchargé ? Que se passerait-il si nous supprimions le travail inutile du processeur et ajoutions plutôt du travail dans le code du nuanceur de fragment ? Voici un nuanceur de fragments inutilement coûteux:

#ifdef GL_ES
precision highp
float;
#endif
void main(void) {
 
for(int i=0; i<9999; i++) {
    gl_FragColor
= vec4(1.0, 0, 0, 1.0);
 
}
}

À quoi ressemble une trace de code utilisant ce nuanceur ?

Traces du GPU et du CPU lorsque vous utilisez un code GPU lent
Traces du GPU et du processeur lors de l'utilisation d'un code GPU lent

Notez à nouveau la durée d'un frame. Ici, le motif répété va d'environ 2 750 ms à 2 950 ms, soit une durée de 200 ms (fréquence d'images d'environ 5 Hz). La ligne CrRendererMain est presque complètement vide, ce qui signifie que le processeur est inactif la plupart du temps, tandis que le GPU est surchargé. Il s'agit d'un signe certain que vos nuanceurs sont trop lourds.

Si vous ne saviez pas exactement ce qui était à l'origine du faible framerate, vous pourriez observer la mise à jour à 5 Hz et être tenté d'accéder au code du jeu pour essayer d'optimiser ou de supprimer la logique de jeu. Dans ce cas, cela ne servirait à rien, car ce n'est pas la logique de la boucle de jeu qui prend du temps. En fait, cette trace indique que le fait d'effectuer plus de travail de processeur à chaque frame serait essentiellement "sans frais", car le processeur est inactif. Par conséquent, lui donner plus de travail n'aura aucun impact sur la durée du frame.

Exemples concrets

Voyons maintenant à quoi ressemblent les données de traçage d'un jeu réel. L'un des avantages des jeux créés avec des technologies Web ouvertes est que vous pouvez voir ce qui se passe dans vos produits préférés. Si vous souhaitez tester des outils de profilage, vous pouvez choisir votre titre WebGL préféré sur le Chrome Web Store et le profiler avec about:tracing. Voici un exemple de trace tiré de l'excellent jeu WebGL Skid Racer.

Suivre un jeu réel
Tracer un jeu réel

Il semble que chaque frame prenne environ 20 ms, ce qui signifie que la fréquence d'images est d'environ 50 FPS. Vous pouvez voir que le travail est équilibré entre le CPU et le GPU, mais que le GPU est la ressource la plus demandée. Si vous souhaitez découvrir ce que c'est que de profiler des exemples réels de jeux WebGL, essayez de jouer avec certains des titres du Chrome Web Store créés avec WebGL, y compris:

Conclusion

Si vous souhaitez que votre jeu s'exécute à 60 Hz, toutes vos opérations doivent s'adapter à 16 ms de temps de processeur et 16 ms de temps de GPU pour chaque frame. Vous disposez de deux ressources pouvant être utilisées en parallèle, et vous pouvez répartir le travail entre elles pour optimiser les performances. La vue about:tracing de Chrome est un outil inestimable pour obtenir des insights sur ce que fait réellement votre code. Elle vous aidera à maximiser votre temps de développement en ciblant les problèmes appropriés.

Étape suivante

En plus du GPU, vous pouvez également suivre d'autres parties de l'environnement d'exécution Chrome. Chrome Canary, la version préliminaire de Chrome, est instrumentée pour suivre les E/S, IndexedDB et plusieurs autres activités. Pour en savoir plus sur l'état actuel des événements de traçage, consultez cet article Chromium.

Si vous êtes développeur de jeux Web, veillez à regarder la vidéo ci-dessous. Il s'agit d'une présentation de l'équipe Google chargée de la promotion des jeux lors de la GDC 2012 sur l'optimisation des performances pour les jeux Chrome: