Narzędzia i skuteczność

Rozwiązywanie problemów z pamięcią za pomocą Firebase Crashlytics

Głośniki

  • Polina Govorkova

    Starszy programista

Informacje

Informacje o wystąpieniu

Dzięki Firebase Crashlytics możesz dbać o to, aby aplikacja nie wykorzystywała zbyt dużo pamięci i nie powodowała błędów naruszenia ochrony pamięci. Podczas tej sesji omówimy nasz kompleksowy zestaw narzędzi do rozwiązywania problemów z pamięcią, które są dostępne w konsoli Firebase i w Android Studio. Zaczniemy od omówienia, jak wykorzystać statystyki błędów ANR i raporty GWP-ASan do diagnozowania trudnych do wykrycia błędów pamięci. Następnie przejdziemy do naszych najnowszych narzędzi do obsługi zdarzeń związanych z brakiem pamięci (OOM) i ograniczeniem pamięci (MLK), które zostały zaprojektowane specjalnie po to, aby pomóc Ci w poruszaniu się po nowych limitach pamięci wprowadzonych w Androidzie 17 i dostosowywaniu się do nich.

Podsumowanie

  • Błędy „Aplikacja nie odpowiada” często wynikają z wąskich gardeł w tle lub wyczerpania pamięci, a nie z aktywnego obciążenia procesora. Firebase Crashlytics wysyła zapytania do ApplicationExitInfo, aby wyświetlać ślady wielowątkowe i identyfikować główne wątki blokujące.
  • Awarie z powodu braku pamięci występują, gdy aplikacje przekraczają limity sterty Javy w czasie działania. Crashlytics integruje się z ProfilingManager w Androidzie 17, aby zbierać zrzuty sterty po deobfuskacji, które można sprawdzać w Perfetto lub za pomocą interfejsu wiersza poleceń Androida.
  • Ogranicznik pamięci w Androidzie 17 kończy poszczególne procesy, które zużywają nadmierną ilość pamięci RAM, w tym alokacje natywne. Crashlytics grupuje te zakończenia w panelu razem ze zrzutami sterty, aby ułatwić oddzielenie wycieków pamięci C++ od zarządzanych alokacji sterty.
  • Uszkodzenie pamięci natywnej często występuje w wątkach systemowych, a nie w kodzie aplikacji. Crashlytics obsługuje GWP-ASan i Arm MTE, a raporty GWP-ASan wskazują dokładne miejsca alokacji i dezalokacji w przypadku błędów takich jak use-after-free.

Zasoby

Gemini Notebook

Tematy

  • firebase-crashlytics
  • android-memory
  • anr
  • brak pamięci
  • profiling-manager
  • perfetto
  • gwp-asan
  • arm-mte

Transkrypcja

Pobierz transkrypcję

Wprowadzenie

  • Polina Govorkova

    Cześć!

  • Witamy w module dotyczącym rozwiązywania problemów z pamięcią za pomocą Firebase Crashlytics.

  • Czekaj, co to za nazwa stanowiska?

  • Zróbmy coś innego.

  • Utrzymywanie aplikacji w dobrej kondycji dzięki Firebase Crashlytics.

  • To brzmi lepiej.

  • Nazywam się Polina i jestem starszą programistką w zespole Firebase Crashlytics. Dzisiaj poprowadzę tę prezentację.

  • Na pewnym etapie cyklu programowania może Ci się wydawać, że wszystkie błędy zostały usunięte.

  • Błędy w analizie JSON, zapomniane sprawdzanie wskaźników null – to już przeszłość.

  • Możesz się zastanawiać, czy to już koniec.

  • Mam dla Ciebie złą wiadomość – nie jesteś.

  • Zostaną Ci tylko trudne problemy.

  • Twierdzę, że 2 najtrudniejsze kategorie problemów, z którymi się zetkniecie, to czas reakcji i pamięć.

  • Dobra wiadomość jest taka, że możesz liczyć na naszą pomoc.

  • Dzisiaj omówimy istniejące i zupełnie nowe narzędzia na Androidzie i w Crashlytics, które pomogą Ci znaleźć główną przyczynę problemów z czasem odpowiedzi, które objawiają się błędami „Aplikacja nie odpowiada”, a także wyjątkami związanymi z brakiem pamięci, wyciekami pamięci i uszkodzeniem pamięci natywnej.

  • Zaczynajmy.

  • Dlaczego problemy z czasem reakcji i pamięcią są tak powszechne?

  • Weźmy klasyczny błąd: deklarujesz ciąg znaków, ustawiasz go na null i w pewnym momencie próbujesz odczytać jego długość.

  • Co zyskujesz?

  • Stary, dobry NullPointerException.

  • Ale w tym przypadku jest to proste, ponieważ zrzut stosu wyjątku wskazuje dokładny wiersz kodu, który spowodował błąd aplikacji.

  • Spójrz na zrzut stosu po lewej stronie.

    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
  • Od razu widać, że problem występuje w wierszu 42 w pliku MainActivity.kt.

  • Takie podejście sprawdza się w przypadku większości błędów.

  • Teraz, gdy pojawia się problem z czasem reakcji lub pamięcią, np. błąd „Aplikacja nie odpowiada” po prawej stronie, zrzut stosu prawie nigdy nie wskazuje prawdziwego winowajcy.

    "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
  • W najlepszym przypadku pokazuje tylko efekt, czyli niewinny wątek lub alokację, które akurat były uruchomione, gdy aplikacja się zawiesiła lub zabrakło jej pamięci (OOM).

  • Tymczasem główna przyczyna wystąpiła znacznie wcześniej lub w zupełnie innym miejscu.

  • Na szczęście w przypadku tego problemu nie musisz zgadywać, bo Firebase Crashlytics ma dla nas odpowiedź.

  • Zanim jednak przejdziemy do rozwiązywania problemów, podnieście rękę, jeśli znacie Crashlytics.

  • O, świetnie.

  • Cieszę się, że widzę tyle rąk.

  • Dla tych, którzy są tu po raz pierwszy: Firebase Crashlytics to rozwiązanie do raportowania błędów w czasie rzeczywistym, które dostarcza dane z pola, a także statystyki kontekstowe i oparte na AI, wskazówki dotyczące rozwiązywania problemów i wyraźne sygnały dotyczące nowych wersji, aby pomóc Ci szybciej rozwiązywać problemy.

  • Crashlytics cieszy się dużą popularnością wśród naszych deweloperów i jest najczęściej stosowanym rozwiązaniem do zgłaszania awarii na urządzeniach mobilnych. Obsługuje ponad 370 tys. aplikacji natywnych, w tym 57% najpopularniejszych aplikacji na Androida.

  • Aby zobaczyć, jak Crashlytics pomaga nam śledzić błędy w praktyce, przeanalizujmy konkretny przykład.

  • Dziś chcę opowiedzieć historię ChromaClip.

  • Jest to narzędzie do wyodrębniania palety kolorów z filmów, które tworzę. Chcę opowiedzieć, jak udało mi się rozwiązać problemy z responsywnością i pamięcią podczas jego tworzenia.

  • Pomysł na ChromaClip jest prosty: użytkownicy wybierają film, na przykład piękny zachód słońca lub scenę z świetną gradacją kolorów, a aplikacja analizuje klatki, aby wygenerować niestandardową paletę kolorów, którą użytkownicy mogą wykorzystać w swoich projektach, sztuce cyfrowej, tablicach nastrojów i nie tylko.

  • Nasza historia zaczyna się tuż przed wydaniem wersji 1.0.

  • Chciałem mieć pewność, że wszystko będzie działać bez zarzutu, więc przeprowadziłem ostatnie testy lokalnie na kilku urządzeniach. Wybrałem film, kliknąłem przycisk, aby wyodrębnić paletę kolorów, i pojawił się ulubiony komunikat każdego programisty aplikacji na Androida.

  • ChromaClip nie odpowiada.

  • Jest to błąd typu „Aplikacja nie odpowiada”, znany również jako ANR.

Debugowanie i analizowanie błędów ANR

  • Polina Govorkova

    Co więc możemy zrobić w takiej sytuacji, oprócz panikowania?

  • Ponieważ telefon testowy jest podłączony bezpośrednio do laptopa, najpierw otwieramy Android Studio i sprawdzamy Logcat.

  • Widzimy zrzut stosu dla wątku głównego.

    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)
  • Co nam to mówi?

  • Na samej górze widzimy potwierdzenie, że jest to błąd ANR, który występuje w MainActivity.

  • Co jeszcze?

  • Widzimy przyczynę, czyli przekroczenie limitu czasu wysyłania danych wejściowych.

  • Oznacza to, że użytkownik kliknął ekran.

  • Tak, udało się.

  • Wątek główny był jednak tak zajęty, że nie mógł przetworzyć tego zdarzenia w ciągu 5 sekund, czyli przed upływem limitu czasu Androida.

  • Widzimy też, że system zrzucił plik do traces.txt, a tuż pod nim logcat wyświetla zrzut stosu dla naszego głównego wątku.

  • Zwróć uwagę, że stan jest równy S.

  • Nasz wątek główny nie obciąża nawet aktywnie procesora.

  • S oznacza spanie.

  • Dlaczego wątek główny jest uśpiony, gdy czekamy?

  • Przyjrzyjmy się bliżej ramkom stosu.

  • W tym miejscu, na środku śladu, widzimy, że nasz wątek główny jest zaparkowany w Unsafe.park i czeka na nieznany obiekt.

  • Dlaczego nie wiemy, na co czeka?

  • W przypadku niektórych błędów ANR, np. gdy wątek jest blokowany przez klasyczną blokadę synchronizacji Javy, zrzut stosu wątku głównego podaje, na co czeka i co utrzymuje blokadę.

  • Jeśli jednak używasz bardziej zaawansowanych elementów podstawowych współbieżności, logcat informuje nas tylko, że czekamy na nieznany obiekt.

  • Nie wiemy więc, na jaki obiekt czekamy, na który wątek instancji roboczej w tle czekamy ani dlaczego trwa to ponad 5 sekund.

  • Ponieważ testujemy lokalnie, możemy pobrać traces.txt z urządzenia i ręcznie przewijać dziesiątki uśpionych wątków, aby znaleźć winowajcę, lub wykorzystać tokeny, pytając AI.

  • W niektórych przypadkach to by zadziałało, i tak byłoby w tym przypadku.

  • Nie tylko nie chcę ręcznie odczytywać nieprzetworzonych zrzutów wątków – szczerze mówiąc, to bardzo przestarzałe podejście – ale wiem też, że takie podejście nie jest w ogóle możliwe, gdy jesteśmy w środowisku produkcyjnym.

  • Nie mamy bezpośredniego dostępu do pliku traces.txt na telefonach użytkowników, prawda?

  • Dobrze, że zintegrowaliśmy się z Firebase Crashlytics.

  • Ponieważ eliminuje potrzebę wyszukiwania i pozwala dotrzeć do sedna błędu ANR nawet w środowisku produkcyjnym.

  • Jak to zrobić?

  • W tym celu używa się interfejsu ApplicationExitInfo API na Androidzie.

  • Począwszy od Androida 11, gdy system wykryje błąd ANR lub zakończy proces aplikacji, system operacyjny rejestruje szczegółowe metadane wyjścia, w tym dokładną przyczynę awarii, stan pamięci i pełne śledzenie systemu dla każdego wątku działającego w danym momencie w procesie.

  • A co najlepsze, nie musisz instalować żadnych dodatkowych wtyczek ani pisać ani jednej linijki kodu.

  • Jeśli masz pakiet Crashlytics SDK w wersji 18.3.5 lub nowszej, funkcja ta działa od razu.

  • Dlatego gdy tylko aplikacja zostanie ponownie uruchomiona, pakiet Crashlytics SDK automatycznie wyśle zapytanie do ApplicationExitInfo, przechwyci pełny zrzut wielowątkowy wraz z telemetrią urządzenia i wyśle go do backendu Crashlytics.

  • Otwórzmy konsolę Crashlytics i zobaczmy, co nam powie.

  • To jest widok szczegółów problemu w konsoli Firebase Crashlytics dotyczący błędu ANR. Na karcie zrzutu stosu od razu widzimy wątek roboczy 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)
  • To jest wątek, który Crashlytics wyświetla na samej górze i który jest wąskim gardłem dla naszego głównego wątku. Crashlytics oznacza go plakietką „Blokowanie głównego wątku”.

  • Jak wyjaśnia etykietka na plakietce, ten wątek instancji roboczej jest rzeczywiście tym, który blokuje wątek, który wywołał błąd ANR.

  • Crashlytics ma też inteligentne plakietki dla innych przyczyn.

  • Jeśli na przykład wątek utknął podczas odczytywania ogromnego pliku lub wykonywania bardzo wolnego zapytania do bazy danych, a następnie wywołał błąd ANR, zostanie on oznaczony jako blokujący operacje wejścia/wyjścia.

  • Jeśli 2 wątki utknęły, czekając na siebie, Crashlytics oznaczy je jako zakleszczone.

  • Przyjrzyjmy się teraz ramkom stosu w głównym wątku blokującym.

  • Mamy więc PaletteExtractor processVideo wywołujący FrameDecoder getUncompressedFrame, który jest zaznaczony na niebiesko. Jest on całkowicie zablokowany w createBitmap, próbując przydzielić pamięć dla pojedynczej klatki.

  • Wiemy, że nie czeka na inną blokadę i nie jest w trakcie wywołania sieciowego, więc głównym podejrzanym musi być pamięć. Możemy to od razu potwierdzić, sprawdzając wolną pamięć urządzenia w konsoli Crashlytics.

  • Spójrz na 2 miejsca, w których Crashlytics wyświetla te informacje.

  • Na karcie danych konkretnego zdarzenia, tuż obok karty zrzutu stosu, którą właśnie analizowaliśmy, a także w prawym górnym rogu, gdzie wyświetlana jest średnia dla wszystkich zdarzeń.

  • W obu przypadkach wygląda na to, że prawie nic nie ma, nawet 100 KB.

  • Zastanówmy się.

  • W tym przypadku procesowi brakowało pamięci, więc środowisko wykonawcze Androida poświęciło cały czas na gorączkowe uruchamianie funkcji czyszczenia pamięci, próbując znaleźć wystarczająco dużo miejsca na kolejną bitmapę, ale nigdy go nie znalazło.

  • Zablokowanie wątku instancji roboczej i brak zasobów dla interfejsu przez ponad 5 sekund.

  • Oto krótka wskazówka.

  • Zanim przejdziemy dalej, pamiętaj, że jeśli błąd ANR występuje przy małej ilości wolnej pamięci RAM, jest to wyraźny sygnał, że błąd ANR jest spowodowany wyciekiem pamięci lub przepełnieniem.

  • Pytanie brzmi: czy nasza aplikacja jest ofiarą, czy też sama zajmuje pamięć?

  • Wróćmy do listy problemów w projekcie i sprawdźmy, czy jest tam coś jeszcze, co może być istotne.

Analizowanie wyjątków związanych z brakiem pamięci

  • Polina Govorkova

    I spójrz tutaj.

  • Na dole widzimy listę problemów, a tuż nad błędem ANR z 8 zdarzeniami znajduje się wyjątek OutOfMemory.

  • Może więc to my jesteśmy winni.

  • To bardzo ważna wskazówka.

  • W systemie Android błąd OutOfMemoryError w języku Java, czyli w skrócie OOM, nie oznacza tylko, że w telefonie było mało pamięci RAM.

  • Oznacza to, że proces naszej aplikacji przekroczył limit sterty Javy w środowisku wykonawczym Androida.

  • Na tym etapie moja teoria robocza jest taka, że błędy ANR i OOM to 2 strony tej samej monety.

  • W zależności od czasu trwania testu ChromaClip albo zacinał się w procesie czyszczenia pamięci na tyle długo, że powodował błąd ANR, albo całkowicie wyczerpywał pamięć sterty, co prowadziło do błędu OOM.

  • Zbadajmy wyjątek OutOfMemory, aby mieć pewność.

  • Zacznij od zrzutu stosu.

    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)
  • Zaczekaj chwilę.

  • Bitmap.createBitmap, to brzmi znajomo.

  • Czy już tego nie widzieliśmy?

  • Tylko że tym razem jest wywoływana z innego miejsca.

  • Nie udało się przydzielić 256 bajtów podczas otwierania aktywności ustawień.

  • Znowu wpadamy w tę samą pułapkę.

  • Jeśli aplikacja ulega awarii, ponieważ nie może rozwinąć małej ikony ustawień, to nie aktywność ustawień jest winna.

  • To po prostu pechowy element wywołujący, który poprosił o odrobinę pamięci, gdy nie było już żadnej.

  • Aby znaleźć prawdziwego winowajcę, musimy spojrzeć na problem z szerszej perspektywy i sprawdzić wszystko, co znajdowało się w pamięci w momencie awarii.

  • Innymi słowy, potrzebujemy zrzutu stosu Java.

  • Jeśli wiesz, jak wywołać błąd OOM, możesz ręcznie pobrać zrzut sterty Javy za pomocą Memory Profiler w Android Studio.

  • Jeśli jednak błąd OOM występuje nieprzewidywalnie podczas testów lub w środowisku produkcyjnym, ta metoda nie zadziała.

Przechwytywanie i analizowanie zrzutów sterty

  • Polina Govorkova

    To kolejny przykład, jak Firebase Crashlytics może pomóc.

  • Oprócz informacji o wyjściu z aplikacji Crashlytics integruje się teraz z interfejsem ProfilingManager API Androida.

  • Interfejs ProfilingManager API w Androidzie 17 został wzbogacony o profilowanie pamięci wywoływane przez system i właśnie tego potrzebujemy.

  • Oznacza to, że gdy aplikacja napotka błąd OutOfMemory lub przekroczy limity pamięci systemowej, system operacyjny przechwyci zrzut sterty w tle, a w Crashlytics wywołana kolekcja będzie dostępna bezpłatnie, o ile używasz pakietu SDK Crashlytics w wersji 20.1.0 lub nowszej.

  • Następnie Crashlytics automatycznie grupuje w panelu błędy OOM i błędy związane z ograniczeniem pamięci, o których opowiemy później.

  • Gdy wyrazisz zgodę na zbieranie zrzutów sterty w konsoli Firebase lub interfejsie wiersza poleceń, Crashlytics zacznie automatycznie zbierać te zrzuty i je deobfuskować. W zasadzie otrzymasz pełny obraz wszystkiego, co znajduje się w stercie Javy aplikacji w momencie awarii.

  • Tak wygląda błąd OOM ChromaClip w konsoli Crashlytics po przechwyceniu sterty.

  • U góry zobaczysz wyjaśnienie, dlaczego Java wyczerpała pamięć sterty i dlaczego tak się stało.

  • Zobaczysz też przydatne wskazówki dotyczące rozwiązywania problemów i linki do zasobów zewnętrznych.

  • Na karcie zrzutu stosu, tuż nad tą mylącą aktywnością ustawień, Crashlytics oznacza, że zrzuty stosu OOM nie są deterministyczne, dzięki czemu użytkownicy nie muszą marnować czasu na analizowanie zrzutu stosu, tak jak my przed chwilą.

  • Następnie Crashlytics dołącza do sesji odczytelny plik Perfetto, czyli zrzut stosu.

  • Z tego miejsca możemy pobrać plik.

  • Możemy przeglądać wszystkie zrzuty w Google Cloud Storage lub otworzyć je w interfejsie Perfetto.

  • Kliknięcie Analizuj w interfejsie Perfetto otwiera eksplorator zrzutów stosu Perfetto.

  • Podnieście rękę, jeśli już to widzieliście.

  • OK, widzę kilka rąk.

  • Dla mnie ten widok jest bardzo onieśmielający, ale w tym przypadku problem jest dość prosty.

  • Spójrz na zestaw dużych słupków, które rozciągają się niemal przez całą stertę Javy.

  • W większości jest to tablica obiektów par, z których każdy zawiera kolor i ciąg znaków.

  • O!

  • Chyba wiem, co się dzieje.

  • Nasz kod przydziela kolor i niektóre dane pomocnicze dla każdego piksela w każdej klatce i przechowuje miliony z nich na liście.

  • Nic dziwnego, że aktywność Ustawienia nie mogła przydzielić tych 256 bajtów.

  • Cóż, przynajmniej jedna zagadka została rozwiązana.

  • Naprawmy to.

  • W tym momencie mamy kilka opcji.

  • Perfetto wskazał już PaletteExtractor, więc możemy otworzyć IDE i ręcznie przepisać pętlę lub użyć agenta AI do kodowania.

  • Możemy też całkowicie pominąć ręczną kontrolę sterty i skorzystać z interfejsu wiersza poleceń Androida.

    # 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>
  • Zainstalowanie interfejsu wiersza poleceń Androida i narzędzia Perfetto umożliwi Ci wyświetlanie i pobieranie zrzutów stosu Crashlytics z usuniętym zaciemnieniem kodu, wysyłanie zapytań o najczęściej zachowywane obiekty z tych zrzutów stosu za pomocą trace_processor oraz pobieranie pasujących danych o problemach Crashlytics, takich jak klucze niestandardowe, logi, menu nawigacyjne, powiązane wdrożenia i inne, a wszystko to z poziomu terminala.

  • A jeszcze lepiej – spójrz na spód slajdów.

  • Nie uruchamiaj tych poleceń samodzielnie.

  • Ponownie poproś ulubionego agenta AI do kodowania w języku naturalnym o sprawdzenie najnowszego zrzutu sterty OOM za pomocą polecenia Androida i wprowadzenie poprawki.

    # 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)
  • Będzie też mieć wystarczający kontekst, aby wykonać cały proces za Ciebie.

  • Możesz wyświetlić listę błędów OOM, pobrać logi, przeprowadzić analizę i wygenerować poprawkę kodu bezpośrednio w projekcie.

  • Spójrz na tę poprawkę w pliku 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
    }
  • Zamiast milionów alokacji obiektów teraz po prostu zmniejszamy próbkowanie klatki i liczymy bezpośrednio liczby całkowite.

  • Zastosujmy więc poprawkę, zamknijmy problemy, podnieśmy wersję i kontynuujmy testowanie.

  • Trzymamy kciuki, żeby nie tylko zniknął błąd braku pamięci, ale też żeby nie było już problemów z odśmiecaniem pamięci, co powinno wyeliminować ten uciążliwy błąd ANR.

  • Wdrożyliśmy więc poprawkę na naszych urządzeniach testowych, wybraliśmy nowy klip i zaczęliśmy wyodrębniać palety.

  • Interfejs znów się zawiesza.

  • Kolejny błąd ANR.

  • Co ciekawe, podczas dalszych testów odkryliśmy, że na niektórych urządzeniach nadal występuje błąd ANR, ale na innych aplikacja po prostu losowo się zawiesza, i to dość często, częściej niż w przypadku błędów OOM.

  • Ale naprawiliśmy błąd OOM, więc o co chodzi?

Analizowanie problemów z pamięcią natywną

  • Polina Govorkova

    Wróćmy do Crashlytics.

  • Zacznijmy od dobrych wiadomości.

  • Sprawdź listę problemów.

  • Wyjątek związany z brakiem pamięci całkowicie zniknął, zgodnie z naszymi oczekiwaniami.

  • Zła wiadomość?

  • Teraz, gdy ChromaClip przetrwa nieco dłużej, ma czas, aby odkryć, co jeszcze kryło się pod spodem.

  • W rezultacie błędy ANR nie zniknęły.

  • Na dole mamy 2 awarie systemowe, na szczęście tylko z kilkoma zdarzeniami.

  • Na samej górze, z 11 zdarzeniami, mamy zakończenie działania przez ogranicznik pamięci, które na szczęście jest już oznaczone jako zawierające zrzut sterty.

  • Zaczekaj.

  • Co to jest zakończenie działania przez ogranicznik pamięci?

  • Kto z Was o tym słyszał? Podnieście ręce.

  • OK.

  • Kilka rzadkich układów.

  • Uczmy się razem.

  • Jeśli nie znasz jeszcze funkcji Memory Limiter, to dlatego, że jest ona zupełnie nowa w Androidzie 17.

  • Błąd braku pamięci w Javie pojawia się tylko wtedy, gdy przekroczysz limit zarządzanej sterty Javy. Jednak gdy całkowite wykorzystanie pamięci przez aplikację obejmuje też natywną pamięć RAM, np. bufory graficzne i alokacje C++, wcześniej nie było limitu na aplikację.

  • Mieliśmy więc globalny mechanizm zabijania procesów przy małej ilości pamięci.

  • Wkraczał, gdy jakaś aplikacja zużywała całą pamięć RAM systemu, aż do momentu, gdy telefon się zawieszał.

  • Od Androida 17 funkcja Memory Limiter ogranicza wykorzystanie pamięci przez poszczególne aplikacje.

  • Gdy proces zużywa nieproporcjonalną ilość pamięci RAM, system operacyjny zamyka tę aplikację.

  • Crashlytics rejestruje wszystkie te szczegóły i grupuje zamknięcia w panelu.

  • Klikając opcję „Zabicie przez ogranicznik pamięci” w Crashlytics, podobnie jak w przypadku braku pamięci, otrzymasz wyjaśnienie, dlaczego system operacyjny interweniował, więc nie musisz zapamiętywać tego, co właśnie powiedziałem.

  • Otrzymasz też wskazówki, jak rozwiązać ten problem, oraz linki do dokumentacji dotyczącej egzekwowania zasad dotyczących pamięci na Androidzie.

  • Podobnie jak w przypadku braku pamięci, zrzut stosu obwinia niewinną klatkę, gdy system operacyjny odciął zasilanie, a panel Crashlytics to podkreśla.

  • Obecnie zatrzymania przez ogranicznik pamięci występują głównie w kodzie natywnym, ponieważ w środowiskach natywnych brakuje warstwy, która obsługuje bezpieczeństwo i alokację pamięci.

  • W przeciwnym razie wystąpiłby błąd braku pamięci.

  • Pierwsze pytanie brzmi: czy nasze obiekty Kotlin przechowują ogromne alokacje oparte na kodzie natywnym?

  • A może to nieprzetworzony kod C++, który sam wycieka?

  • Aby się tego dowiedzieć, kliknij Analyze (Analizuj) w interfejsie Perfetto.

  • Spójrz na kartę Przegląd w eksploratorze zrzutów stosu Perfetto.

  • W sekcji informacji ogólnych widzimy, że anonimowy RSS i przestrzeń wymiany wskazują, że łączna pamięć procesu osiągnęła 4 GB.

  • OK.

  • Jednak poniżej widzimy, że w sekcji Bajty zachowane przez stertę cała zarządzana sterta, w tym pamięć natywna powiązana z obiektami Kotlin, takimi jak mapy bitowe, ma tylko 42 MB.

  • OK.

  • W ciągu 10 sekund wykluczyliśmy cały kod Kotlin i Java.

  • Dlatego wyciek musi pochodzić z natywnej warstwy C++.

  • ChromaClip ma cienką warstwę C++ do przetwarzania klatek, więc nie musimy szukać daleko.

  • Jeśli skierujemy naszego agenta kodowania lub natywny profiler Android Studio na NativePaletteEngine.cpp processFrame, błąd będzie widoczny od razu.

    // 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
    }
  • W każdej klatce filmu nasz kod C++ przydziela tymczasowy bufor roboczy, który nie jest taki tymczasowy, jeśli go nie zwolnisz. To błąd początkującego.

  • Czas połączyć kropki.

  • Co się stało?

  • Po naprawieniu wycieku w Javie wyciek w C++ będzie miał czas na nagromadzenie się i niekontrolowany wzrost.

  • Na Androidzie 17 ogranicznik pamięci wykrył problem i zamknął aplikację.

  • Jednak na starszych urządzeniach, na których nie było jeszcze ogranicznika pamięci, błąd ten objawiał się jako błąd ANR.

  • Teraz wszystko nabiera sensu.

  • Dodajmy tutaj delete[] scratch, podnieśmy wersję i wyślijmy aktualizację.

  • Po wyeliminowaniu błędów ANR i problemów z zamykaniem aplikacji przez ogranicznik pamięci możemy już opublikować wersję 1.0.

  • Ekscytujące.

  • Dzięki temu, że ChromaClip działa teraz szybko i stabilnie, nasza aplikacja zyskuje popularność.

  • Tysiące osób codziennie przesyłają i remiksują filmy.

  • Ale przy całym tym ruchu pamiętasz te tajemnicze awarie systemowe, które widzieliśmy wcześniej na dole panelu?

  • One też szaleją.

  • Nagle jest ich znacznie więcej.

  • Nasza praca jeszcze się nie skończyła.

Analizowanie awarii spowodowanych uszkodzeniem pamięci natywnej

  • Polina Govorkova

    Otwórz tę awarię systemową w Crashlytics i spójrz na zrzut stosu.

  • Nie ma tam ani jednej linijki kodu z naszej aplikacji.

  • Aplikacja RenderThread ulega awarii w głębi libc.so podczas zwalniania pamięci.

  • I jeszcze jedna wskazówka: gdy wątek systemowy ulega awarii w alokatorze pamięci, a nie ma w nim kodu aplikacji, jest to typowy objaw uszkodzenia pamięci natywnej.

  • Oznacza to, że nasz kod C++ uszkodził lub zwolnił bufor wcześniej, a wątek renderowania natknął się na niego później.

  • Jak więc wykryć prawdziwego winowajcę w momencie, gdy dochodzi do uszkodzenia?

  • Android udostępnia 2 potężne narzędzia, a Crashlytics obsługuje oba od razu po zainstalowaniu.

  • Pierwszy to GWP-ASan, czyli lekka sieć bezpieczeństwa stworzona specjalnie na potrzeby środowiska produkcyjnego.

  • Próbkuje on niewielką część alokacji sterty natywnej i otacza je stronami ochronnymi, dzięki czemu narzut procesora i pamięci jest bliski zeru.

  • W tysiącach sesji użytkowników, gdy błąd dotyczy zwolnionej lub niedostępnej pamięci w próbkowanej alokacji, GWP-ASan natychmiast go wykrywa.

  • Jednak ponieważ GWP-ASan jest z natury probabilistyczny, podczas testowania lokalnego nie chcesz uruchamiać testowego telefonu na 16 godzin lub dłużej, aby trafić na losową próbkę.

  • Co więc otrzymujesz?

  • Do deterministycznego debugowania lokalnego służy rozszerzenie ARM MTE (Memory Tagging Extension).

  • MTE używa wskaźnika sprzętowego i tagów pamięci do sprawdzania każdej alokacji. Jest dostępny na urządzeniach ARM v9, takich jak Pixel 8 i nowsze.

  • Dlatego połącz go z GWP-ASan w środowisku produkcyjnym, aby uzyskać najlepsze wyniki.

  • Jak można się spodziewać, gdy któreś z narzędzi wykryje błąd, Crashlytics automatycznie oznacza problem i umożliwia filtrowanie według GWP-ASan lub MTE oraz przypina kartę informacyjną z dokładną przyczyną uszkodzenia w panelu.

  • Zobaczmy, co dzieje się w konsoli Crashlytics, gdy GWP-ASan wykryje awarię.

  • Obok zwykłej karty zrzutu stosu znajduje się karta zrzutu stosu pamięci.

  • U góry widzimy dokładną przyczynę uszkodzenia: Use After Free.

  • Zamiast informacji o awarii wątku renderowania w libc.so GWP-ASan podaje teraz pełną historię tego bufora w naszym effect_pipeline.cc.

  • W sekcji Allocated By (Przydzielone przez) widzimy, gdzie powstał bufor.

  • Tuż pod „Deallocated By” (Zwolnione przez) widzimy, co dokładnie zwolniło pamięć z wątku renderowania.

  • Znajduje się on w linii 208 potoku efektów.

  • Otwieramy więc kod natywny w EffectPipeline i widzimy błąd.

    // 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
    }
  • Za każdym razem, gdy użytkownik przełącza filtr kolorów w wątku w tle, zwalniamy stary bufor, gdy RenderThread nadal go odczytuje.

  • Dodaliśmy więc blokadę mutex wokół zamiany bufora, wprowadziliśmy aktualizację i w końcu zobaczyliśmy, jak raporty GWP-ASan i wszystkie te tajemnicze awarie znikają niemal z dnia na dzień.

  • A nasz wskaźnik użytkowników, u których nie wystąpił błąd, wzrósł do 100%.

  • Super!

  • Pamiętasz, jak na początku prezentacji zadaliśmy sobie pytanie: „Czy to już koniec?”.

  • Zanim jednak do tego doszło, musieliśmy się zmierzyć z praktycznie wszystkimi możliwymi problemami z czasem reakcji i pamięcią.

  • Błędy ANR, błędy braku pamięci, błędy związane z ograniczeniem pamięci i awarie związane z uszkodzeniem pamięci natywnej zostały już rozwiązane.

  • Nawet jeśli zrzut stosu nie jest prawdziwy, a przyczyna błędu ukrywa się gdzieś w wątkach w C++, Android i Firebase Crashlytics dają Ci narzędzia do jej wykrycia i pewnego opublikowania aplikacji.

Podsumowanie i rejestracja w celu uzyskania wcześniejszego dostępu

  • Polina Govorkova

    Podsumujmy, z czego możesz skorzystać od razu po powrocie do laptopa: statystyki wątków ANR i inne istotne metadane za pomocą ApplicationExitInfo; automatyczne zbieranie informacji o wyzwalaczach zamknięcia OOM i ogranicznika pamięci za pomocą ProfilingManager; pełna obsługa GWP-ASan i Arm MTE w przypadku uszkodzenia pamięci natywnej.

  • W ramach prywatnej wersji testowej wprowadzamy też zrzuty sterty pól na Androidzie 17 lub nowszym.

  • Jeśli chcesz być jedną z pierwszych osób, które wypróbują automatyczne zbieranie zrzutów sterty w Firebase Crashlytics i będą je analizować za pomocą Perfetto i interfejsu wiersza poleceń Androida, zrób zdjęcie slajdu i zarejestruj się, korzystając z linku.

  • Omówiliśmy już większość zagadnień, więc dziękuję za udział w dzisiejszej sesji i poświęcony czas.

  • Życzymy, aby Twój wątek główny nie był blokowany, sterta była czysta, a następna wersja była najbardziej stabilna ze wszystkich.

  • Życzymy udanego debugowania.

  • Jeszcze raz dziękuję.

Kod

Fragmenty kodu

Fragment tekstu w 01:45 w „Wprowadzeniu”
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
Fragment tekstu w 01:58 w „Wprowadzeniu”
"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
Fragment tekstu w 04:45 w filmie „Debugowanie i analizowanie błędów ANR”
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)
Fragment tekstu w 08:08 w filmie „Debugowanie i analizowanie błędów ANR”
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)
Fragment tekstu w 11:54 w filmie „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)
Fragment kodu powłoki w 15:54 w filmie „Capturing and analyzing heap dumps”
# 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>
Fragment tekstu w 16:27 w filmie „Capturing and analyzing heap dumps”
# 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)
Fragment kodu w języku Kotlin w 16:47 w filmie „Przechwytywanie i analizowanie zrzutów stosu”
// 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
}
Fragment kodu C++ w 21:06 w filmie „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
}
Fragment kodu C++ w 25:14 w filmie „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
}