Profiler une image native GraalVM avec perf

Profiler une image native GraalVM avec perf

L’outil GraalVM native-image permet de générer un exécutable natif (ou image native) depuis votre application Java.

Cet exécutable natif va démarrer très rapidement et avoir une empreinte mémoire beaucoup plus faible qu’une application Java traditionnelle; au prix de performances en pic réduites et d’un temps de création de ce package natif assez élevé. Plus d’informations sur les exécutables natifs ici.

Un exécutable natif contient une JVM minimaliste appelée SubstratVM, celle-ci a quelques limitations :

  • Support partiel de la reflection
  • Support partiel des proxies dynamiques
  • Support partiel du chargement dynamique des classes
  • Pas de JNI
  • Pas de JVMTI

Pas de support de JVMTI signifie pas de support des agents Java, de JMX, des profilers Java, des debuggers Java, de Java Flight Recorder et Java Mission Control, ainsi que de tous les outils livrés avec le JDK (jps, jstack, jmap).

Pour tous les besoins couverts par ces outils, il faut donc utiliser une solution intégrée à l’application (par exemple, remplacer les métriques JMX par des métriques Prometheus), ou des outils standards fournit par votre système d’exploitation.

Pour profiler l’exécution d’une application, l’OS Linux a un outil très puissant : perf.

L’outil perf a de nombreuses fonctionnalités, il peut accéder à toutes les métriques de l’OS et du CPU (performance counters, d’où son nom : perf) et profiler l’application de plein de manières différentes.

Utiliser perf pour profiler le CPU

L’outil perf va utiliser les symboles intégrés au binaire de votre application pour faire le lien entre un pointeur mémoire et la méthode Java correspondante (ou l’appel système).

Par défaut, ces symboles ne sont pas intégrés aux exécutables natifs, il faut donc demander à l’outil native-image de les y laisser via les options H:-DeleteLocalSymbols -H:+PreserveFramePointer.

Si vous voulez testez ces étapes, vous pouvez utiliser l’application getting-started de Quarkus. Quarkus a un support facilité de l’outil native-image, il suffit d’ajouter à l’application.properties de votre application la propriété quarkus.native.additional-build-args=-H:-DeleteLocalSymbols,-H:+PreserveFramePointer et celui-ci va automatiquement ajouter ces options à la ligne de commande de l’outil native-image.

Après avoir généré votre exécutable natif, vous pouvez le lancer, puis récupérer son PID; nous utiliserons celui-ci dans la ligne de commande de l’outil perf.

Une fois votre application lancée, et idéalement sous charge (vous pouvez utiliser un outil tel que wrk pour générer de la charge), vous pouvez la profiler via la commande perf suivante : perf record -F 99 -p PID --call-graph dwarf sleep 10.

  • record : demande à perf de commencer à profiler l’application.
  • -F 99 : profile à 99 Hertz, ce qui veut dire 99 samples par seconde.
  • -p PID : demande à perf de profiler ce PID en particulier (celui de votre application).
  • –call-graph dwarf : indique à perf d’utiliser les symboles intégrés à votre application (symbole ELF).
  • sleep 10 : comme perf profile un PID et pas une commande, il faut lui passer une commande à exécuter. Quand cette commande sera terminée, perf arrêtera le profilage de votre application. En utilisant sleep 10 comme commande, on va donc profiler l’application pendant 10 secondes.

Quand la commande est terminée, perf aura généré un fichier de données qui contiendra le profil de votre application (profil CPU ici, car on ne lui a pas précisé quel événement il devait profiler) : perf.data.

Vous pouvez utiliser la commande suivante pour visualiser ce profil dans la console : perf report --stdio, vous aurez alors un résultat proche de celui-ci :

# Children      Self  Command          Shared Object                        Symbol                                                                                                                        >
# ........  ........  ...............  ...................................  ..............................................................................................................................>
#
    13.47%     0.00%  tloop-thread-19  libpthread-2.31.so                   [.] start_thread
            |
            ---start_thread
               IsolateEnterStub_PosixJavaThreads_pthreadStartRoutine_e1f4a8c0039f8337338252cd8734f63a79b5e3df_06195ea7c1ac11d884862c6f069b026336aa4f8c
               JavaThreads_threadStartRoutine_241bd8ce6d5858d439c83fac40308278d1b55d23
               Thread_run_857ee078f8137062fcf27275732adf5c4870652a
               FastThreadLocalRunnable_run_0329ad2c5210a091812879bcecd155c58e561e60
               ThreadExecutorMap$2_run_66c8943ee6536a10df07f979fb6cd278adcf96bc
               SingleThreadEventExecutor$4_run_1b47df7867e302a2fb7f28d7657a73e92f89d91f
               |          
               |--12.64%--NioEventLoop_run_be89580b4d16514bef6e948913d2ed21c5e4f679
               |          |          
               |          |--5.14%--NioEventLoop_processSelectedKeys_9a76c58d657b781ee037bbb65f41f01d2eb54e7c
               |          |          NioEventLoop_processSelectedKeysOptimized_c36ca161e53573665bc03cb5392e91c123bcd359
               |          |          NioEventLoop_processSelectedKey_3a0d92ce472db6c251df4485227a85acb9d3a1ca
               |          |          AbstractNioByteChannel$NioByteUnsafe_read_45358e803c643a6380776021e488e79d981b159d

Et ce sur des milliers de lignes … pas facile à analyser hein ?

Pour analyser facilement un profil généré par perf, on peut utiliser l’outil FlameGraph, accessible ici : https://github.com/brendangregg/FlameGraph

Un FlameGraph est une manière de visualiser le profil d’une application permettant de détecter instantanément le chemin de code le plus fréquent. Il va afficher, en abscisse, la population (généralement la méthode) dont la taille est proportionnelle aux nombres de samples du profil, et en ordonnée la profondeur dans la stack. Plus d’informations sur les FlameGraphs ici.

On peut noter un petit soucis dans les données de profil, la colonne Command au lieu de contenir la commande passée, contient le nom du thread (tronqué qui plus est). C’est un bug dans l’outil native-image, pour le contourner, nous allons utiliser sed pour modifier les données de profil avant des les utiliser dans l’outil FlameGraph. La valeur de la colonne Command se retrouve à la base du FlameGraph, elle doit normalement être unique pour que l’aggrégation des stacks se fassent.

La première étape est d’utiliser perf script pour extraire les données du profil dans un format textuel, puis d’utiliser sed pour corriger les problèmes de nom de commande pour pouvoir ensuite générer un FlameGraph.

perf script > out.perf
sed -i -E "s/cutor-thread-[0-9]*/executor-thread/" out.perf
sed -i -E "s/ntloop-thread-[0-9]*/eventloop-thread/" out.perf
sed -i -E "s/tloop-thread-[0-9]*/eventloop-thread/" out.perf
~/FlameGraph/stackcollapse-perf.pl out.perf | ~FlameGraph/flamegraph.pl > perf.svg

Voici un exemple de FlameGraph généré :

Ce que j’apprécie le plus avec les FlameGraphs c’est que l’on peut zoomer dessus en cliquant sur une frame :

Utiliser perf pour profiler la mémoire

Pour profiler la mémoire, nous allons utiliser la même technique avec une commande légèrement modifiée.

Il y a plusieurs manières de profiler la mémoire avec perf, on peut demander à perf d’enregistrer des événements OS liés à la mémoire, profiler une des méthodes système qui alloue la mémoire, ou utiliser pef mem. C’est cette dernière solution que l’on va utiliser.

Pour cela, il faut démarrer votre application via l’outil perf : perf mem record --call-graph dwarf -F 99 ./getting-started-1.0-SNAPSHOT-runner.

Quand l’application s’arrête, perf va enregistrer sur le disque les données de profil qui pourront alors être utilisées de la même manière que celles du profil CPU (via perf report, perf script et l’outil FlameGraph).

Pour aller plus loin

Un talk que j’ai donné sur le sujet (en anglais), démarre à la minute 44 : https://www.youtube.com/watch?v=TXnJ9eyoEhw.

Des conseils pour utiliser perf avec plein de recettes toutes faites : http://www.brendangregg.com/perf.html.

Un article décrivant en détail ce qu’est un FlameGraph : https://queue.acm.org/detail.cfm?id=2927301.

Laisser un commentaire

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.