Jaga jejak memori aplikasi Anda tetap ramping, efisien, dan bebas dari kesalahan segmentasi dengan Firebase Crashlytics. Sesi ini membahas rangkaian lengkap alat pemecahan masalah memori yang tersedia di Firebase console dan Android Studio. Kita akan mulai dengan membahas cara memanfaatkan insight ANR dan laporan GWP-ASan untuk mendiagnosis bug memori yang sulit ditemukan, lalu mempelajari alat terbaru kami untuk peristiwa Kehabisan Memori (OOM) dan Penghentian Pembatas Memori (MLK), yang dirancang khusus untuk membantu Anda menavigasi dan beradaptasi dengan batas memori baru yang diperkenalkan di Android 17.
ApplicationExitInfo untuk menampilkan trace multithread dan mengidentifikasi thread pemblokiran utama.ProfilingManager Android 17 untuk mengumpulkan cuplikan heap yang telah di-deobfuscate yang dapat diperiksa di Perfetto atau melalui Android CLI.Transkrip
Polina Govorkova
Halo semuanya.
Selamat datang di Pemecahan masalah memori dengan Firebase Crashlytics.
Tunggu, jabatan perusahaan yang keren.
Mari kita ubah sedikit.
Menjaga aplikasi tetap ramping dan efisien dengan Firebase Crashlytics.
Oh, kedengarannya lebih baik.
Nama saya Polina, dan saya adalah developer software senior di tim Firebase Crashlytics, dan presenter Anda hari ini.
Pada titik tertentu dalam siklus pengembangan, Anda akan merasa telah menyelesaikan semua bug.
Kesalahan ketik penguraian JSON, pemeriksaan pointer null yang terlupakan, semuanya hilang.
Dan Anda mungkin bertanya pada diri sendiri, "Apakah saya sudah selesai?"
Sayangnya, Anda tidak demikian.
Karena yang akan tersisa adalah masalah yang sulit.
Dan saya menyatakan hari ini bahwa dua kategori masalah tersulit yang akan Anda hadapi adalah responsivitas dan memori.
Kabar baiknya, kami siap membantu Anda.
Hari ini, kami akan membahas alat yang sudah ada dan yang baru di Android dan Crashlytics untuk membantu Anda menemukan akar masalah responsivitas yang muncul melalui error Aplikasi Tidak Merespons, serta pengecualian kehabisan memori, kebocoran memori, dan kerusakan memori native.
Ayo kita mulai.
Mengapa masalah responsivitas dan memori begitu terkenal?
Ambil bug klasik: Anda mendeklarasikan string, menetapkannya ke null, dan, pada titik tertentu, mencoba membaca panjangnya.
Apa yang Anda dapatkan?
NullPointerException yang biasa terjadi.
Namun, sebenarnya mudah di sini, karena pelacakan tumpukan pengecualian mengarah ke baris kode yang tepat yang merusak aplikasi Anda.
Lihat pelacakan tumpukan di sebelah kiri.
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.mainKita dapat langsung mengetahui bahwa masalahnya ada di baris 42 di MainActivity.kt.
Pendekatan ini berlaku untuk sebagian besar bug yang tidak signifikan.
Sekarang, saat terjadi masalah responsivitas atau memori, seperti Aplikasi Tidak Merespons di sebelah kanan, stack trace hampir tidak pernah mengarah ke penyebab sebenarnya.
"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.mainDalam kasus terbaik, alat ini hanya menampilkan efeknya, yaitu beberapa thread atau alokasi yang tidak berbahaya yang kebetulan berjalan pada saat aplikasi berhenti berfungsi atau kehabisan memori.
Padahal, penyebab utamanya terjadi jauh lebih awal, atau, mungkin, di tempat lain.
Untungnya, terkait masalah ini, Anda tidak perlu menebak-nebak, karena Firebase Crashlytics memiliki jawabannya untuk kita.
Namun, sebelum kita mulai memecahkan masalah, angkat tangan jika Anda sudah familier dengan Crashlytics.
Oh, bagus.
Senang melihat semua tangan itu.
Jadi, bagi Anda yang baru mengenal Firebase Crashlytics, ini adalah solusi pelaporan error real-time yang menyediakan data lapangan, serta insight kontekstual dan berbasis AI, tips pemecahan masalah, dan sinyal yang jelas tentang rilis baru Anda, semuanya untuk membantu Anda memperbaiki masalah lebih cepat.
Crashlytics sangat disukai oleh developer kami, dan menjadi solusi pelaporan error yang paling banyak digunakan di perangkat seluler, yang mendukung lebih dari 370.000 aplikasi native, termasuk 57% aplikasi Android teratas.
Untuk melihat cara Crashlytics membantu kita melacak bug, mari kita lihat contoh konkretnya.
Hari ini, saya ingin menceritakan kisah ChromaClip.
Ini adalah ekstraktor palet warna video yang sedang saya buat, dan saya ingin memberi tahu Anda cara saya mengatasi bug responsivitas dan memori saat mengembangkannya.
Jadi, ide di balik ChromaClip sangat sederhana: pengguna memilih video, mungkin klip matahari terbenam yang indah atau adegan dengan penyesuaian warna yang bagus, dan aplikasi akan menganalisis frame untuk menghasilkan palet warna kustom, yang kemudian dapat digunakan pengguna untuk desain, seni digital, mood board, dan apa pun.
Jadi, cerita kita dimulai tepat sebelum rilis versi 1.0.
Saya ingin memastikan aplikasi ini benar-benar stabil sebelum diluncurkan, jadi saya menjalankan pengujian akhir secara lokal di beberapa perangkat, memilih video, mengetuk tombol untuk mengekstrak palet warna, dan, sialnya, dialog favorit setiap developer Android muncul, bukan?
ChromaClip tidak merespons.
Jadi, ini adalah error Aplikasi Tidak Merespons, atau dikenal sebagai ANR.
Polina Govorkova
Jadi, apa yang harus kita lakukan dalam situasi ini, selain panik?
Mengikuti insting pertama, karena ponsel pengujian dicolokkan langsung ke laptop, kita membuka Android Studio dan memeriksa Logcat.
Kita melihat stack trace untuk thread utama.
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)Apa informasi yang sebenarnya kita peroleh dari keterangan ini?
Pertama, tepat di bagian atas, kita mendapatkan konfirmasi bahwa ini adalah ANR, yang berjalan di MainActivity.
Apa lagi?
Kita melihat alasannya, yaitu waktu tunggu pengiriman input habis.
Artinya, pengguna mengetuk layar.
Ya, kami melakukannya.
Namun, thread utama sangat sibuk sehingga tidak dapat memproses peristiwa tersebut dalam waktu tunggu watchdog Android selama lima detik.
Kemudian, kita juga melihat bahwa sistem membuang file ke traces.txt dan tepat di bawahnya, logcat mencetak pelacakan tumpukan untuk thread utama kita.
Perhatikan bahwa status sama dengan S di sana.
Thread utama kita bahkan tidak secara aktif menggunakan CPU.
S berarti tidur.
Mengapa thread utama kita tidur saat kita menunggu?
Mari kita lihat frame stack lebih dekat.
Tepat di tengah trace, kita melihat thread utama kita diparkir di dalam Unsafe.park menunggu objek yang tidak diketahui.
Mengapa kita tidak tahu apa yang ditunggunya?
Sebenarnya, untuk beberapa ANR, seperti saat thread diblokir oleh kunci tersinkronisasi Java klasik, stack trace thread utama cukup baik untuk memberi tahu kita apa yang sebenarnya ditunggu, apa yang menahan kunci.
Namun, saat Anda menggunakan primitif serentak tingkat yang lebih tinggi di balik layar, logcat hanya memberi tahu kita bahwa kita sedang menunggu objek yang tidak diketahui.
Jadi, kita tidak tahu objek apa yang kita tunggu, thread pekerja latar belakang mana yang kita tunggu secara khusus, atau mengapa prosesnya memerlukan waktu lebih dari 5 detik.
Dan karena kita menguji secara lokal, kita dapat menarik traces.txt tersebut dari perangkat dan men-scroll secara manual puluhan thread yang tidak aktif, dan mungkin memburu pelakunya atau mungkin menghabiskan token untuk bertanya kepada AI.
Dalam beberapa kasus, cara ini akan berhasil, dan sebenarnya, cara ini akan berhasil untuk kasus ini.
Namun, selain saya benar-benar tidak ingin membaca dump thread mentah secara manual-- jujur saja, itu sangat ketinggalan zaman-- saya tahu bahwa pendekatan ini bahkan tidak dapat diterapkan sama sekali setelah kita berada dalam produksi.
Kita tidak memiliki akses langsung ke file traces.txt di ponsel pengguna, bukan?
Jadi, untung saja kita sudah mengintegrasikan Firebase Crashlytics.
Karena fitur ini menghilangkan kebutuhan akan penelusuran dan menyelesaikan masalah ANR bahkan dalam produksi.
Bagaimana caranya?
Di sinilah ApplicationExitInfo API Android berperan.
Mulai Android 11, setiap kali sistem menandai ANR, atau menghentikan proses aplikasi Anda, OS akan mencatat metadata keluar yang lengkap, termasuk alasan pasti error, status memori, dan pelacakan sistem lengkap untuk setiap thread yang berjalan dalam proses Anda saat ini.
Dan bagian terbaiknya, Anda tidak perlu menginstal plugin tambahan atau menulis satu baris kode pun.
Selama Anda menggunakan Crashlytics SDK 18.3.5 atau yang lebih tinggi, fitur ini akan langsung berfungsi.
Jadi, segera setelah aplikasi Anda dimulai ulang, Crashlytics SDK akan otomatis mengkueri ApplicationExitInfo, mengambil dump multithread lengkap bersama telemetri perangkat, dan mengirimkannya ke backend Crashlytics.
Mari kita buka Crashlytics console dan lihat apa yang ditampilkan.
Ini adalah tampilan detail masalah konsol Firebase Crashlytics untuk ANR, dan kita langsung melihat di tab stack trace, 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)Ini sebenarnya adalah thread yang muncul di bagian paling atas oleh Crashlytics yang menjadi bottleneck untuk thread utama kita, dan Crashlytics menandainya dengan badge yang disebut Root blocking.
Seperti yang dijelaskan tooltip pada badge, thread pekerja ini memang merupakan thread yang memblokir thread yang memicu ANR.
Ngomong-ngomong, Crashlytics juga memiliki badge cerdas untuk penyebab lainnya.
Jadi, misalnya, jika thread macet saat membaca file besar atau menjalankan kueri database yang sangat lambat, dan jika thread tersebut memicu ANR, thread tersebut akan diberi tag sebagai pemblokiran root I/O.
Jika dua thread macet karena saling menunggu, Crashlytics akan menandainya sebagai deadlock.
Sekarang, mari kita lihat frame stack itu sendiri dalam thread pemblokiran root.
Jadi, PaletteExtractor processVideo kita memanggil FrameDecoder getUncompressedFrame yang ditandai dengan warna biru, dan itu benar-benar terjebak di dalam createBitmap hanya untuk mencoba mengalokasikan memori untuk satu frame.
Karena kita tahu bahwa aplikasi tidak menunggu kunci lain, dan kita tahu bahwa aplikasi tidak berada dalam panggilan jaringan, tersangka utama kita adalah memori, dan kita dapat langsung mengonfirmasinya dengan memeriksa memori kosong perangkat di sini, di konsol Crashlytics juga.
Lihat dua tempat Crashlytics menampilkan hal ini.
Di bawah tab data untuk peristiwa tertentu tepat di samping tab stack trace yang baru saja kita jelajahi, dan juga di kanan atas yang menampilkan rata-rata di semua peristiwa.
Dan dalam kedua kasus tersebut, sepertinya hampir tidak ada apa-apa, bahkan tidak sampai 100 KB.
Jadi, mari kita pikirkan.
Yang terjadi di sini adalah karena prosesnya kekurangan memori, Android runtime menghabiskan seluruh waktunya untuk menjalankan pembersihan sampah memori dengan panik, mencoba menemukan ruang yang cukup untuk bitmap berikutnya, dan tidak pernah mendapatkannya.
Membekukan thread pekerja dan membuat UI tidak responsif selama lebih dari lima detik.
Jadi, berikut tips singkat untuk Anda.
Sebelum Anda mendapatkan, Setiap kali Anda mendapatkan ANR yang dipasangkan dengan jenis RAM kosong yang rendah ini, itu adalah bukti kuat bahwa ANR sebenarnya adalah masalah kebocoran memori atau bloat.
Sekarang, pertanyaannya adalah apakah aplikasi kita yang menjadi korban atau apakah aplikasi kita juga yang menimbun memori?
Jadi, mari kita kembali ke daftar masalah project dan melihat apakah ada hal lain yang terasa relevan.
Polina Govorkova
Dan lihat di sana.
Di bagian bawah, kita melihat daftar masalah, dan tepat di atas ANR dengan delapan peristiwa, kita mendapatkan pengecualian OutOfMemory.
Jadi, mungkin kitalah pelakunya.
Dan itu sebenarnya adalah petunjuk yang sangat besar.
Karena di Android, OutOfMemoryError Java, atau singkatnya OOM, tidak hanya berarti ponsel kehabisan RAM.
Artinya, proses aplikasi kita sendiri melampaui batas heap Java Android Runtime-nya.
Jadi, pada tahap ini, teori kerja saya adalah ANR dan OOM sebenarnya adalah dua sisi dari mata uang yang sama.
Bergantung pada pengaturan waktu selama pengujian, ChromaClip akan terjebak dalam thrashing di dalam pembersihan sampah memori yang cukup lama untuk memicu ANR, atau menghabiskan heap sepenuhnya, sehingga menyebabkan OOM.
Mari kita selidiki pengecualian OutOfMemory untuk memastikannya.
Dimulai dengan stack trace.
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)Tunggu sebentar.
Bitmap.createBitmap, itu sudah tidak asing lagi.
Bukankah kita pernah melihatnya sebelumnya?
Namun, lihat dari mana fungsi ini dipanggil kali ini.
Gagal mengalokasikan 256 byte saat membuka aktivitas setelan.
Kita jatuh ke perangkap yang sama lagi.
Jika aplikasi Anda error karena tidak dapat meng-inflate ikon setelan kecil, aktivitas setelan tidak akan menjadi penyebabnya.
Hanya pemanggil yang tidak beruntung yang meminta sedikit memori saat tidak ada memori yang tersedia.
Jadi, untuk menemukan penyebab sebenarnya, kita perlu memperkecil tampilan dan melihat semua yang ada di memori saat error terjadi.
Dengan kata lain, kita memerlukan heap dump Java.
Di meja kerja, jika Anda tahu cara memicu OOM, Anda dapat mengambil heap dump Java secara manual dengan Memory Profiler Android Studio.
Namun, saat OOM terjadi secara tidak terduga, di seluruh pengujian atau di lingkungan produksi, sekali lagi, metode ini tidak akan berfungsi.
Polina Govorkova
Jadi, ini adalah momen gemilang lainnya untuk Firebase Crashlytics.
Selain info keluar aplikasi, Crashlytics kini terintegrasi dengan ProfilingManager API Android.
ProfilingManager API di Android 17 dilengkapi dengan pembuatan profil memori yang dipicu sistem, dan itulah yang kita butuhkan di sini.
Artinya, tepat saat aplikasi Anda mengalami error OutOfMemory atau melanggar batas memori sistem, OS akan mengambil cuplikan heap di latar belakang, lalu di Crashlytics, pengumpulan yang dipicu akan langsung tersedia secara gratis asalkan Anda menggunakan Crashlytics SDK 20.1.0 atau yang lebih tinggi.
Kemudian, Crashlytics akan otomatis mengelompokkan penghentian OOM dan Pembatas Memori, yang akan kita bahas lebih lanjut nanti, di dasbor.
Saat Anda memilih ikut serta dalam pengumpulan heap dump melalui Firebase console atau CLI, Crashlytics akan otomatis mulai mengumpulkan cuplikan heap yang diambil, melakukan de-obfuscation untuk Anda, dan pada dasarnya, Anda akan mendapatkan rontgen lengkap dari semua yang ada di heap Java aplikasi Anda pada saat error.
Jadi, berikut tampilan OOM ChromaClip di Crashlytics console setelah heap diambil.
Di bagian atas, Anda akan melihat penjelasan mengapa Java menghabiskan heap dan mengapa hal itu terjadi.
Kemudian, Anda juga akan melihat tips pemecahan masalah yang berguna dan petunjuk ke referensi eksternal.
Di tab pelacakan tumpukan, tepat di atas aktivitas setelan yang menyesatkan itu, Crashlytics sebenarnya menandai bahwa pelacakan tumpukan OOM tidak deterministik, sehingga orang tidak perlu membuang waktu melihat pelacakan tumpukan seperti yang baru saja kita lakukan.
Kemudian, Crashlytics akan melampirkan file Perfetto yang telah di-deobfuscate, yaitu heap dump, untuk sesi tersebut.
Dan dari sini, kita dapat mendownload file.
Kita dapat menjelajahi semua dump di Google Cloud Storage, atau membukanya di UI Perfetto.
Jadi, mengklik Analyze di UI Perfetto akan membuka penjelajah heap dump Perfetto.
Sekarang, angkat tangan jika Anda sudah pernah melihatnya.
Baiklah, ada beberapa orang yang mengangkat tangan.
Bagi saya, tampilan ini sangat mengintimidasi, tetapi, dalam hal ini, masalahnya sebenarnya cukup sederhana.
Lihat kumpulan batang besar yang membentang hampir di seluruh heap Java.
Jadi, sebagian besar adalah array objek pasangan, yang masing-masing menggabungkan warna dan beberapa instance string.
Oh!
Sepertinya saya tahu apa yang terjadi.
Kode kita mengalokasikan warna ditambah beberapa data pendukung untuk setiap piksel dalam setiap frame, dan menyimpan jutaan piksel tersebut dalam daftar.
Jadi, tidak heran jika aktivitas Setelan tidak dapat mengalokasikan 256 byte tersebut.
Setidaknya satu misteri terpecahkan.
Mari kita perbaiki.
Pada tahap ini, kita memiliki beberapa opsi.
Jadi, karena Perfetto sudah mengarahkan kita ke PaletteExtractor, kita dapat membuka IDE dan menulis ulang loop secara manual, atau menggunakan agen coding AI.
Atau, kita dapat mengabaikan pemeriksaan heap manual sepenuhnya, dan memanfaatkan Android CLI.
# 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>Jadi, dengan menginstal skill Android CLI dan Perfetto, Anda dapat mencantumkan dan mendownload heap dump Crashlytics yang telah di-deobfuscate, mengkueri objek teratas yang dipertahankan dari heap dump ini melalui trace_processor, dan mengambil data masalah Crashlytics yang cocok seperti kunci kustom, log, breadcrumb, peluncuran terkait, dan lainnya, semuanya dari terminal Anda.
Lebih baik lagi, lihat bagian bawah slide.
Jangan jalankan perintah ini sendiri.
Sekali lagi, minta agen coding AI favorit Anda dalam bahasa alami untuk memeriksa heap dump OOM terbaru menggunakan perintah Android dan mengirimkan perbaikannya.
# 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)Dan Gemini akan memiliki konteks yang cukup untuk melakukan seluruh alur untuk Anda.
Jadi, mencantumkan OOM, mendownload rekaman aktivitas, menjalankan analisis, dan membuat patch kode langsung di project Anda.
Dan lihat patch ini di file 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
}Alih-alih mengalokasikan jutaan objek, kini kita cukup melakukan downsampling frame dan menghitung bilangan bulat mentah secara langsung.
Jadi, mari kita terapkan perbaikan, tutup masalah, tingkatkan versi, dan terus lakukan pengujian.
Semoga saja, error kehabisan memori tidak hanya akan hilang, tetapi karena kita tidak akan lagi mengalami masalah dalam pembersihan sampah memori, semoga ANR yang mengganggu itu juga akan hilang.
Jadi, kami men-deploy build patch di seluruh perangkat pengujian, memilih klip baru, dan mulai mengekstrak palet.
Dan UI akan berhenti berfungsi lagi.
ANR lainnya.
Menariknya, setelah melakukan lebih banyak pengujian, kami menemukan bahwa beberapa perangkat masih mengalami ANR, tetapi untuk perangkat lain, aplikasi kini mengalami error secara acak, dan cukup sering, bahkan lebih sering daripada saat mengalami OOM.
Namun, kita telah memperbaiki OOM, jadi apa yang terjadi?
Polina Govorkova
Nah, mari kita kembali ke Crashlytics.
Kabar baiknya dulu.
Lihat daftar masalah kami.
Pengecualian kehabisan memori kita benar-benar hilang, seperti yang kita harapkan.
Kabar buruknya?
Sekarang, ChromaClip bertahan sedikit lebih lama, sehingga memiliki waktu untuk mengungkap apa lagi yang tersembunyi di baliknya.
Jadi, ANR kami tidak hilang.
Dan sekarang kita memiliki dua masalah pada kode native di bagian bawah, untungnya hanya dengan beberapa peristiwa.
Dan tepat di bagian paling atas, dengan 11 peristiwa, kita memiliki penghentian Memory Limiter, untungnya, sudah diberi badge sebagai memiliki heap dump.
Sebentar.
Apa yang dimaksud dengan penghentian paksa oleh Pembatas Memori?
Boleh saya minta Anda mengangkat tangan, siapa di antara Anda yang pernah mendengarnya?
Oke.
Beberapa kartu langka.
Mari belajar bersama.
Jika Anda belum pernah melihat penghentian oleh Pembatas Memori, itu karena fitur ini baru tersedia di Android 17.
OOM Java hanya terpicu saat Anda melampaui batas heap Java terkelola, tetapi saat total jejak memori aplikasi Anda juga mencakup RAM native, seperti buffer grafis dan alokasi C++, sebelumnya, tidak ada batas per aplikasi.
Jadi, kita akan memiliki low-memory killer, yang bersifat global.
Fitur ini akan berfungsi saat beberapa aplikasi menghabiskan RAM sistem hingga seluruh ponsel mengalami gangguan.
Kini, mulai Android 17, Pembatas Memori membatasi jejak memori setiap aplikasi.
Jadi, saat proses menggunakan jumlah RAM yang tidak proporsional, OS akan menghentikan aplikasi tersebut.
Crashlytics mencatat semua detail tersebut dan mengelompokkan penghentian bersama-sama di dasbor.
Jadi, dengan mengklik penghentian Memory Limiter di Crashlytics, seperti halnya OOM, Anda akan mendapatkan penjelasan mengapa OS menghentikan aplikasi, sehingga Anda tidak perlu menghafal apa yang baru saja saya katakan.
Anda juga akan mendapatkan tips tentang cara mengatasinya, dan link langsung ke dokumen penerapan memori Android.
Sama seperti OOM, stack trace menyalahkan frame yang tidak bersalah saat OS menghentikan proses, dan dasbor Crashlytics menyorotinya.
Sekarang, penghentian Pembatas Memori terjadi terutama di kode native karena lingkungan native tidak memiliki lapisan yang menangani keamanan dan alokasi memori.
Jika tidak, akan terjadi OOM.
Jadi, pertanyaan pertama kita adalah, apakah objek Kotlin kita menyimpan alokasi besar yang didukung native?
Atau apakah ini kode C++ mentah yang bocor dengan sendirinya?
Mari kita klik Analyze in Perfetto UI untuk mencari tahu.
Lihat tab Overview di penjelajah heap dump Perfetto.
Kita melihat bahwa di bagian informasi umum, anon RSS plus swap menunjukkan bahwa total memori proses mencapai 4 GB.
Oke.
Namun, di bawahnya, kita melihat bahwa di bagian Bytes Retained by Heap, seluruh heap terkelola kita, termasuk memori native yang terkait dengan objek Kotlin seperti bitmap, hanya berukuran 42 MB.
Oke.
Jadi, dalam 10 detik, kita mengesampingkan semua kode Kotlin dan Java.
Jadi, penyebabnya pasti kebocoran dari lapisan C++ native.
Dan karena ChromaClip memiliki lapisan C++ tipis untuk pemrosesan frame, kita tidak perlu mencari jauh-jauh.
Dengan mengarahkan agen coding atau profiler native Android Studio ke NativePaletteEngine.cpp processFrame, bug akan langsung terlihat.
// 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
}Jadi, di setiap frame video, kode C++ kita mengalokasikan buffer sementara, yang tidak terlalu sementara jika Anda tidak merilisnya, kesalahan yang sangat amatir.
Jadi, saatnya menghubungkan berbagai informasi.
Apa yang terjadi?
Setelah kita memperbaiki kebocoran Java, kebocoran C++ akan memiliki waktu untuk terakumulasi dan menjadi tidak terkendali.
Di Android 17, Pembatas Memori mendeteksinya dan menghentikan aplikasi.
Namun, di perangkat lama yang belum memiliki Pembatas Memori, masalah ini muncul sebagai ANR.
Jadi, akhirnya, semuanya masuk akal.
Mari kita tambahkan delete[] scratch di sini, tingkatkan versi, dan kirim update.
Dengan hilangnya penghentian paksa dan ANR akibat Pembatas Memori, kita siap merilis versi 1.0.
Menarik.
Berkat kecepatan dan stabilitas ChromaClip sekarang, aplikasi kami menjadi populer.
Ribuan orang mengupload dan me-remix video setiap hari.
Namun, dengan semua traffic tersebut, ingatkah Anda dengan masalah pada kode native misterius yang kita lihat sebelumnya di bagian bawah dasbor?
Mereka juga menjadi liar.
Tiba-tiba, jumlahnya jadi jauh lebih banyak.
Dan tugas kami belum selesai.
Polina Govorkova
Jadi, buka masalah pada kode native tersebut di Crashlytics, lalu lihat stack trace-nya.
Tidak ada satu baris kode pun dari aplikasi kita sendiri.
RenderThread error jauh di dalam libc.so saat mengosongkan memori.
Tips lainnya, jika thread sistem mengalami error di dalam pengalokasi memori tanpa ada kode aplikasi Anda di dalamnya, itu adalah tanda umum kerusakan memori native.
Jadi, kode C++ kita merusak atau mengosongkan buffer lebih awal, dan thread render baru tersandung setelahnya.
Jadi, bagaimana cara kita menangkap pelaku sebenarnya tepat saat kerusakan terjadi?
Android memberi kita dua alat yang andal, dan Crashlytics mendukung keduanya secara langsung.
Pertama, GWP-ASan, yang merupakan jaring pengaman ringan yang dibuat khusus untuk produksi.
Alat ini mengambil sampel sebagian kecil alokasi heap native Anda dan mengelilinginya dengan halaman pelindung sehingga overhead CPU dan memori hampir nol.
Di ribuan sesi pengguna di lapangan, saat bug menyentuh memori yang dikosongkan atau di luar batas pada alokasi sampel, GWP-ASan akan langsung menangkapnya.
Namun, karena GWP-ASan bersifat probabilistik, saat menguji berbagai hal secara lokal, Anda tidak ingin menjalankan ponsel pengujian selama 16 jam atau lebih untuk mendapatkan sampel acak.
Jadi, apa yang akan Anda dapatkan?
Untuk proses debug lokal deterministik, Anda dapat menggunakan ARM MTE, atau Memory Tagging Extension.
MTE menggunakan pointer hardware dan tag memori untuk memeriksa setiap alokasi, dan tersedia di perangkat ARM v9 seperti Pixel 8 dan yang lebih baru.
Jadi, pasangkan dengan GWP-ASan dalam produksi, dan Anda akan mendapatkan yang terbaik dari keduanya.
Seperti yang mungkin Anda duga, saat salah satu alat menemukan bug, Crashlytics akan otomatis memberi badge pada masalah tersebut dan memungkinkan Anda memfilter berdasarkan GWP-ASan atau MTE serta menyematkan kartu info dengan penyebab kerusakan yang tepat di dasbor.
Jadi, lihat apa yang terjadi di Crashlytics console saat GWP-ASan mendeteksi error.
Di samping tab stack trace reguler, ada stack trace memori.
Dan di bagian atas, kita melihat penyebab kerusakan yang sebenarnya, yaitu Use After Free.
Alih-alih menampilkan error thread render di libc.so, GWP-ASan kini memberi kita biografi lengkap buffer tersebut di dalam effect_pipeline.cc kita sendiri.
Jadi, di bagian Allocated By, kita melihat tempat buffer dibuat.
Dan tepat di bawah Deallocated By, kita melihat penyebab pasti yang membebaskannya dari thread render.
Dan itu tersembunyi di baris 208 pada pipeline efek kita.
Jadi, kita membuka kode native di EffectPipeline, dan bug-nya terlihat jelas.
// 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
}Setiap kali pengguna beralih filter warna di thread latar belakang, kita mengosongkan buffer lama saat RenderThread masih membacanya.
Oke, jadi kita tambahkan kunci mutex di sekitar pertukaran buffer, kirim update, dan akhirnya melihat laporan GWP-ASan dan semua error misterius itu hilang hampir dalam semalam.
Metrik pengguna bebas error kami pun melonjak hingga 100%.
Hore!
Ingat, di awal diskusi, kita bertanya kepada diri sendiri: "Apakah saya sudah selesai?"
Tepat sebelum terkena dampak dari hampir semua masalah responsivitas dan memori yang ada.
Nah, ANR, error Kehabisan Memori, penghentian pembatas memori, dan error kerusakan memori native kini telah diselesaikan.
Meskipun stack trace berbohong dan penyebabnya bersembunyi di suatu tempat di seluruh thread di C++, Android dan Firebase Crashlytics memberi Anda alat untuk melacak dan mengirimkan dengan percaya diri.
Polina Govorkova
Jadi, mari kita rangkum apa yang dapat Anda gunakan langsung saat kembali ke laptop: insight thread ANR dan metadata relevan lainnya melalui ApplicationExitInfo; pengumpulan otomatis pemicu penghentian OOM dan pembatas memori melalui ProfilingManager; serta dukungan penuh GWP-ASan dan Arm MTE untuk kerusakan memori native.
Dan diluncurkan dalam pratinjau pribadi, heap dump lapangan di Android 17 atau yang lebih baru.
Jika Anda ingin menjadi salah satu yang pertama mencoba pengumpulan heap dump otomatis di Firebase Crashlytics dan menganalisisnya dengan Perfetto dan Android CLI, ambil foto slide ini, dan daftar melalui link tersebut.
Jadi, setelah kita membahas sebagian besar materi, saya ingin berterima kasih atas kehadiran dan waktu Anda hari ini.
Semoga thread utama Anda tetap tidak diblokir, heap Anda tetap bersih, dan rilis berikutnya menjadi yang paling stabil.
Selamat melakukan proses debug.
Sekali lagi, terima kasih banyak.
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
}