Tools und Leistung

Fehlerbehebung bei Arbeitsspeicherproblemen mit Firebase Crashlytics

Lautsprecher

  • Polina Govorkova

    Senior Software Developer

Info

Über den Vortrag

Mit Firebase Crashlytics können Sie den Speicherbedarf Ihrer App gering halten und Speicherzugriffsfehler vermeiden. In dieser Session stellen wir Ihnen unsere umfassende Suite von Tools zur Fehlerbehebung bei Arbeitsspeicherproblemen vor, die in der Firebase Console und in Android Studio verfügbar sind. Zuerst sehen wir uns an, wie ihr ANR-Statistiken und GWP-ASan-Berichte nutzen könnt, um schwer zu findende Speicherfehler zu diagnostizieren. Anschließend stellen wir euch unsere neuesten Tools für OOM- (Out-Of-Memory) und MLK-Ereignisse (Memory Limiter Kill) vor, die speziell dafür entwickelt wurden, euch bei der Anpassung an die neuen Speicherlimits in Android 17 zu unterstützen.

Zusammenfassung

  • „App antwortet nicht“-Fehler sind oft auf Engpässe im Hintergrund oder auf einen erschöpften Arbeitsspeicher zurückzuführen, nicht auf die aktive CPU-Auslastung. Firebase Crashlytics fragt ApplicationExitInfo ab, um Multithread-Traces anzuzeigen und die blockierenden Threads zu identifizieren.
  • Out of Memory-Abstürze treten auf, wenn Apps die Java-Heap-Limits der Laufzeit überschreiten. Crashlytics lässt sich in ProfilingManager von Android 17 einbinden, um de-obfuskierte Heap-Snapshots zu erfassen, die in Perfetto oder über die Android-Befehlszeile geprüft werden können.
  • Der Memory Limiter von Android 17 beendet einzelne Prozesse, die übermäßig viel RAM verbrauchen, einschließlich nativer Zuweisungen. Crashlytics gruppiert diese Beendigungen im Dashboard zusammen mit Heap-Dumps, um C++-Speicherlecks von verwalteten Heap-Zuweisungen zu trennen.
  • Eine Beschädigung des nativen Arbeitsspeichers tritt häufig in Systemthreads und nicht im App-Code auf. Crashlytics unterstützt GWP-ASan und Arm MTE. GWP-ASan-Berichte zeigen die genauen Stellen für die Zuweisung und Freigabe von Fehlern wie Use-After-Free an.

Ressourcen

Gemini Notebook

Themen

  • firebase-crashlytics
  • android-memory
  • anr
  • out-of-memory
  • profiling-manager
  • perfetto
  • gwp-asan
  • arm-mte

Transkript

Einführung

  • Polina Govorkova

    Hallo zusammen.

  • Willkommen bei der Fehlerbehebung für Speicherprobleme mit Firebase Crashlytics.

  • Moment, was für ein Titel.

  • Lass uns ein bisschen Abwechslung reinbringen.

  • Apps mit Firebase Crashlytics schlank und leistungsstark halten.

  • Oh, das klingt besser.

  • Mein Name ist Polina. Ich bin Senior Software Developer im Firebase Crashlytics-Team und heute Ihre Moderatorin.

  • Irgendwann im Entwicklungszyklus werden Sie das Gefühl haben, alle Fehler behoben zu haben.

  • JSON-Parsing-Tippfehler, vergessene Nullzeigerprüfungen – alles weg!

  • Sie fragen sich vielleicht: „Bin ich fertig?“

  • Dann habe ich schlechte Nachrichten für Sie, denn das sind Sie nicht.

  • Denn was übrig bleibt, sind die wirklich schwierigen Probleme.

  • Und ich behaupte heute, dass die beiden Kategorien der schwierigsten Probleme, mit denen Sie konfrontiert werden, Reaktionsfähigkeit und Speicher sind.

  • Die gute Nachricht ist, dass wir Ihnen helfen können.

  • Heute stellen wir Ihnen vorhandene und brandneue Tools in Android und Crashlytics vor, mit denen Sie die Ursachen von Problemen mit der Reaktionsfähigkeit ermitteln können, die sich durch „App antwortet nicht“-Fehler, Out of Memory-Ausnahmen, Speicherlecks und native Speicherschäden äußern.

  • Sehen wir uns das genauer an.

  • Warum sind Probleme mit der Reaktionsfähigkeit und dem Arbeitsspeicher so berüchtigt?

  • Nehmen wir einen klassischen Fehler: Sie deklarieren einen String, setzen ihn auf „null“ und versuchen irgendwann, seine Länge zu lesen.

  • Vorteile

  • Eine gute alte NullPointerException.

  • Aber hier ist es eigentlich ganz einfach, denn der Stacktrace der Ausnahme verweist auf die genaue Codezeile, die Ihre App zum Absturz gebracht hat.

  • Sehen Sie sich den Stacktrace auf der linken Seite an.

    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
  • Wir sehen sofort, dass das Problem in Zeile 42 der MainActivity.kt liegt.

  • Dieser Ansatz gilt für die meisten trivialen Fehler.

  • Wenn ein Problem mit der Reaktionsfähigkeit oder dem Arbeitsspeicher auftritt, wie z. B. die Meldung „App antwortet nicht“ rechts, verweist der Stacktrace fast nie auf die tatsächliche Ursache.

    "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.main
  • Im besten Fall wird nur der Effekt angezeigt, d. h. ein harmloser Thread oder eine harmlose Zuweisung, die gerade ausgeführt wurde, als die App eingefroren ist oder Out of Memory auftrat.

  • Die eigentliche Ursache liegt jedoch viel weiter zurück oder vielleicht sogar an einer ganz anderen Stelle.

  • Glücklicherweise müssen Sie bei diesen Problemen nicht im Dunkeln tappen, denn Firebase Crashlytics hat die Antwort für uns.

  • Bevor wir mit der Fehlerbehebung beginnen, heben Sie bitte die Hand, wenn Sie mit Crashlytics vertraut sind.

  • Oh, super.

  • Ich freue mich, dass so viele von Ihnen sich gemeldet haben.

  • Für alle, die neu hier sind: Firebase Crashlytics ist eine Echtzeitlösung für Fehlerberichte, die Felddaten sowie kontextbezogene und KI-gestützte Informationen, Tipps zur Fehlerbehebung und klare Signale zu Ihren neuen Releases liefert, damit Sie Probleme schneller beheben können.

  • Crashlytics ist bei unseren Entwicklern sehr beliebt und die am häufigsten verwendete Lösung für Absturzberichte auf Mobilgeräten. Sie wird in über 370.000 nativen Apps eingesetzt, darunter 57 % der Top-Android-Apps.

  • Sehen wir uns ein konkretes Beispiel an, wie Crashlytics uns bei der Fehlersuche hilft.

  • Heute möchte ich euch die Geschichte eines ChromaClips erzählen.

  • Ich habe ein Tool zur Extraktion von Farbpaletten aus Videos entwickelt und möchte euch erzählen, wie ich die Probleme mit der Reaktionsfähigkeit und dem Arbeitsspeicher während der Entwicklung gelöst habe.

  • Die Idee hinter ChromaClip ist einfach: Nutzer wählen ein Video aus, zum Beispiel einen Clip mit einem wunderschönen Sonnenuntergang oder eine Szene mit tollem Color Grading. Die App analysiert die Frames und generiert eine benutzerdefinierte Farbpalette, die Nutzer dann für ihre Designs, digitale Kunst, Moodboards oder andere Zwecke verwenden können.

  • Unsere Geschichte beginnt kurz vor dem Release der Version 1.0.

  • Ich wollte sichergehen, dass alles vor dem Launch einwandfrei funktioniert. Also habe ich letzte Tests lokal auf mehreren Geräten durchgeführt, das Video ausgewählt, auf die Schaltfläche zum Extrahieren der Farbpalette getippt und – wie sollte es anders sein – das Lieblingsdialogfeld aller Android-Entwickler erhalten.

  • ChromaClip reagiert nicht.

  • Das ist ein „App antwortet nicht“-Fehler, auch ANR genannt.

ANRs debuggen und analysieren

  • Polina Govorkova

    Was tun wir also in dieser Situation, außer in Panik zu geraten?

  • Da das Test-Smartphone direkt mit dem Laptop verbunden ist, öffnen wir Android Studio und prüfen Logcat.

  • Wir sehen den Stacktrace für den Hauptthread.

    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)
  • Was sagt sie uns eigentlich?

  • Ganz oben sehen wir, dass es sich um einen ANR-Fehler handelt, der in der MainActivity auftritt.

  • Was noch?

  • Wir sehen den Grund: „input dispatching timed out“ (Zeitüberschreitung bei der Eingabeverteilung).

  • Das bedeutet, dass der Nutzer auf das Display getippt hat.

  • Ja, das haben wir.

  • Der Hauptthread war jedoch so ausgelastet, dass er dieses Ereignis nicht innerhalb des fünfsekündigen Watchdog-Timeouts von Android verarbeiten konnte.

  • Außerdem sehen wir, dass das System die Datei in traces.txt abgelegt hat. Direkt darunter gibt Logcat den Stacktrace für unseren Hauptthread aus.

  • Beachten Sie, dass der Status dort „S“ ist.

  • Unser Hauptthread verbraucht nicht einmal aktiv CPU.

  • „S“ steht für „sleeping“ (schlafen).

  • Warum schläft der Hauptthread, während wir warten?

  • Sehen wir uns die Stapelframes genauer an.

  • Hier in der Mitte des Trace sehen wir, dass unser Hauptthread in Unsafe.park geparkt ist und auf ein unbekanntes Objekt wartet.

  • Warum wissen wir nicht, worauf er wartet?

  • Bei einigen ANRs, z. B. wenn ein Thread durch eine klassische Java-Synchronisierungssperre blockiert wird, ist der Stacktrace des Hauptthreads tatsächlich so freundlich, uns mitzuteilen, worauf er wartet und was die Sperre hält.

  • Wenn Sie jedoch ein höherwertiges Nebenläufigkeits-Primitiv verwenden, erfahren wir von Logcat nur, dass wir auf ein unbekanntes Objekt warten.

  • Wir wissen also nicht, auf welches Objekt wir warten, auf welchen Hintergrund-Arbeitsthread wir genau warten oder warum es länger als 5 Sekunden dauert.

  • Da wir lokal testen, könnten wir die traces.txt vom Gerät abrufen und manuell durch Dutzende von schlafenden Threads scrollen, um den Verursacher zu finden, oder die Tokens verbrennen, indem wir die KI fragen.

  • In einigen Fällen würde das tatsächlich funktionieren, und in diesem Fall würde es auch funktionieren.

  • Ich möchte nicht nur keine Thread-Dumps manuell lesen – das ist wirklich nicht mehr zeitgemäß –, sondern ich weiß auch, dass dieser Ansatz in der Produktion überhaupt nicht praktikabel ist.

  • Wir haben nur keinen direkten Zugriff auf die traces.txt-Datei auf den Smartphones der Nutzer, oder?

  • Es ist also gut, dass wir Firebase Crashlytics eingebunden haben.

  • Weil die Suche entfällt und die Ursache des ANR-Fehlers auch in der Produktion ermittelt wird.

  • Wie?

  • Hier kommt die ApplicationExitInfo API von Android ins Spiel.

  • Seit Android 11 zeichnet das Betriebssystem immer dann, wenn das System einen ANR-Fehler meldet oder den App-Prozess beendet, umfangreiche Metadaten zum Beenden auf. Dazu gehören der genaue Grund für den Absturz, der Speicherstatus und ein vollständiger System-Trace für jeden Thread, der zu diesem Zeitpunkt in Ihrem Prozess ausgeführt wird.

  • Und das Beste daran: Sie müssen keine zusätzlichen Plug-ins installieren oder eine einzige Zeile Code schreiben.

  • Wenn Sie das Crashlytics SDK 18.3.5 oder höher verwenden, funktioniert es sofort.

  • Sobald Ihre App neu gestartet wird, fragt das Crashlytics SDK automatisch die ApplicationExitInfo ab, erfasst den vollständigen Multithread-Dump zusammen mit den Gerätetelemetriedaten und sendet ihn an das Crashlytics-Backend.

  • Öffnen wir die Crashlytics-Konsole und sehen wir uns an, was sie uns sagt.

  • Dies ist die Detailansicht des ANR-Fehlers in der Firebase Crashlytics Console. Auf dem Tab „Stacktrace“ sehen wir sofort den Worker 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)
  • Das ist der Thread, der von Crashlytics ganz oben angezeigt wird und der den Engpass für unseren Hauptthread darstellt. Crashlytics kennzeichnet ihn mit dem Badge „Root blocking“.

  • Wie in der Kurzinfo des Badges erläutert, ist dieser Arbeitsthread tatsächlich derjenige, der den Thread blockiert, der den ANR-Fehler ausgelöst hat.

  • Übrigens gibt es in Crashlytics auch Smart Badges für andere Verursacher.

  • Wenn ein Thread beispielsweise beim Lesen einer riesigen Datei oder beim Ausführen einer sehr langsamen Datenbankabfrage hängen bleibt und einen ANR auslöst, wird der Thread als „I/O root blocking“ gekennzeichnet.

  • Wenn zwei Threads aufeinander warten, werden sie von Crashlytics als blockiert gekennzeichnet.

  • Sehen wir uns nun die Stapelframes im blockierenden Stammthread an.

  • Hier sehen wir, dass der PaletteExtractor processVideo den FrameDecoder getUncompressedFrame aufruft (blau hervorgehoben). Der FrameDecoder hängt in createBitmap fest und versucht, Speicher für einen einzelnen Frame zuzuweisen.

  • Da wir wissen, dass es nicht auf eine andere Sperre wartet und sich nicht in einem Netzwerkaufruf befindet, ist unser Hauptverdächtiger der Arbeitsspeicher. Das können wir auch gleich bestätigen, indem wir den verfügbaren Arbeitsspeicher des Geräts direkt hier in der Crashlytics-Konsole prüfen.

  • Sehen Sie sich die beiden Stellen an, an denen Crashlytics diese Informationen anzeigt.

  • Unten auf dem Tab „Daten“ für das jeweilige Ereignis, direkt neben dem Tab „Stacktrace“, den wir uns gerade angesehen haben, und auch oben rechts, wo der Durchschnitt für alle Ereignisse angezeigt wird.

  • In beiden Fällen ist kaum etwas vorhanden, nicht einmal 100 KB.

  • Überlegen wir mal.

  • Da der Prozess nicht genügend Arbeitsspeicher hatte, hat die Android Runtime die ganze Zeit damit verbracht, die automatische Speicherbereinigung auszuführen, um genügend Speicherplatz für die nächste Bitmap zu finden, was jedoch nicht gelang.

  • Der Arbeitsthread wird eingefroren und die UI wird für mehr als fünf Sekunden nicht aktualisiert.

  • Hier ist ein kurzer Tipp für Sie.

  • Wenn Sie einen ANR-Fehler in Verbindung mit einem geringen verfügbaren RAM erhalten, ist das ein klares Zeichen dafür, dass der ANR-Fehler entweder auf ein Speicherleck oder ein Bloat-Problem zurückzuführen ist.

  • Jetzt stellt sich die Frage, ob unsere App das Opfer ist oder ob sie auch den Speicher belegt.

  • Sehen wir uns also noch einmal die Problemliste unseres Projekts an und prüfen, ob es noch etwas gibt, das relevant sein könnte.

Ausnahmen aufgrund von Out of Memory untersuchen

  • Polina Govorkova

    Und sieh mal da.

  • Unten sehen wir die Liste der Probleme und direkt über dem ANR-Fehler mit den acht Ereignissen die OutOfMemory-Ausnahme.

  • Wir könnten also doch die Schuldigen sein.

  • Das ist ein wichtiger Hinweis.

  • Denn unter Android bedeutet ein Java-OutOfMemoryError (kurz OOM) nicht nur, dass der Arbeitsspeicher des Smartphones knapp war.

  • Das bedeutet, dass der eigene App-Prozess das Java-Heap-Limit der Android-Laufzeit überschritten hat.

  • Meine Arbeitstheorie ist also, dass ANR- und OOM-Fehler eigentlich zwei Seiten derselben Medaille sind.

  • Je nach Zeitpunkt während eines Testlaufs blieb ChromaClip entweder so lange in der Garbage Collection hängen, dass ein ANR-Fehler ausgelöst wurde, oder der Heap wurde vollständig aufgebraucht, was zu einem OOM-Fehler führte.

  • Sehen wir uns die OutOfMemory-Ausnahme genauer an, um sicherzugehen.

  • Beginnen wir mit dem Stacktrace.

    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)
  • Gib mir einen Moment zum Überlegen...

  • Bitmap.createBitmap, das kommt mir bekannt vor.

  • Haben wir das nicht schon einmal gesehen?

  • Aber sehen Sie sich an, woher sie dieses Mal aufgerufen wird.

  • Beim Öffnen der Aktivität „Einstellungen“ konnten keine 256 Byte zugewiesen werden.

  • Wir tappen wieder in dieselbe Falle.

  • Wenn Ihre App abstürzt, weil sie ein winziges Einstellungssymbol nicht aufblähen kann, ist die Einstellungsaktivität nicht der Übeltäter.

  • Es ist nur der unglückliche Aufrufer, der nach einem Tropfen Speicher gefragt hat, als keiner da war.

  • Um den wahren Schuldigen zu finden, müssen wir also herauszoomen und uns alles ansehen, was sich zum Zeitpunkt des Absturzes im Arbeitsspeicher befand.

  • Wir benötigen also einen Java-Heap-Dump.

  • Wenn Sie wissen, wie Sie einen OOM-Fehler auslösen, können Sie mit dem Speicher-Profiler von Android Studio manuell einen Java-Heap-Dump erstellen.

  • Wenn ein OOM-Fehler jedoch unvorhersehbar auftritt, sei es bei Testläufen oder in der Praxis, funktioniert diese Methode nicht.

Heap-Dumps erfassen und analysieren

  • Polina Govorkova

    Das ist ein weiterer großer Vorteil von Firebase Crashlytics.

  • Zusätzlich zu den Informationen zum Beenden der App ist Crashlytics jetzt in die ProfilingManager API von Android eingebunden.

  • Die ProfilingManager API in Android 17 bietet ein systemgesteuertes Speicherprofiling, und genau das brauchen wir hier.

  • Das bedeutet, dass das Betriebssystem im Hintergrund einen Heap-Snapshot erstellt, sobald in Ihrer App ein OutOfMemory-Fehler auftritt oder die Systemarbeitsspeicherlimits überschritten werden. In Crashlytics ist die ausgelöste Erfassung dann kostenlos, sofern Sie Crashlytics SDK 20.1.0 oder höher verwenden.

  • Anschließend werden die OOM- und Memory Limiter-Abbrüche automatisch im Dashboard gruppiert. Darauf gehen wir später noch genauer ein.

  • Wenn Sie die Erfassung von Heap-Dumps über die Firebase Console oder die Befehlszeile aktivieren, beginnt Crashlytics automatisch mit der Erfassung dieser Heap-Snapshots und de-obfuskiert sie für Sie. Sie erhalten also eine vollständige Übersicht über alles, was sich zum Zeitpunkt des Absturzes im Java-Heap Ihrer App befindet.

  • So sieht der ChromaClip-OOM in der Crashlytics-Konsole aus, nachdem der Heap erfasst wurde.

  • Oben sehen Sie eine Erklärung, warum Java den Heap erschöpft hat und warum das passiert ist.

  • Außerdem werden nützliche Tipps zur Fehlerbehebung und Hinweise auf externe Ressourcen angezeigt.

  • Auf dem Tab „Stacktrace“ direkt über der irreführenden Aktivität „SettingsActivity“ weist Crashlytics darauf hin, dass OOM-Stacktraces nicht deterministisch sind. So müssen Nutzer keine Zeit damit verschwenden, sich den Stacktrace anzusehen, wie wir es gerade getan haben.

  • Anschließend hängt Crashlytics die de-obfuskierte Perfetto-Datei, den Heap-Dump, für die Sitzung an.

  • Von hier aus können wir die Datei herunterladen.

  • Wir können alle Dumps in Google Cloud Storage durchsuchen oder in der Perfetto-Benutzeroberfläche öffnen.

  • Wenn Sie in der Perfetto-UI auf „Analysieren“ klicken, wird der Perfetto-Heap-Dump-Explorer geöffnet.

  • Heben Sie die Hand, wenn Sie das schon gesehen haben.

  • Okay, ein paar Hände sind oben.

  • Diese Ansicht ist für mich sehr einschüchternd, aber in diesem Fall ist das Problem eigentlich ganz einfach.

  • Sehen Sie sich die großen Balken an, die sich fast über den gesamten Java-Heap erstrecken.

  • Es handelt sich also größtenteils um ein Array von Paarobjekten, die jeweils eine Farbe und eine Stringinstanz enthalten.

  • Oh!

  • Ich glaube, ich weiß, was los ist.

  • Unser Code weist jedem einzelnen Pixel in jedem Frame eine Farbe und einige unterstützende Daten zu und speichert Millionen davon in einer Liste.

  • Kein Wunder also, dass die Aktivität „Einstellungen“ diese 256 Byte nicht zuweisen konnte.

  • Na ja, zumindest ein Rätsel ist gelöst.

  • Lass uns das beheben.

  • An dieser Stelle haben wir mehrere Möglichkeiten.

  • Da Perfetto uns bereits auf den PaletteExtractor hingewiesen hat, könnten wir unsere IDE öffnen und die Schleife manuell umschreiben oder den KI-Codierungs-Agent verwenden.

  • Oder wir können die manuelle Heap-Prüfung ganz umgehen und die Android-Befehlszeile verwenden.

    # 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>
  • Wenn Sie die Android-Befehlszeile und Perfetto installieren, können Sie deobfuskierte Crashlytics-Heap-Dumps auflisten und herunterladen, die am häufigsten beibehaltenen Objekte aus diesen Heap-Dumps über die trace_processor abfragen und passende Crashlytics-Problemdaten wie benutzerdefinierte Schlüssel, Logs, Breadcrumbs, zugehörige Roll-outs und mehr abrufen – alles über Ihr Terminal.

  • Noch besser: Sieh dir die Unterseite der Folien an.

  • Führen Sie diese Befehle nicht selbst aus.

  • Bitten Sie Ihren bevorzugten KI-Coding-Agenten noch einmal in natürlicher Sprache, den neuesten OOM-Heap-Dump mit dem Android-Befehl zu prüfen und den Fix zu senden.

    # 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)
  • Und es hat genügend Kontext, um den gesamten Ablauf für Sie zu erledigen.

  • Dazu gehören das Auflisten von OOMs, das Herunterladen des Trace, das Ausführen der Analyse und das Generieren des Code-Patches direkt in Ihrem Projekt.

  • Und sehen Sie sich diesen Patch in der Datei „PaletteExtractor“ an.

    // 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
    }
  • Anstatt Millionen von Objekten zuzuweisen, wird der Frame jetzt einfach herunterskaliert und die Roh-Ganzzahlen werden direkt gezählt.

  • Wenden wir also die Korrektur an, schließen die Probleme, erhöhen die Version und testen weiter.

  • Wenn alles gut geht, sollte nicht nur der Fehler aufgrund von Speichermangel verschwinden, sondern auch der lästige ANR-Fehler, da wir nicht mehr so viel Zeit mit der Speicherbereinigung verbringen.

  • Wir haben also den Patch-Build auf unseren Testgeräten bereitgestellt, einen neuen Clip ausgewählt und mit dem Extrahieren von Paletten begonnen.

  • Und die Benutzeroberfläche friert wieder ein.

  • Noch ein ANR-Fehler.

  • Interessanterweise haben wir bei weiteren Tests festgestellt, dass bei einigen Geräten immer noch ANR-Fehler auftreten, bei anderen Geräten die App jetzt aber einfach zufällig abstürzt, und zwar ziemlich häufig, häufiger als bei den OOM-Fehlern.

  • Aber wir haben den OOM-Fehler behoben. Was ist also los?

Probleme mit nativem Arbeitsspeicher untersuchen

  • Polina Govorkova

    Kehren wir zu Crashlytics zurück.

  • Zuerst die gute Nachricht:

  • Sehen Sie sich unsere Liste der Probleme an.

  • Die Ausnahme aufgrund fehlenden Speichers ist wie erhofft vollständig verschwunden.

  • Die schlechte Nachricht?

  • Da der ChromaClip jetzt etwas länger überlebt, hat er Zeit, herauszufinden, was sich sonst noch darunter verbirgt.

  • Der ANR-Fehler war also immer noch da.

  • Und unten sehen wir zwei native Abstürze, zum Glück mit nur wenigen Ereignissen.

  • Und ganz oben, mit 11 Ereignissen, haben wir den Memory Limiter-Kill, der glücklicherweise bereits mit dem Heap-Dump gekennzeichnet ist.

  • Warte.

  • Was ist ein Memory Limiter-Kill?

  • Wer von Ihnen hat schon davon gehört?

  • Okay.

  • Ein paar seltene Hände.

  • Gemeinsam die Zukunft mitgestalten:

  • Wenn ihr noch nie gesehen habt, dass der Memory Limiter eine App beendet, liegt das daran, dass er erst mit Android 17 eingeführt wurde.

  • Ein Java-OOM wird nur ausgelöst, wenn Sie das Limit für den verwalteten Java-Heap überschreiten. Wenn der Gesamtspeicherbedarf Ihrer App jedoch auch nativen RAM wie Grafikpuffer und C++-Zuweisungen umfasst, gab es bisher keine Obergrenze pro App.

  • Wir hatten also den Low Memory Killer, der global war.

  • Er griff ein, wenn eine App den Arbeitsspeicher des Systems so weit beanspruchte, dass das gesamte Smartphone nicht mehr reagierte.

  • Ab Android 17 begrenzt der Memory Limiter den Speicherbedarf jeder einzelnen App.

  • Wenn der Prozess also eine unverhältnismäßig große Menge an RAM verbraucht, beendet das Betriebssystem diese bestimmte App.

  • Crashlytics erfasst alle diese Details und gruppiert die Abstürze im Dashboard.

  • Wenn Sie in Crashlytics auf „Memory Limiter kill“ klicken, erhalten Sie wie bei OOM eine Erklärung, warum das Betriebssystem eingegriffen hat. Sie müssen sich also nicht merken, was ich gerade gesagt habe.

  • Außerdem erhalten Sie Tipps, wie Sie das Problem beheben können, und Links direkt zur Dokumentation zur Speicherverwaltung unter Android.

  • Wie bei OOM wird auch hier im Stacktrace ein unschuldiger Frame beschuldigt, wenn das Betriebssystem den Stecker zieht, und das Crashlytics-Dashboard hebt ihn hervor.

  • Beendigungen durch den Memory Limiter treten hauptsächlich in nativem Code auf, da in nativen Umgebungen die Ebene fehlt, die für die Speichersicherheit und ‑zuweisung zuständig ist.

  • Andernfalls wäre es ein OOM-Fehler gewesen.

  • Die erste Frage lautet also: Halten unsere Kotlin-Objekte große native Zuordnungen?

  • Oder ist es ein nativer C++-Code, der selbst ein Leck hat?

  • Klicken wir in der Perfetto-UI auf „Analysieren“, um das herauszufinden.

  • Sehen Sie sich den Tab „Übersicht“ im Perfetto-Heap-Dump-Explorer an.

  • Unter „Allgemeine Informationen“ sehen wir, dass der gesamte Prozessarbeitsspeicher bei „Anonyme RSS + Auslagerung“ 4 GB erreicht hat.

  • Okay.

  • Darunter sehen wir jedoch, dass unter „Bytes Retained by Heap“ der gesamte verwaltete Heap, einschließlich des nativen Speichers, der an Kotlin-Objekte wie Bitmaps gebunden ist, nur 42 MB beträgt.

  • Okay.

  • In nur 10 Sekunden haben wir also den gesamten Kotlin- und Java-Code ausgeschlossen.

  • Der Fehler muss also von einer nativen C++-Ebene stammen.

  • Und da ChromaClip eine dünne C++-Ebene für die Frame-Verarbeitung hat, müssen wir nicht lange suchen.

  • Wenn wir unseren Coding-Agenten oder den nativen Profiler von Android Studio auf NativePaletteEngine.cpp processFrame verweisen, wird der Fehler sofort gefunden.

    // 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
    }
  • Unser C++-Code weist also jedem einzelnen Videoframe einen temporären Scratch-Zwischenspeicher zu. Wenn Sie ihn nicht freigeben, ist er nicht so temporär. Was für ein Anfängerfehler!

  • Es ist also an der Zeit, die Punkte zu verbinden.

  • Was ist passiert?

  • Sobald wir das Java-Leck behoben haben, hat das C++-Leck Zeit, sich anzuhäufen und sich auszubreiten.

  • Unter Android 17 wurde die App vom Memory Limiter erkannt und beendet.

  • Auf älteren Geräten, auf denen der Memory Limiter noch nicht verfügbar war, wurde dies jedoch als ANR-Fehler angezeigt.

  • Jetzt ergibt alles Sinn.

  • Fügen wir hier delete[] scratch hinzu, erhöhen die Version und veröffentlichen das Update.

  • Da die Probleme mit dem Beenden von Prozessen durch den Memory Limiter und ANRs endlich behoben sind, können wir Version 1.0 veröffentlichen.

  • Aufregend!

  • Und weil ChromaClip jetzt so schnell und stabil ist, geht unsere App durch die Decke.

  • Tausende Nutzer laden täglich Videos hoch und erstellen Remixe.

  • Aber erinnern Sie sich bei all dem Traffic an die mysteriösen nativen Abstürze, die wir vorhin unten im Dashboard gesehen haben?

  • Die spielen auch verrückt.

  • Plötzlich gibt es viel mehr davon.

  • Und unsere Arbeit ist noch nicht getan.

Abstürze aufgrund von Beschädigungen des nativen Arbeitsspeichers untersuchen

  • Polina Govorkova

    Wenn Sie diesen nativen Absturz in Crashlytics öffnen, sehen Sie sich den Stacktrace an.

  • Es gibt keine einzige Zeile Code aus unserer eigenen App.

  • RenderThread stürzt tief in libc.so ab, während der Speicher freigegeben wird.

  • Noch ein Tipp: Wenn ein Systemthread im Arbeitsspeicherzuweiser abstürzt und kein App-Code enthalten ist, ist das ein typisches Zeichen für eine Beschädigung des nativen Arbeitsspeichers.

  • Der C++-Code hat den Puffer also früher beschädigt oder freigegeben und der Rendering-Thread ist später darüber gestolpert.

  • Wie können wir also den wahren Schuldigen genau in dem Moment erwischen, in dem die Beschädigung passiert ist?

  • Android bietet zwei leistungsstarke Tools, die beide von Crashlytics unterstützt werden.

  • Erstens: GWP-ASan, ein leichtgewichtiges Sicherheitsnetz, das speziell für die Produktion entwickelt wurde.

  • Es werden nur sehr wenige native Heap-Zuweisungen stichprobenartig erfasst und mit Guard-Pages umgeben, sodass der CPU- und Arbeitsspeicheraufwand nahezu null ist.

  • Wenn ein Fehler in einer der Tausenden von Nutzersitzungen auf freigegebenen oder außerhalb des zulässigen Bereichs liegenden Speicher in einer Stichprobenzuweisung zugreift, wird er von GWP-ASan sofort erkannt.

  • Da GWP-ASan jedoch von Natur aus probabilistisch ist, möchten Sie beim lokalen Testen Ihr Test-Smartphone nicht 16 Stunden oder länger laufen lassen, um die Zufallsstichprobe zu erhalten.

  • Was erhalten Sie also?

  • Für deterministisches lokales Debugging gibt es die ARM MTE (Memory Tagging Extension).

  • MTE verwendet einen Hardware-Pointer und Speicher-Tags, um jede Zuweisung zu prüfen. Es ist auf ARM v9-Geräten wie dem Pixel 8 und neueren Modellen verfügbar.

  • Wenn Sie es also in der Produktion mit GWP-ASan kombinieren, erhalten Sie das Beste aus beiden Welten.

  • Wenn eines der beiden Tools den Fehler findet, wird das Problem in Crashlytics automatisch gekennzeichnet. Sie können dann nach GWP-ASan oder MTE filtern und eine Infokarte mit der genauen Ursache des Fehlers im Dashboard anpinnen.

  • Sehen wir uns an, was in der Crashlytics-Konsole passiert, wenn GWP-ASan den Absturz erkennt.

  • Neben dem regulären Stacktrace-Tab gibt es den Stacktrace für den Arbeitsspeicher.

  • Oben sehen wir die genaue Ursache der Beschädigung: „Use After Free“.

  • Anstatt den Absturz des Render-Threads in libc.so anzuzeigen, liefert GWP-ASan jetzt die vollständige Biografie dieses Puffers in unserem eigenen effect_pipeline.cc.

  • Unter „Allocated By“ sehen wir, wo der Puffer erstellt wurde.

  • Und direkt unter „Deallocated By“ sehen wir den genauen Verursacher, der die Freigabe im Render-Thread ausgelöst hat.

  • Und das ist in Zeile 208 unserer Effekt-Pipeline versteckt.

  • Wir öffnen also unseren nativen Code in der EffectPipeline und der Fehler grinst uns an.

    // 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
    }
  • Wenn ein Nutzer den Farbfilter im Hintergrundthread wechselt, wird der alte Puffer freigegeben, während der RenderThread ihn noch liest.

  • Wir fügen also die Mutex-Sperre um den Pufferwechsel hinzu, veröffentlichen das Update und sehen schließlich, wie sowohl die GWP-ASan-Berichte als auch all diese mysteriösen Abstürze fast über Nacht verschwinden.

  • Und der Messwert für Nutzer ohne Abstürze ist auf 100 % gestiegen.

  • Sehr gut!

  • Erinnern Sie sich noch an die Frage, die wir uns zu Beginn des Vortrags gestellt haben: „Bin ich fertig?“

  • Kurz bevor sie von so ziemlich jedem Boss für Reaktionsfähigkeit und Speicher getroffen wird, den es gibt.

  • Die ANRs, die Out of Memory-Fehler, die Beendigungen durch den Arbeitsspeicherbegrenzer und die Abstürze aufgrund von nativer Speicherbeschädigung sind jetzt alle behoben.

  • Und selbst wenn der Stacktrace lügt und sich der Schuldige irgendwo in den Threads in C++ versteckt, bieten Android und Firebase Crashlytics die Tools, um ihn aufzuspüren und die App mit gutem Gewissen zu veröffentlichen.

Zusammenfassung und Anmeldung für den Vorabzugriff

  • Polina Govorkova

    Fassen wir also zusammen, was Sie sofort nutzen können, wenn Sie wieder an Ihrem Laptop sind: ANR-Thread-Statistiken und andere relevante Metadaten über ApplicationExitInfo, automatische Erfassung von OOM- und Speicherbegrenzer-Kill-Triggern über ProfilingManager sowie vollständige GWP-ASan- und Arm-MTE-Unterstützung für native Speicherbeschädigung.

  • Außerdem führen wir in der privaten Vorschau Heap-Dumps im Feld unter Android 17 oder höher ein.

  • Wenn Sie zu den Ersten gehören möchten, die die automatische Heap-Dump-Erfassung in Firebase Crashlytics ausprobieren und mit Perfetto und der Android-Befehlszeile analysieren, machen Sie ein Foto der Folie und melden Sie sich über den Link an.

  • Wir sind nun am Ende des Vortrags angelangt. Vielen Dank, dass Sie heute dabei waren und sich die Zeit genommen haben.

  • Möge Ihr Hauptthread unblockiert, Ihr Heap sauber und Ihr nächster Release der stabilste sein.

  • Viel Spaß beim Debuggen!

  • Nochmals vielen Dank.

Code

Code-Snippets

Text-Snippet bei 01:45 in „Einführung“
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
Textausschnitt bei 01:58 in „Einführung“
"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.main
Textausschnitt bei 04:45 in „ANRs debuggen und analysieren“
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)
Textausschnitt bei 08:08 in „ANRs debuggen und analysieren“
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)
Text-Snippet bei 11:54 in „Investigating out of memory exceptions“
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)
Shell-Snippet bei 15:54 in „Heap-Dumps erfassen und analysieren“
# 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>
Text-Snippet bei 16:27 in „Heap-Dumps erfassen und analysieren“
# 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)
Kotlin-Snippet bei 16:47 in „Capturing and analyzing heap dumps“
// 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
}
C++-Snippet bei 21:06 in „Investigating native memory issues“
// 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
}
C++-Snippet bei 25:14 in „Investigating native memory corruption crashes“
// 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
}