Library JankStats

Library JankStats membantu Anda melacak dan menganalisis masalah performa di aplikasi. Jank mengacu pada frame aplikasi yang memerlukan waktu terlalu lama untuk dirender, dan library JankStats memberikan laporan tentang statistik jank aplikasi Anda.

Kemampuan

JankStats dibangun berdasarkan kemampuan platform Android yang ada, termasuk FrameMetrics API di Android 7 (API level 24) dan yang lebih baru atau OnPreDrawListener pada versi sebelumnya. Mekanisme ini dapat membantu aplikasi melacak waktu yang diperlukan untuk menyelesaikan frame. Library JankStats memberikan dua kemampuan tambahan yang membuatnya lebih dinamis dan lebih mudah digunakan: heuristik jank dan status UI.

Heuristik jank

Meskipun Anda dapat menggunakan FrameMetrics untuk melacak durasi frame, FrameMetrics tidak memberikan bantuan apa pun dalam menentukan jank aktual. Namun, JankStats memiliki mekanisme internal yang dapat dikonfigurasi untuk menentukan kapan jank terjadi sehingga laporan menjadi lebih berguna.

Status UI

Sering kali Anda perlu mengetahui konteks masalah performa di aplikasi Anda. Misalnya, jika Anda mengembangkan aplikasi multilayar yang kompleks yang menggunakan FrameMetrics dan menemukan bahwa aplikasi tersebut sering memiliki banyak frame yang mengalami jank, Anda perlu melakukan kontekstualisasi informasi tersebut dengan mengetahui lokasi terjadinya masalah, apa yang dilakukan pengguna, dan cara mereplikasinya.

JankStats mengatasi masalah ini dengan memperkenalkan state API yang memungkinkan Anda berkomunikasi dengan library untuk memberikan informasi tentang Activity aplikasi. Saat JankStats melakukan log informasi tentang frame yang mengalami jank, log akan menyertakan status aplikasi saat ini dalam laporan jank.

Penggunaan

Untuk mulai menggunakan JankStats, buat instance dan aktifkan library untuk setiap Window. Setiap objek JankStats hanya melacak data dalam Window. Pembuatan instance library memerlukan instance Window dan pemroses OnFrameListener, yang keduanya digunakan untuk mengirim metrik ke klien. Pemroses dipanggil dengan FrameData pada setiap frame dan menjelaskan:

  • Waktu mulai frame
  • Nilai durasi
  • Apakah frame harus dianggap sebagai jank atau tidak
  • Serangkaian pasangan String yang berisi informasi tentang status aplikasi selama frame

Agar JankStats lebih berguna, aplikasi harus mengisi library dengan informasi status UI yang relevan untuk pelaporan di FrameData. Anda dapat melakukannya melalui PerformanceMetricsState API (bukan JankStats secara langsung), tempat semua logika pengelolaan status dan API berada.

Inisialisasi

Untuk mulai menggunakan library JankStats, tambahkan dependensi JankStats terlebih dahulu ke file Gradle Anda:

implementation "androidx.metrics:metrics-performance:1.0.0-beta01"

Berikutnya, lakukan inisialisasi dan aktifkan JankStats untuk setiap Window. Anda juga harus menjeda pelacakan JankStats jika suatu Activity masuk ke latar belakang. Buat dan aktifkan objek JankStats dalam penggantian Activity Anda:

class JankLoggingActivity : AppCompatActivity() {

    private lateinit var jankStats: JankStats


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // ...
        // metrics state holder can be retrieved regardless of JankStats initialization
        val metricsStateHolder = PerformanceMetricsState.getHolderForHierarchy(binding.root)

        // initialize JankStats for current window
        jankStats = JankStats.createAndTrack(window, jankFrameListener)

        // add activity name as state
        metricsStateHolder.state?.putState("Activity", javaClass.simpleName)
        // ...
    }

Contoh di atas memasukkan informasi status tentang Activity saat ini setelah membuat objek JankStats. Semua laporan FrameData mendatang yang dibuat untuk objek JankStats ini kini juga mencakup informasi Activity.

Metode JankStats.createAndTrack mengambil referensi ke objek Window, yang merupakan proxy untuk hierarki Tampilan di dalam Window tersebut serta untuk Window itu sendiri. jankFrameListener dipanggil pada thread yang sama dengan yang digunakan untuk mengirimkan informasi tersebut dari platform ke JankStats secara internal.

Untuk mengaktifkan pelacakan dan pelaporan pada objek JankStats, panggil isTrackingEnabled = true. Meskipun diaktifkan secara default, menjeda aktivitas akan menonaktifkan pelacakan. Dalam hal ini, pastikan untuk mengaktifkan kembali pelacakan sebelum melanjutkan. Untuk menghentikan pelacakan, panggil isTrackingEnabled = false.

override fun onResume() {
    super.onResume()
    jankStats.isTrackingEnabled = true
}

override fun onPause() {
    super.onPause()
    jankStats.isTrackingEnabled = false
}

Pelaporan

Library JankStats melaporkan semua pelacakan data Anda, untuk setiap frame, ke OnFrameListener untuk objek JankStats yang diaktifkan. Aplikasi dapat menyimpan dan menggabungkan data ini untuk diupload di lain waktu. Untuk mengetahui informasi selengkapnya, lihat contoh yang diberikan di bagian Agregasi.

Anda harus membuat dan menyediakan OnFrameListener untuk aplikasi agar dapat menerima laporan per frame. Pemroses ini dipanggil di setiap frame untuk menyediakan data jank yang sedang berlangsung ke aplikasi.

private val jankFrameListener = JankStats.OnFrameListener { frameData ->
    // A real app could do something more interesting, like writing the info to local storage and later on report it.
    Log.v("JankStatsSample", frameData.toString())
}

Pemroses menyediakan informasi per frame tentang jank dengan objek FrameData. File ini berisi informasi berikut tentang frame yang diminta:

Jika menggunakan Android 12 (API level 31) atau yang lebih tinggi, Anda dapat menggunakan cara berikut untuk menampilkan lebih banyak data tentang durasi frame:

Gunakan StateInfo di pemroses untuk menyimpan informasi tentang status aplikasi.

Perlu diperhatikan bahwa OnFrameListener dipanggil pada thread yang sama dengan yang digunakan secara internal untuk mengirimkan informasi per frame ke JankStats. Di Android versi 6 (API level 23) dan yang lebih rendah, thread tersebut adalah thread (UI) Utama. Pada Android versi 7 (API level 24) dan yang lebih baru, thread tersebut dibuat untuk dan digunakan oleh FrameMetrics. Dalam kedua kasus tersebut, penting untuk menangani callback dan kembali dengan cepat untuk mencegah masalah performa pada thread tersebut.

Selain itu, perhatikan bahwa objek FrameData yang dikirim dalam callback digunakan kembali pada setiap frame agar tidak perlu mengalokasikan objek baru untuk pelaporan data. Artinya, Anda harus menyalin dan meng-cache data tersebut di tempat lain karena objek tersebut harus dianggap statis dan tidak berlaku lagi segera setelah callback ditampilkan.

Menggabungkan

Anda mungkin ingin kode aplikasi menggabungkan data per frame agar dapat menyimpan dan mengupload informasi sesuai kebijaksanaan Anda sendiri. Meskipun detail seputar penyimpanan dan upload berada di luar cakupan rilis alfa JankStats API, Anda dapat melihat Activity awal untuk menggabungkan data per frame ke dalam koleksi yang lebih besar menggunakan JankAggregatorActivity yang tersedia di Repositori GitHub.

JankAggregatorActivity menggunakan class JankStatsAggregator untuk melapisi mekanisme pelaporannya sendiri di atas mekanisme OnFrameListener JankStats guna memberikan abstraksi level yang lebih tinggi untuk hanya melaporkan kumpulan informasi yang mencakup banyak frame.

Daripada membuat objek JankStats secara langsung, JankAggregatorActivity akan membuat objek JankStatsAggregator, yang membuat objek JankStats sendiri secara internal:

class JankAggregatorActivity : AppCompatActivity() {

    private lateinit var jankStatsAggregator: JankStatsAggregator


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // ...
        // Metrics state holder can be retrieved regardless of JankStats initialization.
        val metricsStateHolder = PerformanceMetricsState.getHolderForHierarchy(binding.root)

        // Initialize JankStats with an aggregator for the current window.
        jankStatsAggregator = JankStatsAggregator(window, jankReportListener)

        // Add the Activity name as state.
        metricsStateHolder.state?.putState("Activity", javaClass.simpleName)
    }

Mekanisme serupa digunakan di JankAggregatorActivity untuk menjeda dan melanjutkan pelacakan, dengan penambahan peristiwa pause() sebagai sinyal untuk memberikan laporan dengan panggilan ke issueJankReport(), karena perubahan siklus proses menjadi waktu yang tepat untuk menangkap status jank dalam aplikasi:

override fun onResume() {
    super.onResume()
    jankStatsAggregator.jankStats.isTrackingEnabled = true
}

override fun onPause() {
    super.onPause()
    // Before disabling tracking, issue the report with (optionally) specified reason.
    jankStatsAggregator.issueJankReport("Activity paused")
    jankStatsAggregator.jankStats.isTrackingEnabled = false
}

Contoh kode di atas adalah semua yang diperlukan aplikasi untuk mengaktifkan JankStats dan menerima data frame.

Mengelola status

Anda mungkin ingin memanggil API lain untuk menyesuaikan JankStats, Misalnya, memasukkan informasi status aplikasi membuat data frame lebih membantu dengan memberikan konteks untuk frame tempat jank terjadi.

Metode statis ini mengambil objek MetricsStateHolder saat ini untuk hierarki Tampilan tertentu.

PerformanceMetricsState.getHolderForHierarchy(view: View): MetricsStateHolder

Setiap tampilan dalam hierarki aktif dapat digunakan. Secara internal, pemeriksaan ini akan memeriksa apakah terdapat objek Holder yang ada yang terkait dengan hierarki tampilan tersebut. Informasi ini di-cache dalam tampilan di bagian atas hierarki tersebut. Jika tidak ada objek tersebut, getHolderForHierarchy() akan membuatnya.

Metode getHolderForHierarchy() statis memungkinkan Anda menghindari meng-cache instance holder di suatu tempat untuk pengambilan nanti, dan mempermudah pengambilan objek status yang ada dari mana saja dalam kode (atau bahkan kode library, yang tidak akan memiliki akses ke instance asli).

Perhatikan bahwa nilai yang ditampilkan adalah objek holder, bukan objek status itu sendiri. Nilai objek status di dalam holder ditetapkan hanya oleh JankStats. Artinya, jika aplikasi membuat objek JankStats untuk jendela yang berisi hierarki tampilan tersebut, objek status akan dibuat dan ditetapkan. Jika tidak, tanpa perlu JankStats melacak informasi, objek status tidak diperlukan, dan kode aplikasi atau library tidak diperlukan untuk memasukkan status.

Pendekatan ini memungkinkan untuk mengambil holder yang dapat diisi oleh JankStats. Kode eksternal dapat meminta holder kapan saja. Pemanggil dapat meng-cache objek Holder ringan dan menggunakannya kapan saja untuk menetapkan status, bergantung pada nilai properti state internalnya, seperti pada kode contoh di bawah, dengan status hanya ditetapkan jika properti status internal holder non-null:

val metricsStateHolder = PerformanceMetricsState.getHolderForHierarchy(binding.root)
// ...
metricsStateHolder.state?.putState("Activity", javaClass.simpleName)

Untuk mengontrol status UI/aplikasi, aplikasi dapat memasukkan (atau menghapus) status dengan metode putState dan removeState. JankStats melakukan log stempel waktu untuk panggilan ini. Jika frame tumpang-tindih dengan waktu mulai dan berakhir status, JankStats akan melaporkan informasi tersebut bersama dengan data waktu untuk frame tersebut.

Untuk status apa pun, tambahkan dua informasi: key (kategori status, seperti “RecyclerView”) dan value (informasi tentang apa yang terjadi saat itu, seperti “scrolling”).

Hapus status menggunakan metode removeState() jika status tersebut tidak lagi valid, untuk memastikan informasi yang salah atau menyesatkan tidak dilaporkan dengan data frame.

Memanggil putState() dengan key yang ditambahkan sebelumnya akan menggantikan value status yang ada dengan status baru.

Versi putSingleFrameState() API status menambahkan status yang hanya di-log satu kali, pada frame yang dilaporkan berikutnya. Setelah itu, sistem akan otomatis menghapusnya sehingga Anda tidak akan kehilangan status obsolete dalam kode secara tidak sengaja. Perhatikan bahwa tidak ada singleFrame yang setara dengan removeState() karena JankStats menghapus status frame tunggal secara otomatis.

private val scrollListener = object : RecyclerView.OnScrollListener() {
    override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
        // check if JankStats is initialized and skip adding state if not
        val metricsState = metricsStateHolder?.state ?: return

        when (newState) {
            RecyclerView.SCROLL_STATE_DRAGGING -> {
                metricsState.putState("RecyclerView", "Dragging")
            }
            RecyclerView.SCROLL_STATE_SETTLING -> {
                metricsState.putState("RecyclerView", "Settling")
            }
            else -> {
                metricsState.removeState("RecyclerView")
            }
        }
    }
}

Perhatikan bahwa kunci yang digunakan untuk status harus cukup bermakna guna memungkinkan analisis nanti. Khususnya, karena status dengan key yang sama seperti status yang ditambahkan sebelumnya akan menggantikan nilai sebelumnya, Anda harus mencoba menggunakan nama key unik untuk objek yang mungkin memiliki instance berbeda di aplikasi atau library Anda. Misalnya, sebuah aplikasi dengan lima RecyclerView yang berbeda mungkin ingin menyediakan kunci yang dapat diidentifikasi untuk setiap RecyclerView, bukan hanya menggunakan RecyclerView untuk setiap RecyclerView dan kemudian tidak dapat dengan mudah membedakan instance mana yang dirujuk oleh data frame dalam hasil data.

Heuristik jank

Untuk menyesuaikan algoritme internal guna menentukan apa saja yang dianggap sebagai jank, gunakan properti jankHeuristicMultiplier.

Secara default, sistem menentukan jank sebagai frame yang perlu dirender dua kali lebih lama dari kecepatan refresh saat ini. Sistem tidak memperlakukan jank sebagai sesuatu yang melebihi kecepatan refresh karena informasi terkait waktu rendering aplikasi tidak sepenuhnya jelas. Oleh karena itu, lebih baik menambahkan buffer dan hanya melaporkan masalah jika menyebabkan masalah performa yang terlihat.

Kedua nilai ini dapat diubah melalui metode ini agar lebih sesuai dengan situasi aplikasi, atau dalam pengujian untuk memaksa jank agar terjadi atau tidak terjadi, sebagaimana diperlukan untuk pengujian.

Penggunaan di Jetpack Compose

Saat ini ada sangat sedikit penyiapan yang diperlukan untuk menggunakan JankStats di Compose. Untuk mempertahankan PerformanceMetricsState di seluruh perubahan konfigurasi, ingat seperti berikut:

/**
 * Retrieve MetricsStateHolder from compose and remember until the current view changes.
 */
@Composable
fun rememberMetricsStateHolder(): PerformanceMetricsState.Holder {
    val view = LocalView.current
    return remember(view) { PerformanceMetricsState.getHolderForHierarchy(view) }
}

Untuk menggunakan JankStats, tambahkan status saat ini ke stateHolder seperti yang ditampilkan di sini:

val metricsStateHolder = rememberMetricsStateHolder()

// Reporting scrolling state from compose should be done from side effect to prevent recomposition.
LaunchedEffect(metricsStateHolder, listState) {
    snapshotFlow { listState.isScrollInProgress }.collect { isScrolling ->
        if (isScrolling) {
            metricsStateHolder.state?.putState("LazyList", "Scrolling")
        } else {
            metricsStateHolder.state?.removeState("LazyList")
        }
    }
}

Untuk mengetahui detail lengkap cara menggunakan JankStats dalam aplikasi Jetpack Compose, lihat aplikasi contoh performa kami.

Berikan masukan

Sampaikan masukan dan ide Anda kepada kami melalui resource berikut:

Issue tracker
Laporkan masalah agar kami dapat memperbaiki bug.