Maintenez l'espace mémoire utilisé par votre application à un niveau optimal et évitez les erreurs de segmentation avec Firebase Crashlytics. Cette session explore notre suite complète d'outils de dépannage de la mémoire disponibles dans la console Firebase et dans Android Studio. Nous commencerons par expliquer comment exploiter les insights sur les ANR et les rapports GWP-ASan pour diagnostiquer les bugs de mémoire difficiles à identifier. Nous nous intéresserons ensuite à nos derniers outils pour les événements de mémoire insuffisante (OOM) et d'arrêt par le limiteur de mémoire (MLK), conçus spécifiquement pour vous aider à vous adapter aux nouvelles limites de mémoire introduites dans Android 17.
ApplicationExitInfo pour afficher les traces multithread et identifier les threads bloquants d'origine.ProfilingManager d'Android 17 pour collecter des instantanés de segment de mémoire désobscurcis qui peuvent être inspectés dans Perfetto ou via la CLI Android.Transcription
Polina Govorkova
Bonjour !
Bienvenue dans le module "Résoudre les problèmes de mémoire avec Firebase Crashlytics".
Attendez, quel titre d'entreprise !
Changeons un peu de sujet.
Créer des applications légères et efficaces avec Firebase Crashlytics
Oh, ça me semble mieux.
Je m'appelle Polina, je suis Senior Software Developer dans l'équipe Firebase Crashlytics et je serai votre présentatrice aujourd'hui.
À un moment donné de votre cycle de développement, vous aurez l'impression d'avoir résolu tous les bugs.
Les erreurs de saisie lors de l'analyse JSON, les vérifications de pointeurs nuls oubliées… Tout cela appartient au passé !
Vous vous demandez peut-être si vous avez terminé.
Eh bien, j'ai une mauvaise nouvelle pour vous : ce n'est pas le cas.
car ce qu'il vous restera, ce sont les problèmes difficiles.
Et je soutiens aujourd'hui que les deux catégories de problèmes les plus difficiles auxquelles vous serez confronté sont la réactivité et la mémoire.
La bonne nouvelle, c'est que nous sommes là pour vous aider.
Aujourd'hui, nous allons passer en revue les outils existants et nouveaux d'Android et de Crashlytics pour vous aider à identifier la cause principale des problèmes de réactivité qui se manifestent par des erreurs "L'application ne répond pas", des exceptions de mémoire insuffisante, des fuites de mémoire et des erreurs de mémoire native corrompue.
Découvrons tout cela plus en détail.
Pourquoi les problèmes de réactivité et de mémoire sont-ils si fréquents ?
Prenons un bug classique : vous déclarez une chaîne, vous la définissez sur une valeur nulle et, à un moment donné, vous essayez de lire sa longueur.
Quels sont ses avantages ?
Une bonne vieille exception NullPointerException.
Mais en réalité, c'est facile ici, car la trace de la pile d'exceptions pointe vers la ligne de code exacte qui a provoqué le plantage de votre application.
Regardez la trace de la pile à gauche.
java.lang.NullPointerException
at MainActivity.onCreate(MainActivity.kt:42)
at android.app.Activity.performCreate
at android.os.Handler.dispatchMessage
at android.os.Looper.loopOnce
at android.app.ActivityThread.mainNous pouvons voir immédiatement que le problème se situe à la ligne 42 du fichier MainActivity.kt.
Cette approche est valable pour la plupart des bugs mineurs.
Désormais, lorsqu'un problème de réactivité ou de mémoire survient, comme l'erreur "L'application ne répond pas" à droite, la trace de la pile ne pointe presque jamais vers le véritable coupable.
"main" prio=5 tid=1 Blocked
| group="main" sCount=1 dsCount=0 flags=1
| state=S schedstat=( 1450239 48192 145 )
at DataManager.getData(DataManager.kt:25)
- waiting to lock <0x0a1b> held by thread 12
at MainActivity.onResume(MainActivity.kt:50)
at android.app.Activity.performResume
at android.os.Handler.dispatchMessage
at android.os.Looper.loopOnce
at android.app.ActivityThread.mainDans le meilleur des cas, il ne vous montre que l'effet, c'est-à-dire un thread ou une allocation inoffensifs qui étaient en cours d'exécution au moment où l'application s'est figée ou a manqué de mémoire.
alors que la véritable cause principale s'est produite bien plus tôt, ou peut-être même ailleurs.
Heureusement, vous n'avez pas à deviner d'où vient le problème, car Firebase Crashlytics a la réponse.
Mais avant de nous retrousser les manches pour résoudre les problèmes, levez la main si vous connaissez Crashlytics.
Oh, super.
C'est formidable de voir toutes ces mains levées.
Pour ceux qui ne connaissent pas encore Firebase Crashlytics, il s'agit d'une solution de rapport d'erreurs en temps réel qui fournit des données de terrain, ainsi que des insights contextuels et basés sur l'IA, des conseils de dépannage et des signaux clairs sur vos nouvelles versions, afin de vous aider à résoudre les problèmes plus rapidement.
Crashlytics est très apprécié par nos développeurs. C'est la solution de création de rapports de plantages la plus adoptée sur mobile, qui alimente plus de 370 000 applications natives, dont 57 % des meilleures applications Android.
Pour voir comment Crashlytics nous aide à traquer les bugs, prenons un exemple concret.
Aujourd'hui, je souhaite vous raconter l'histoire de ChromaClip.
Il s'agit d'un extracteur de palette de couleurs vidéo que j'ai développé, et je souhaite vous expliquer comment j'ai résolu les bugs de réactivité et de mémoire lors de son développement.
L'idée derrière ChromaClip est simple : les utilisateurs choisissent une vidéo, par exemple un magnifique coucher de soleil ou une scène avec un superbe étalonnage des couleurs, et l'application analyse les images pour générer une palette de couleurs personnalisée. Les utilisateurs peuvent ensuite l'utiliser pour leurs créations, leur art numérique, leurs moodboards, etc.
Notre histoire commence juste avant la sortie de la version 1.0.
Je voulais m'assurer qu'elle était parfaitement stable avant le lancement. J'ai donc effectué des tests finaux en local sur plusieurs appareils, en sélectionnant la vidéo et en appuyant sur le bouton pour extraire la palette de couleurs. Et là, j'ai eu droit à la boîte de dialogue préférée de tous les développeurs Android.
ChromaClip ne répond pas.
Il s'agit d'une erreur "L'application ne répond pas", également appelée "ANR".
Polina Govorkova
Alors, que faire dans cette situation, à part paniquer ?
En suivant notre premier instinct, puisque le téléphone de test est branché directement sur l'ordinateur portable, nous ouvrons Android Studio et vérifions Logcat.
Nous voyons la trace de la pile pour le thread principal.
14:32:15.105 1205-1250/system_process E/ActivityManager: ANR in com.chromaclip.app (com.chromaclip.app/.MainActivity)
PID: 14205
Reason: Input dispatching timed out
Dumped ANR trace to /data/anr/traces.txt
"main" prio=5 tid=1 Native
| group="main" sCount=1 dsCount=0 flags=1 obj=0x72a8c3d0 self=0x759fb92000
| sysTid=14205 nice=-10 cgrp=default sched=0/0 handle=0x7623910548
| state=S schedstat=( 1450239120 4819203 1453 ) utm=132 stm=13 core=4 HZ=100
| stack=0x7fed2a0000-0x7fed2a2000 stackSize=8MB
at sun.misc.Unsafe.park(Native method)
- waiting on an unknown object
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:190)
at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:279)
at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:85)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:59)
at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source:1)
at com.chromaclip.app.extractor.PaletteExtractor.extractColors(PaletteExtractor.kt:42)Qu'est-ce que cela nous indique ?
Tout d'abord, en haut, nous avons la confirmation qu'il s'agit d'une ANR, qui s'exécute dans MainActivity.
Quoi d'autre ?
Nous voyons la raison : le délai d'envoi de l'entrée a expiré.
Cela signifie que l'utilisateur a appuyé sur l'écran.
Oui, nous l'avons fait.
Mais le thread principal était tellement occupé qu'il n'a pas pu traiter cet événement dans le délai de surveillance de cinq secondes d'Android.
Ensuite, nous voyons également que le système a sauvegardé le fichier dans traces.txt et, juste en dessous, logcat affiche la trace de la pile pour notre thread principal.
Notez que l'état est égal à S.
Notre thread principal n'utilise même pas activement le processeur.
"S" signifie "sommeil".
Pourquoi notre thread principal est-il en veille pendant que nous attendons ?
Examinons plus en détail les blocs de pile.
Ici, au milieu de la trace, nous voyons que notre thread principal est parqué dans Unsafe.park en attente d'un objet inconnu.
Pourquoi ne savons-nous pas ce qu'il attend ?
En fait, pour certaines erreurs ANR, par exemple lorsqu'un thread est bloqué par un verrou synchronisé Java classique, la trace de la pile du thread principal est suffisamment explicite pour nous indiquer ce qu'elle attend, c'est-à-dire ce qui détient le verrou.
Mais lorsque vous utilisez des primitives de simultanéité de niveau supérieur en arrière-plan, Logcat nous indique simplement que nous attendons un objet inconnu.
Nous ne savons donc pas quel objet nous attendons, quel thread de travail d'arrière-plan nous attendons spécifiquement, ni pourquoi cela prend plus de cinq secondes.
Comme nous effectuons des tests localement, nous pouvons extraire ce traces.txt de l'appareil et faire défiler manuellement des dizaines de threads en veille, puis éventuellement traquer le coupable ou utiliser des jetons pour demander à l'IA.
Dans certains cas, cela fonctionnerait, et en réalité, cela fonctionnerait pour ce cas.
Mais non seulement je n'ai pas envie de lire manuellement des vidages de threads bruts (franchement, c'est tellement 2019), mais je sais aussi que cette approche n'est pas du tout viable une fois que nous sommes en production.
Nous n'avons pas d'accès direct au fichier traces.txt sur les téléphones des utilisateurs, n'est-ce pas ?
Heureusement, nous avons intégré Firebase Crashlytics.
Parce qu'il élimine le besoin de recherche et va au fond de l'ANR, même en production.
Comment ?
C'est là qu'intervient l'API ApplicationExitInfo d'Android.
À partir d'Android 11, chaque fois que le système signale une erreur ANR ou met fin au processus de votre application, l'OS enregistre des métadonnées de sortie détaillées, y compris la raison exacte du plantage, l'état de la mémoire et une trace système complète pour chaque thread en cours d'exécution dans votre processus à ce moment-là.
Et le mieux, c'est que vous n'avez pas besoin d'installer de plug-ins supplémentaires ni d'écrire une seule ligne de code.
Si vous utilisez le SDK Crashlytics 18.3.5 ou une version ultérieure, cela fonctionne directement.
Ainsi, dès que votre application redémarre, le SDK Crashlytics interroge automatiquement ApplicationExitInfo, capture l'intégralité du dump multithread ainsi que la télémétrie de l'appareil, et l'envoie au backend Crashlytics.
Ouvrons la console Crashlytics et voyons ce qu'elle nous indique.
Il s'agit de la vue des détails du problème dans la console Firebase Crashlytics pour l'ANR. Sous l'onglet "Trace de la pile", nous voyons immédiatement le thread de calcul DefaultDispatcher.
DefaultDispatcher-worker-1 (Root blocking)
dalvik.system.VMRuntime.allocateNode (Native method)
android.graphics.Bitmap.createBitmap (Bitmap.java:1124)
android.graphics.Bitmap.createBitmap (Bitmap.java:1091)
com.chromaclip.app.extractor.FrameDecoder.getUncompressedFrame (FrameDecoder.kt:112)
com.chromaclip.app.extractor.PaletteExtractor$processVideo$2.invokeSuspend (PaletteExtractor.kt:65)
kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith (ContinuationImpl.kt:33)
kotlinx.coroutines.DispatchedTask.run (DispatchedTask.kt:106)
java.util.concurrent.ThreadPoolExecutor.runWorker (ThreadPoolExecutor.java:1167)
java.util.concurrent.ThreadPoolExecutor$Worker.run (ThreadPoolExecutor.java:641)
java.lang.Thread.run (Thread.java:923)Il s'agit en fait du thread qui est apparu tout en haut de Crashlytics et qui constitue le goulot d'étranglement de notre thread principal. Crashlytics le marque avec un badge appelé "Root blocking" (Blocage racine).
Comme l'explique l'info-bulle du badge, ce thread de travail est bien celui qui bloque le thread qui a déclenché l'ANR.
Sachez que Crashlytics propose également des badges intelligents pour d'autres coupables.
Par exemple, si un thread était bloqué lors de la lecture d'un fichier volumineux ou de l'exécution d'une requête de base de données très lente, et que cela déclenchait une erreur ANR, le thread serait tagué comme "Blocage racine d'E/S".
Et si deux threads étaient bloqués en attente l'un de l'autre, Crashlytics les marquerait comme étant en interblocage.
Examinons maintenant les blocs de pile eux-mêmes dans le thread bloquant racine.
Nous avons donc notre PaletteExtractor processVideo qui appelle FrameDecoder getUncompressedFrame, mis en surbrillance en bleu, et qui est complètement bloqué dans createBitmap, en essayant simplement d'allouer de la mémoire pour une seule image.
Puisque nous savons qu'il n'attend pas un autre verrou et qu'il n'est pas dans un appel réseau, notre principal suspect doit être la mémoire. Nous pouvons d'ailleurs le confirmer immédiatement en vérifiant la mémoire disponible de l'appareil, ici même dans la console Crashlytics.
Regardez les deux endroits où Crashlytics affiche cette information.
Dans l'onglet "Données" de l'événement spécifique, juste à côté de l'onglet "Trace de la pile" que nous venons d'explorer, et en haut à droite, qui indique la moyenne de tous les événements.
Dans les deux cas, il n'y a presque rien, même pas 100 Ko.
Réfléchissons.
Dans ce cas, comme le processus manquait de mémoire, l'Android Runtime a passé tout son temps à exécuter frénétiquement la récupération de mémoire, en essayant de trouver suffisamment d'espace pour le bitmap suivant, sans jamais y parvenir.
Le thread de travail est bloqué et l'UI est privée de ressources pendant plus de cinq secondes.
Voici donc un petit conseil.
Chaque fois que vous obtenez une erreur ANR associée à ce type de faible quantité de RAM disponible, cela signifie que l'erreur ANR est en fait une fuite de mémoire ou un problème de gonflement.
Maintenant, la question est de savoir si notre application est la victime ou si c'est elle qui monopolise la mémoire.
Revenons donc à la liste des problèmes de notre projet et voyons s'il y a d'autres éléments pertinents.
Polina Govorkova
Et regardez ici.
En bas, nous voyons la liste des problèmes, et juste au-dessus de notre ANR avec les huit événements, nous avons une exception OutOfMemory.
Il se peut donc que nous soyons les coupables, après tout.
C'est un indice très important.
En effet, sur Android, une erreur Java OutOfMemoryError (ou OOM) ne signifie pas seulement que le téléphone manquait de RAM.
Cela signifie que le processus de notre propre application a dépassé la limite de tas Java de l'environnement d'exécution Android.
À ce stade, mon hypothèse de travail est que les erreurs ANR et OOM sont en fait les deux faces d'une même pièce.
Selon le moment où le test est exécuté, ChromaClip peut se retrouver bloqué dans le garbage collection suffisamment longtemps pour déclencher une erreur ANR, ou épuiser complètement le tas de mémoire, ce qui entraîne une erreur OOM.
Examinons l'exception OutOfMemory pour en être sûrs.
Commençons par la trace de la pile.
java.lang.OutOfMemoryError: Failed to allocate a 256 byte allocation
at android.graphics.Bitmap.createBitmap
at android.view.LayoutInflater.inflate(LayoutInflater.java:532)
at com.chromaclip.app.SettingsActivity.onCreate(SettingsActivity.kt:21)Une petite minute.
Bitmap.createBitmap, ça me dit quelque chose.
On ne l'a pas déjà vu ?
Sauf que cette fois, regardez d'où elle est appelée.
L'allocation de 256 octets a échoué lors de l'ouverture de l'activité des paramètres.
Nous tombons à nouveau dans le même piège.
Si votre application plante parce qu'elle ne peut pas gonfler une petite icône de paramètres, l'activité des paramètres n'est pas en cause.
C'est juste l'appelant malchanceux qui a demandé une goutte de mémoire alors qu'il n'y en avait pas.
Pour trouver le véritable coupable, nous devons donc faire un zoom arrière et voir tout ce qui se trouvait en mémoire au moment du plantage.
En d'autres termes, nous avons besoin d'une empreinte de la mémoire Java.
Si vous savez comment déclencher une erreur OOM, vous pouvez récupérer manuellement une empreinte de la mémoire Java à l'aide du profileur de mémoire Android Studio.
Mais lorsqu'une erreur OOM se produit de manière imprévisible, lors de vos tests ou dans la nature, cette méthode ne fonctionne pas.
Polina Govorkova
C'est un autre moment fort pour Firebase Crashlytics.
En plus des informations sur la fermeture de l'application, Crashlytics s'intègre désormais à l'API ProfilingManager d'Android.
L'API ProfilingManager d'Android 17 est optimisée avec le profilage de la mémoire déclenché par le système, et c'est exactement ce dont nous avons besoin ici.
Cela signifie que dès que votre application rencontre une erreur OutOfMemory ou dépasse les limites de mémoire du système, l'OS capture un instantané de segment de mémoire en arrière-plan. Ensuite, dans Crashlytics, la collecte déclenchée est disponible sans frais, à condition que vous utilisiez le SDK Crashlytics 20.1.0 ou une version ultérieure.
Crashlytics regroupe ensuite automatiquement vos arrêts OOM et Memory Limiter (nous en reparlerons plus tard) dans le tableau de bord.
Si vous activez la collecte des empreintes de la mémoire via la console Firebase ou la CLI, Crashlytics commencera à collecter automatiquement ces instantanés de segment de mémoire et à les désobscurcir. Vous obtiendrez ainsi une radiographie complète de tout ce qui se trouve dans le segment de mémoire Java de votre application au moment du plantage.
Voici à quoi ressemble l'erreur de mémoire insuffisante de ChromaClip dans la console Crashlytics une fois le tas de mémoire capturé.
En haut, vous voyez une explication de la raison pour laquelle Java a épuisé le tas de mémoire et pourquoi cela s'est produit.
Vous découvrirez également des conseils de dépannage utiles et des liens vers des ressources externes.
Dans l'onglet "Trace de la pile", juste au-dessus de l'activité des paramètres trompeuse, Crashlytics signale que les traces de la pile OOM ne sont pas déterministes, afin que les utilisateurs ne perdent pas de temps à examiner la trace de la pile comme nous venons de le faire.
Ensuite, Crashlytics associe le fichier Perfetto désobscurci, l'empreinte de la mémoire, pour la session.
À partir de là, nous pouvons télécharger le fichier.
Nous pouvons parcourir tous les dumps dans Google Cloud Storage ou les ouvrir dans l'UI de Perfetto.
En cliquant sur "Analyze" (Analyser) dans l'UI Perfetto, vous ouvrez l'explorateur d'empreintes de la mémoire Perfetto.
Levez la main si vous avez déjà vu celui-ci.
D'accord, quelques mains se sont levées.
Pour moi, cette vue est très intimidante, mais dans ce cas, le problème est en fait assez simple.
Examinez l'ensemble de barres massives qui s'étendent sur la quasi-totalité du tas Java.
Il s'agit donc principalement d'un tableau d'objets de paires, chacun encapsulant une couleur et une instance de chaîne.
Oh !
Je crois que je sais ce qui se passe.
Notre code alloue une couleur et des données complémentaires pour chaque pixel de chaque image, et en conserve des millions dans une liste.
Il n'est donc pas étonnant que l'activité "Paramètres" n'ait pas pu allouer ces 256 octets.
Au moins, un mystère est résolu.
Résolvons ce problème.
À ce stade, plusieurs options s'offrent à nous.
Comme Perfetto nous a déjà orientés vers PaletteExtractor, nous pourrions ouvrir notre IDE et réécrire la boucle manuellement, ou utiliser l'agent de codage IA.
Nous pouvons également contourner l'inspection manuelle du tas de mémoire et utiliser la CLI Android.
# 1. Install Android CLI & Perfetto skills
$ android skills add android-cli perfetto
# 2. List recent heap dumps from Crashlytics
$ android signals heaps list --kind oom --timeframe=90d
# 3. Download a heap dump as a Perfetto trace
$ android signals heaps download <heap-dump-id> \
--output ./heaps
# 4. Query top retained objects in the heap dump
$ trace_processor query ./heaps/oom.perfetto-trace ...
# 5. Fetch matching Crashlytics events
$ android signals heaps describe <heap-dump-id>Ainsi, l'installation des compétences Android CLI et Perfetto vous permettra de lister et de télécharger les empreintes de la mémoire Crashlytics désobscurcies, d'interroger les principaux objets conservés à partir de ces empreintes de la mémoire via trace_processor, et de récupérer les données de problème Crashlytics correspondantes, telles que les clés personnalisées, les journaux, les fils d'Ariane, les déploiements associés et plus encore, le tout depuis votre terminal.
Mieux encore, regardez en bas des diapositives.
N'exécutez pas ces commandes vous-même.
Encore une fois, demandez à votre agent de codage IA préféré en langage naturel d'inspecter la dernière empreinte de la mémoire OOM à l'aide de la commande Android et de déployer le correctif.
# Or ask your favorite LLM in natural language
> Using `android` command, inspect the latest heap dump
for my OOM issue and propose a fix
● Ran android signals heaps list · download · describe
● Analyzed ./heaps/oom.perfetto-trace (perfetto)
● Updated PaletteExtractor.kt (+6 -4 lines)Il disposera de suffisamment de contexte pour effectuer l'ensemble du flux à votre place.
Il peut lister les erreurs de mémoire insuffisante, télécharger la trace, exécuter l'analyse et générer le correctif de code directement dans votre projet.
Regardez ce correctif dans le fichier PaletteExtractor.
// PaletteExtractor.kt
fun extractPalette(framePixels: IntArray): SparseIntArray {
- val pixelColors = mutableListOf<Pair<Color, String>>() // 8.3M objects per 4K frame!
- for (px in framePixels) {
- pixelColors.add(Color.valueOf(px) to "#%06X".format(px))
- }
+ val colorHistogram = SparseIntArray() // Downsampled & packed ARGB ints!
+ for (i in framePixels.indices step 16) {
+ val argb = framePixels[i]
+ colorHistogram.put(argb, colorHistogram.get(argb) + 1)
+ }
+ return colorHistogram
}Au lieu de millions d'allocations d'objets, nous sous-échantillonnons désormais l'image et comptons directement les entiers bruts.
Appliquons donc le correctif, fermons les problèmes, mettons à jour notre version et continuons les tests.
Si tout se passe bien, non seulement l'erreur de mémoire insuffisante devrait disparaître, mais comme nous n'aurons plus de problème de récupération de mémoire, l'erreur ANR devrait également disparaître.
Nous avons donc déployé la version corrigée sur nos appareils de test, sélectionné un nouvel extrait et commencé à extraire des palettes.
Et l'UI se fige à nouveau.
Encore une ANR.
En effectuant d'autres tests, nous avons découvert que sur certains appareils, l'ANR se produit toujours, mais que sur d'autres, l'application plante de manière aléatoire, et assez fréquemment, plus fréquemment qu'avec ces erreurs OOM.
Mais nous avons corrigé l'erreur de mémoire insuffisante. Alors, que se passe-t-il ?
Polina Govorkova
Revenons à Crashlytics.
Commençons par la bonne nouvelle.
Consultez notre liste de problèmes.
Notre exception de mémoire insuffisante a complètement disparu, comme nous l'espérions.
La mauvaise nouvelle ?
Maintenant que le ChromaClip survit un peu plus longtemps, il a le temps de découvrir ce qui se cachait d'autre sous la surface.
L'ANR n'a donc pas disparu.
Nous avons maintenant deux plantages natifs en bas, heureusement avec seulement quelques événements.
Et tout en haut, avec 11 événements, nous avons l'arrêt du limiteur de mémoire, qui, heureusement, est déjà marqué comme ayant l'empreinte de la mémoire.
Attends.
Qu'est-ce qu'un arrêt par le limiteur de mémoire ?
Pouvez-vous lever la main si vous en avez déjà entendu parler ?
Très bien.
Quelques mains rares.
Apprenons ensemble.
Si vous n'avez jamais vu Memory Limiter kill, c'est parce qu'il s'agit d'une nouveauté d'Android 17.
Une erreur OOM Java ne se déclenche que lorsque vous dépassez la limite du tas de mémoire Java géré. Toutefois, lorsque l'espace mémoire total utilisé par votre application inclut également de la RAM native, comme des tampons graphiques et des allocations C++, il n'y avait auparavant pas de plafond par application.
Nous avions donc le tueur de mémoire faible, qui était global.
Il intervenait lorsqu'une application consommait toute la RAM du système jusqu'à ce que le téléphone soit complètement saturé.
À partir d'Android 17, le limiteur de mémoire plafonne l'espace mémoire utilisé par chaque application.
Ainsi, lorsque le processus consomme une quantité disproportionnée de RAM, l'OS met fin à cette application spécifique.
Crashlytics capture tous ces détails et regroupe les arrêts dans le tableau de bord.
En cliquant sur "Memory Limiter kill" dans Crashlytics, comme pour les erreurs OOM, vous obtenez une explication de la raison pour laquelle l'OS est intervenu. Vous n'avez donc pas besoin de mémoriser ce que je viens de dire.
Vous obtenez également des conseils pour résoudre le problème, ainsi que des liens directs vers la documentation sur l'application des limites de mémoire Android.
Comme pour les erreurs OOM, la trace de la pile incrimine un frame innocent lorsque l'OS a mis fin au processus, et le tableau de bord Crashlytics le met en évidence.
Désormais, les arrêts par le limiteur de mémoire se produisent principalement dans le code natif, car les environnements natifs ne disposent pas de la couche qui gère la sécurité et les allocations de mémoire.
Sinon, une erreur de mémoire insuffisante se serait produite.
La première question à se poser est donc la suivante : nos objets Kotlin conservent-ils d'énormes allocations d'objets natifs ?
Ou s'agit-il d'un code C++ brut qui présente une fuite ?
Cliquez sur "Analyze in Perfetto UI" (Analyser dans l'UI de Perfetto) pour le savoir.
Consultez l'onglet "Aperçu" dans l'explorateur d'empreintes de la mémoire Perfetto.
Dans les informations générales, nous constatons que la mémoire RSS anonyme plus l'espace d'échange indiquent que la mémoire totale du processus a atteint 4 Go.
OK.
Pourtant, en dessous, nous voyons que sous "Bytes Retained by Heap" (Octets conservés par le tas), l'intégralité de notre tas géré, y compris la mémoire native liée aux objets Kotlin tels que les bitmaps, n'est que de 42 Mo.
OK.
En 10 secondes, nous avons donc éliminé tout le code Kotlin et Java.
Le problème doit donc provenir d'une fuite dans une couche C++ native.
Et comme ChromaClip dispose d'une fine couche C++ pour le traitement des images, nous n'avons pas à chercher bien loin.
En pointant notre agent de codage ou le profileur natif d'Android Studio sur NativePaletteEngine.cpp processFrame, le bug est immédiatement visible.
// NativePaletteEngine.cpp
void NativePaletteEngine::processFrame(const uint8_t* rgba, size_t size) {
uint8_t* scratch = new uint8_t[size]; // Temporary frame buffer
transformColorSpace(rgba, scratch, size);
// BUG: Allocated every frame, but never freed!
+ delete[] scratch; // Free native buffer when done
}Ainsi, sur chaque image vidéo, notre code C++ alloue un tampon de travail temporaire, qui n'est pas si temporaire que ça si vous ne le libérez pas. Quelle erreur de débutant !
Il est temps de faire le lien.
Que s'est-il passé ?
Une fois la fuite Java corrigée, la fuite C++ aura le temps de s'accumuler et de se propager.
Sur Android 17, le limiteur de mémoire l'a détecté et a arrêté l'application.
Mais sur les appareils plus anciens où Memory Limiter n'était pas encore disponible, cela se manifestait par un ANR.
Tout s'explique enfin.
Ajoutons delete[] scratch ici, incrémentons la version et publions la mise à jour.
Maintenant que les arrêts forcés par le limiteur de mémoire et les ANR ont disparu, nous devrions pouvoir publier la version 1.0.
Passionnant.
Et grâce à la rapidité et à la stabilité de ChromaClip, notre application décolle.
Des milliers de personnes mettent en ligne et remixent des vidéos chaque jour.
Mais avec tout ce trafic, vous vous souvenez des mystérieux plantages natifs que nous avons vus plus tôt en bas du tableau de bord ?
Eh bien, ils sont déchaînés eux aussi.
Soudain, il y en a beaucoup plus.
Et notre travail n'est pas encore terminé.
Polina Govorkova
Ouvrez ce plantage natif dans Crashlytics et examinez la trace de la pile.
Il n'y a pas une seule ligne de code provenant de notre propre application.
RenderThread plante au cœur de libc.so lors de la libération de la mémoire.
Autre conseil : lorsqu'un thread système plante dans l'allocateur de mémoire sans qu'aucun code de votre application ne soit présent, c'est un signe classique de corruption de la mémoire native.
Notre code C++ a donc corrompu ou libéré le tampon plus tôt, et le thread de rendu l'a rencontré plus tard.
Alors, comment attraper le vrai coupable au moment exact où la corruption s'est produite ?
Android nous fournit deux outils puissants, et Crashlytics les prend en charge tous les deux de manière native.
Tout d'abord, GWP-ASan, un filet de sécurité léger conçu spécifiquement pour la production.
Il échantillonne une infime partie de vos allocations de segments de mémoire natifs et les entoure de pages de garde, de sorte que la surcharge du processeur et de la mémoire est presque nulle.
Sur des milliers de sessions utilisateur, lorsqu'un bug touche une mémoire libérée ou hors limites sur une allocation échantillonnée, GWP-ASan le détecte immédiatement.
Toutefois, comme GWP-ASan est, par nature, probabiliste, lorsque vous testez des éléments localement, vous ne voulez pas exécuter votre téléphone de test pendant 16 heures ou plus pour atteindre l'échantillon aléatoire.
Alors, qu'obtenez-vous ?
Pour le débogage local déterministe, vous disposez de l'extension MTE (Memory Tagging Extension) ARM.
MTE utilise un pointeur matériel et des tags de mémoire pour vérifier chaque allocation. Il est disponible sur les appareils ARM v9 comme le Pixel 8 et les modèles plus récents.
Associez-le donc à GWP-ASan en production pour profiter du meilleur des deux mondes.
Comme vous vous en doutez, lorsque l'un de ces outils détecte le bug, Crashlytics ajoute automatiquement un badge au problème et vous permet de filtrer par GWP-ASan ou MTE, et d'épingler une fiche d'informations indiquant la cause exacte de la corruption dans le tableau de bord.
Voyons ce qui se passe dans la console Crashlytics lorsque GWP-ASan détecte le plantage.
À côté de l'onglet de trace de la pile standard, nous avons la trace de la pile de mémoire.
En haut, nous voyons la cause exacte de la corruption : Use After Free.
Au lieu d'afficher le plantage du thread de rendu dans libc.so, GWP-ASan nous donne désormais l'historique complet de ce tampon dans notre propre effect_pipeline.cc.
Sous "Allocated By" (Alloué par), nous voyons où le tampon a été créé.
Juste en dessous de "Deallocated By" (Désalloué par), nous voyons le coupable exact qui l'a libéré du thread de rendu.
Et cela se cache à la ligne 208 de notre pipeline d'effets.
Nous ouvrons donc notre code natif dans l'EffectPipeline, et le bug nous nargue.
// effect_pipeline.cc
void EffectPipeline::onFilterSwitched(FilterPresetId preset) {
// BUG: Freed on worker thread while RenderThread is still reading it!
+ std::lock_guard<std::mutex> lock(lut_mutex_);
releaseLutBuffer(); // Frees old color-grading LUT
loadFilterPreset(preset); // Allocates new color-grading LUT
}Chaque fois qu'un utilisateur change de filtre de couleur dans le thread d'arrière-plan, nous libérons l'ancien tampon pendant que RenderThread le lit encore.
Nous ajoutons donc le verrou mutex autour de l'échange de tampon, déployons la mise à jour et constatons que les rapports GWP-ASan et tous ces plantages mystérieux disparaissent presque du jour au lendemain.
Et notre métrique d'utilisateurs n'ayant pas subi de plantage a grimpé en flèche pour atteindre 100 %.
Bravo !
Rappelez-vous, au début de la présentation, nous nous sommes demandé : "Ai-je terminé ?"
Juste avant d'être frappé par pratiquement tous les boss de réactivité et de mémoire du jeu.
Les erreurs ANR, les erreurs de mémoire insuffisante, les arrêts dus à la limite de mémoire et les plantages dus à une corruption de la mémoire native sont désormais tous résolus.
Et même si la trace de la pile est trompeuse et que le coupable se cache quelque part dans les threads en C++, Android et Firebase Crashlytics vous fournissent les outils nécessaires pour le traquer et publier votre application en toute confiance.
Polina Govorkova
Récapitulons ce que vous pouvez utiliser dès que vous serez de retour sur votre ordinateur portable : les insights sur les threads ANR et d'autres métadonnées pertinentes via ApplicationExitInfo, la collecte automatique des déclencheurs d'arrêt OOM et du limiteur de mémoire via ProfilingManager, et la compatibilité complète avec GWP-ASan et Arm MTE pour la corruption de la mémoire native.
Nous lançons également en preview privée les empreintes de la mémoire sur le terrain sur Android 17 ou version ultérieure.
Si vous souhaitez être parmi les premiers à essayer la collecte automatique des empreintes de la mémoire sur Firebase Crashlytics et à les analyser avec Perfetto et la CLI Android, prenez une photo rapide de la diapositive et inscrivez-vous via le lien.
Maintenant que nous avons abordé la plupart des sujets, je tiens à vous remercier d'avoir participé à cette session et de m'avoir accordé de votre temps.
Que votre thread principal reste débloqué, que votre tas reste propre et que votre prochaine version soit la plus stable à ce jour.
Bon débogage !
Merci encore.
java.lang.NullPointerException
at MainActivity.onCreate(MainActivity.kt:42)
at android.app.Activity.performCreate
at android.os.Handler.dispatchMessage
at android.os.Looper.loopOnce
at android.app.ActivityThread.main"main" prio=5 tid=1 Blocked
| group="main" sCount=1 dsCount=0 flags=1
| state=S schedstat=( 1450239 48192 145 )
at DataManager.getData(DataManager.kt:25)
- waiting to lock <0x0a1b> held by thread 12
at MainActivity.onResume(MainActivity.kt:50)
at android.app.Activity.performResume
at android.os.Handler.dispatchMessage
at android.os.Looper.loopOnce
at android.app.ActivityThread.main14:32:15.105 1205-1250/system_process E/ActivityManager: ANR in com.chromaclip.app (com.chromaclip.app/.MainActivity)
PID: 14205
Reason: Input dispatching timed out
Dumped ANR trace to /data/anr/traces.txt
"main" prio=5 tid=1 Native
| group="main" sCount=1 dsCount=0 flags=1 obj=0x72a8c3d0 self=0x759fb92000
| sysTid=14205 nice=-10 cgrp=default sched=0/0 handle=0x7623910548
| state=S schedstat=( 1450239120 4819203 1453 ) utm=132 stm=13 core=4 HZ=100
| stack=0x7fed2a0000-0x7fed2a2000 stackSize=8MB
at sun.misc.Unsafe.park(Native method)
- waiting on an unknown object
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:190)
at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:279)
at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:85)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:59)
at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source:1)
at com.chromaclip.app.extractor.PaletteExtractor.extractColors(PaletteExtractor.kt:42)DefaultDispatcher-worker-1 (Root blocking)
dalvik.system.VMRuntime.allocateNode (Native method)
android.graphics.Bitmap.createBitmap (Bitmap.java:1124)
android.graphics.Bitmap.createBitmap (Bitmap.java:1091)
com.chromaclip.app.extractor.FrameDecoder.getUncompressedFrame (FrameDecoder.kt:112)
com.chromaclip.app.extractor.PaletteExtractor$processVideo$2.invokeSuspend (PaletteExtractor.kt:65)
kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith (ContinuationImpl.kt:33)
kotlinx.coroutines.DispatchedTask.run (DispatchedTask.kt:106)
java.util.concurrent.ThreadPoolExecutor.runWorker (ThreadPoolExecutor.java:1167)
java.util.concurrent.ThreadPoolExecutor$Worker.run (ThreadPoolExecutor.java:641)
java.lang.Thread.run (Thread.java:923)java.lang.OutOfMemoryError: Failed to allocate a 256 byte allocation
at android.graphics.Bitmap.createBitmap
at android.view.LayoutInflater.inflate(LayoutInflater.java:532)
at com.chromaclip.app.SettingsActivity.onCreate(SettingsActivity.kt:21)# 1. Install Android CLI & Perfetto skills
$ android skills add android-cli perfetto
# 2. List recent heap dumps from Crashlytics
$ android signals heaps list --kind oom --timeframe=90d
# 3. Download a heap dump as a Perfetto trace
$ android signals heaps download <heap-dump-id> \
--output ./heaps
# 4. Query top retained objects in the heap dump
$ trace_processor query ./heaps/oom.perfetto-trace ...
# 5. Fetch matching Crashlytics events
$ android signals heaps describe <heap-dump-id># Or ask your favorite LLM in natural language
> Using `android` command, inspect the latest heap dump
for my OOM issue and propose a fix
● Ran android signals heaps list · download · describe
● Analyzed ./heaps/oom.perfetto-trace (perfetto)
● Updated PaletteExtractor.kt (+6 -4 lines)// PaletteExtractor.kt
fun extractPalette(framePixels: IntArray): SparseIntArray {
- val pixelColors = mutableListOf<Pair<Color, String>>() // 8.3M objects per 4K frame!
- for (px in framePixels) {
- pixelColors.add(Color.valueOf(px) to "#%06X".format(px))
- }
+ val colorHistogram = SparseIntArray() // Downsampled & packed ARGB ints!
+ for (i in framePixels.indices step 16) {
+ val argb = framePixels[i]
+ colorHistogram.put(argb, colorHistogram.get(argb) + 1)
+ }
+ return colorHistogram
}// NativePaletteEngine.cpp
void NativePaletteEngine::processFrame(const uint8_t* rgba, size_t size) {
uint8_t* scratch = new uint8_t[size]; // Temporary frame buffer
transformColorSpace(rgba, scratch, size);
// BUG: Allocated every frame, but never freed!
+ delete[] scratch; // Free native buffer when done
}// effect_pipeline.cc
void EffectPipeline::onFilterSwitched(FilterPresetId preset) {
// BUG: Freed on worker thread while RenderThread is still reading it!
+ std::lock_guard<std::mutex> lock(lut_mutex_);
releaseLutBuffer(); // Frees old color-grading LUT
loadFilterPreset(preset); // Allocates new color-grading LUT
}