Comment lire un flamegraph
Vous avez lancé un profiling, par exemple avec async-profiler, et vous voilà devant un flamegraph. C’est l’une des meilleures façons de comprendre où part le temps d’un programme, à condition de connaître la grille de lecture. Elle tient en quelques règles.
Ce que représente chaque axe
Un flamegraph agrège des milliers d’échantillons de stacks. Chaque rectangle est une frame, c’est-à-dire une méthode.
- Largeur : la proportion d’échantillons où cette méthode était présente dans la pile. Plus c’est large, plus le programme y a passé de temps (CPU ou wall-clock, selon ce que vous avez mesuré). C’est la seule dimension qui sert à prioriser.
- Hauteur : la profondeur de la pile d’appels. Une frame posée sur une autre, c’est un appel : celle du bas a appelé celle du dessus.
- Couleur : en général, aucune signification. Les teintes sont tirées au hasard pour distinguer les blocs voisins. Certains modes s’en servent pour séparer Java, natif et noyau, donc vérifiez la légende avant d’en tirer une conclusion.
Le piège classique : l’horizontale n’est pas le temps
C’est l’erreur qu’on voit le plus souvent. De gauche à droite, il n’y a pas de chronologie. Les frames d’un même niveau sont juste triées par ordre alphabétique pour regrouper les piles identiques. Une méthode tout à droite ne s’exécute pas « après » celle de gauche. Un flamegraph répond à la question « où passe le temps ? », pas à « dans quel ordre ? ». Pour de la chronologie, il faut un autre outil, une trace ou une timeline.
En pratique : chercher les plateaux
La méthode qui marche :
- Partir du haut et repérer les frames larges. Le sommet d’une pile, c’est le code qui était réellement en train de tourner quand l’échantillon a été pris (son « self time »). Un large plateau en haut, c’est du temps brûlé directement là. Premier suspect.
- Redescendre pour comprendre le chemin. En suivant une frame large vers le bas, on voit qui a mené jusque-là. Bien souvent le coupable n’est pas la feuille elle-même, mais le fait qu’on l’appelle beaucoup trop, depuis plus haut.
- Ignorer les tours fines et isolées. Une pile étroite et très haute consomme peu : beaucoup d’appels imbriqués, mais peu de temps total. Pas une priorité.
La règle à garder en tête : la largeur dit combien ça coûte, la position en haut dit où c’est effectivement dépensé.
Quelques motifs qu’on reconnaît vite
- Un large plateau plat tout en haut : une méthode chaude, du calcul, de la sérialisation, du parsing. Optimisable directement.
- Une base très large qui se ramifie vers le haut : un point d’entrée qui éparpille le temps dans une foule de sous-appels. Là, cherchez le facteur commun plutôt qu’une méthode unique.
- Des couches de framework ou de proxies empilées sous votre code : souvent de l’AOP, des intercepteurs, de la reflection. C’est normal, sauf quand ça devient large.
Adapter sa lecture à l’événement profilé
Le même graphe ne se lit pas pareil selon ce qui a été mesuré. Sur un flamegraph CPU, une frame large, c’est du calcul à optimiser. Sur un flamegraph wall-clock, une frame large peut être une attente (I/O, lock, park), et l’objectif n’est alors pas d’accélérer le code mais de supprimer le blocage. Sur un flamegraph d’allocations, la largeur représente des octets alloués, pas du temps. Gardez toujours en tête ce que vous avez demandé à async-profiler de mesurer.
En résumé
Cherchez le large, partez du haut, ne lisez pas l’horizontale comme une horloge, et souvenez-vous de ce que l’événement profilé veut dire. Avec ça, un flamegraph qui paraissait illisible devient une carte assez directe de ce qu’il faut corriger en premier.
Si vous n’avez pas encore généré le vôtre : Profiler la JVM avec async-profiler.