Mengidentifikasi Jank . Terkait Jitter

Jitter adalah perilaku sistem acak yang mencegah pekerjaan yang terlihat berjalan. Halaman ini menjelaskan cara mengidentifikasi dan mengatasi masalah jank terkait jitter.

Penundaan penjadwal utas aplikasi

Penundaan penjadwal adalah gejala jitter yang paling jelas: Proses yang harus dijalankan dibuat dapat dijalankan tetapi tidak berjalan untuk beberapa waktu yang signifikan. Signifikansi penundaan bervariasi sesuai dengan konteksnya. Sebagai contoh:

  • Utas pembantu acak dalam suatu aplikasi mungkin dapat ditunda selama beberapa milidetik tanpa masalah.
  • Utas UI aplikasi mungkin dapat mentolerir jitter 1-2 md.
  • Kthreads driver yang berjalan sebagai SCHED_FIFO dapat menyebabkan masalah jika dapat dijalankan untuk 500us sebelum dijalankan.

Waktu yang dapat dijalankan dapat diidentifikasi di systrace oleh bilah biru yang mendahului segmen utas yang sedang berjalan. Waktu runnable juga dapat ditentukan oleh lamanya waktu antara sched_wakeup event untuk thread dan event sched_switch yang menandakan dimulainya eksekusi thread.

Utas yang berjalan terlalu panjang

Utas UI aplikasi yang dapat dijalankan terlalu lama dapat menyebabkan masalah. Utas tingkat rendah dengan waktu runnable yang lama umumnya memiliki penyebab yang berbeda, tetapi mencoba mendorong waktu runnable utas UI menuju nol mungkin memerlukan perbaikan beberapa masalah yang sama yang menyebabkan utas tingkat rendah memiliki waktu runnable yang lama. Untuk mengurangi penundaan:

  1. Gunakan cpuset seperti yang dijelaskan di Thermal throttling .
  2. Tingkatkan nilai CONFIG_HZ.
    • Secara historis, nilainya telah ditetapkan ke 100 pada platform arm dan arm64. Namun, ini adalah kecelakaan sejarah dan bukan nilai yang baik untuk digunakan untuk perangkat interaktif. CONFIG_HZ=100 berarti jiffy panjangnya 10 ms, yang berarti bahwa penyeimbangan beban antar CPU mungkin memerlukan waktu 20 md (dua jiffies) untuk terjadi. Ini dapat secara signifikan berkontribusi pada jank pada sistem yang dimuat.
    • Perangkat terbaru (Nexus 5X, Nexus 6P, Pixel, dan Pixel XL) telah dikirimkan dengan CONFIG_HZ=300. Ini harus memiliki biaya daya yang dapat diabaikan sambil secara signifikan meningkatkan waktu yang dapat dijalankan. Jika Anda melihat peningkatan yang signifikan dalam konsumsi daya atau masalah kinerja setelah mengubah CONFIG_HZ, kemungkinan salah satu driver Anda menggunakan pengatur waktu berdasarkan jiffie mentah alih-alih milidetik dan mengonversi ke jiffies. Ini biasanya merupakan perbaikan yang mudah (lihat tambalan yang memperbaiki masalah pengatur waktu kgsl pada Nexus 5X dan 6P saat mengonversi ke CONFIG_HZ=300).
    • Terakhir, kami telah bereksperimen dengan CONFIG_HZ=1000 pada Nexus/Pixel dan ternyata menawarkan kinerja yang nyata dan pengurangan daya karena penurunan overhead RCU.

Dengan dua perubahan itu saja, perangkat akan terlihat jauh lebih baik untuk waktu runnable thread UI di bawah beban.

Menggunakan sys.use_fifo_ui

Anda dapat mencoba mengarahkan waktu runnable thread UI ke nol dengan menyetel properti sys.use_fifo_ui ke 1.

Peringatan : Jangan gunakan opsi ini pada konfigurasi CPU yang heterogen kecuali Anda memiliki penjadwal RT yang mengetahui kapasitas. Dan, saat ini, TIDAK ADA PENJADWAL RT PENGIRIMAN SAAT INI ADALAH KAPASITAS-Sadar . Kami sedang mengerjakan satu untuk EAS, tetapi belum tersedia. Penjadwal RT default didasarkan murni pada prioritas RT dan apakah CPU sudah memiliki utas RT dengan prioritas yang sama atau lebih tinggi.

Akibatnya, penjadwal RT default akan dengan senang hati memindahkan utas UI Anda yang berjalan relatif lama dari inti besar berfrekuensi tinggi ke inti kecil pada frekuensi minimum jika kthread FIFO prioritas lebih tinggi kebetulan terbangun di inti besar yang sama. Ini akan memperkenalkan regresi kinerja yang signifikan . Karena opsi ini belum digunakan pada perangkat Android pengiriman, jika Anda ingin menggunakannya, hubungi tim kinerja Android untuk membantu Anda memvalidasinya.

Saat sys.use_fifo_ui diaktifkan, ActivityManager melacak utas UI dan RenderThread (dua utas paling kritis UI) dari aplikasi teratas dan menjadikan utas tersebut SCHED_FIFO alih-alih SCHED_OTHER. Ini secara efektif menghilangkan jitter dari UI dan RenderThreads; jejak yang kami kumpulkan dengan opsi ini diaktifkan menunjukkan waktu yang dapat dijalankan dalam urutan mikrodetik, bukan milidetik.

Namun, karena penyeimbang beban RT tidak mengetahui kapasitas, ada penurunan 30% dalam kinerja startup aplikasi karena utas UI yang bertanggung jawab untuk memulai aplikasi akan dipindahkan dari inti Kryo emas 2,1Ghz ke inti Kryo perak 1,5GHz. . Dengan penyeimbang beban RT yang sadar kapasitas, kami melihat kinerja yang setara dalam operasi massal dan pengurangan 10-15% pada waktu bingkai persentil ke-95 dan ke-99 di banyak tolok ukur UI kami.

Mengganggu lalu lintas

Karena platform ARM mengirimkan interupsi ke CPU 0 hanya secara default, kami merekomendasikan penggunaan penyeimbang IRQ (irqbalance atau msm_irqbalance pada platform Qualcomm).

Selama pengembangan Pixel, kami melihat jank yang dapat secara langsung dikaitkan dengan CPU 0 yang berlebihan dengan interupsi. Misalnya, jika utas mdss_fb0 dijadwalkan pada CPU 0, ada kemungkinan yang jauh lebih besar untuk tersendat karena interupsi yang dipicu oleh tampilan segera sebelum pemindaian. mdss_fb0 akan berada di tengah pekerjaannya sendiri dengan tenggat waktu yang sangat ketat, dan kemudian akan kehilangan beberapa waktu untuk penangan interupsi MDSS. Awalnya, kami mencoba untuk memperbaikinya dengan menyetel afinitas CPU dari utas mdss_fb0 ke CPU 1-3 untuk menghindari perselisihan dengan interupsi, tetapi kemudian kami menyadari bahwa kami belum mengaktifkan msm_irqbalance. Dengan mengaktifkan msm_irqbalance, jank meningkat secara nyata bahkan ketika mdss_fb0 dan interupsi MDSS berada di CPU yang sama karena berkurangnya pertentangan dari interupsi lain.

Ini dapat diidentifikasi di systrace dengan melihat bagian sched serta bagian irq. Bagian terjadwal menunjukkan apa yang telah dijadwalkan, tetapi wilayah yang tumpang tindih di bagian irq berarti interupsi sedang berjalan selama waktu itu alih-alih proses yang dijadwalkan secara normal. Jika Anda melihat sebagian besar waktu yang diambil selama interupsi, opsi Anda meliputi:

  • Buat penangan interupsi lebih cepat.
  • Mencegah interupsi terjadi di tempat pertama.
  • Ubah frekuensi interupsi menjadi tidak sefase dengan pekerjaan reguler lain yang mungkin mengganggu (jika interupsi reguler).
  • Atur afinitas CPU terhadap interupsi secara langsung dan cegah agar tidak seimbang.
  • Atur afinitas CPU dari utas yang diinterupsi untuk menghindari interupsi.
  • Andalkan penyeimbang interupsi untuk memindahkan interupsi ke CPU yang lebih sedikit bebannya.

Menyetel afinitas CPU umumnya tidak disarankan tetapi dapat berguna untuk kasus tertentu. Secara umum, terlalu sulit untuk memprediksi status sistem untuk interupsi yang paling umum, tetapi jika Anda memiliki serangkaian kondisi yang sangat spesifik yang memicu interupsi tertentu di mana sistem lebih dibatasi dari biasanya (seperti VR), afinitas CPU eksplisit mungkin menjadi solusi yang baik.

Softirq panjang

Saat softirq sedang berjalan, itu menonaktifkan preemption. softirqs juga dapat dipicu di banyak tempat di dalam kernel dan dapat berjalan di dalam proses pengguna. Jika ada aktivitas softirq yang cukup, proses pengguna akan berhenti menjalankan softirqs, dan ksoftirqd bangun untuk menjalankan softirqs dan menjadi beban seimbang. Biasanya, ini baik-baik saja. Namun, satu softirq yang sangat panjang dapat mendatangkan malapetaka pada sistem.


softirqs terlihat di dalam bagian irq dari sebuah jejak, sehingga mudah dikenali jika masalah dapat direproduksi saat menelusuri. Karena softirq dapat berjalan dalam proses pengguna, softirq yang buruk juga dapat bermanifestasi sebagai runtime tambahan di dalam proses pengguna tanpa alasan yang jelas. Jika Anda melihatnya, periksa bagian irq untuk melihat apakah softirqs yang harus disalahkan.

Pengemudi meninggalkan preemption atau IRQ dinonaktifkan terlalu lama

Menonaktifkan preemption atau interupsi terlalu lama (puluhan milidetik) menghasilkan jank. Biasanya, jank bermanifestasi sebagai utas yang menjadi dapat dijalankan tetapi tidak berjalan pada CPU tertentu, bahkan jika utas yang dapat dijalankan secara signifikan memiliki prioritas lebih tinggi (atau SCHED_FIFO) daripada utas lainnya.

Beberapa pedoman:

  • Jika utas yang dapat dijalankan adalah SCHED_FIFO dan utas yang sedang berjalan adalah SCHED_OTHER, utas yang sedang berjalan memiliki preemption atau interupsi yang dinonaktifkan.
  • Jika utas yang dapat dijalankan secara signifikan memiliki prioritas lebih tinggi (100) daripada utas yang sedang berjalan (120), utas yang sedang berjalan kemungkinan memiliki preemption atau interupsi yang dinonaktifkan jika utas yang dapat dijalankan tidak berjalan dalam dua sekejap.
  • Jika utas yang dapat dijalankan dan utas yang sedang berjalan memiliki prioritas yang sama, utas yang sedang berjalan kemungkinan memiliki preemption atau interupsi yang dinonaktifkan jika utas yang dapat dijalankan tidak berjalan dalam 20 ms.

Ingatlah bahwa menjalankan penangan interupsi mencegah Anda melayani interupsi lain, yang juga menonaktifkan preemption.


Pilihan lain untuk mengidentifikasi wilayah yang melanggar adalah dengan pelacak preemptirqsoff (lihat Menggunakan ftrace dinamis ). Pelacak ini dapat memberikan wawasan yang jauh lebih luas tentang akar penyebab wilayah yang tidak pernah terputus (seperti nama fungsi), tetapi membutuhkan pekerjaan yang lebih invasif untuk mengaktifkannya. Meskipun mungkin memiliki lebih banyak dampak kinerja, itu pasti patut dicoba.

Penggunaan antrian kerja yang salah

Penangan interupsi sering kali perlu melakukan pekerjaan yang dapat berjalan di luar konteks interupsi, memungkinkan pekerjaan untuk dipindahkan ke utas yang berbeda di kernel. Pengembang driver mungkin memperhatikan kernel memiliki fungsionalitas tugas asinkron seluruh sistem yang sangat nyaman yang disebut antrian kerja dan mungkin menggunakannya untuk pekerjaan yang berhubungan dengan interupsi.

Namun, antrian kerja hampir selalu merupakan jawaban yang salah untuk masalah ini karena selalu SCHED_OTHER. Banyak interupsi perangkat keras berada di jalur kritis kinerja dan harus segera dijalankan. Workqueue tidak memiliki jaminan kapan akan dijalankan. Setiap kali kita melihat antrian kerja di jalur kritis kinerja, itu menjadi sumber jank sporadis, terlepas dari perangkatnya. Di Pixel, dengan prosesor unggulan, kami melihat bahwa satu antrean kerja dapat ditunda hingga 7 mdtk jika perangkat sedang dimuat, bergantung pada perilaku penjadwal dan hal lain yang berjalan di sistem.

Alih-alih antrian kerja, driver yang perlu menangani pekerjaan seperti interupsi di dalam utas terpisah harus membuat kthread SCHED_FIFO mereka sendiri. Untuk bantuan melakukan ini dengan fungsi kthread_work, lihat patch ini.

Perdebatan kunci kerangka kerja

Perselisihan kunci kerangka kerja dapat menjadi sumber jank atau masalah kinerja lainnya. Ini biasanya disebabkan oleh kunci ActivityManagerService tetapi juga dapat dilihat di kunci lain. Misalnya, kunci PowerManagerService dapat memengaruhi kinerja layar. Jika Anda melihat ini di perangkat Anda, tidak ada perbaikan yang baik karena hanya dapat ditingkatkan melalui peningkatan arsitektur pada kerangka kerja. Namun, jika Anda memodifikasi kode yang berjalan di dalam system_server, sangat penting untuk menghindari menahan kunci dalam waktu lama, terutama kunci ActivityManagerService.

Perselisihan kunci pengikat

Secara historis, pengikat memiliki satu kunci global. Jika utas yang menjalankan transaksi pengikat telah didahulukan saat menahan kunci, tidak ada utas lain yang dapat melakukan transaksi pengikat hingga utas asli melepaskan kunci. Ini buruk; perselisihan pengikat dapat memblokir semua yang ada di sistem, termasuk mengirim pembaruan UI ke tampilan (utas UI berkomunikasi dengan SurfaceFlinger melalui pengikat).

Android 6.0 menyertakan beberapa tambalan untuk meningkatkan perilaku ini dengan menonaktifkan preemption sambil menahan kunci pengikat. Ini aman hanya karena kunci pengikat harus ditahan selama beberapa mikrodetik dari waktu proses yang sebenarnya. Ini secara dramatis meningkatkan kinerja dalam situasi yang tidak diinginkan dan mencegah pertengkaran dengan mencegah sebagian besar sakelar penjadwal saat kunci pengikat ditahan. Namun, preemption tidak dapat dinonaktifkan untuk seluruh waktu proses menahan kunci binder, yang berarti bahwa preemption diaktifkan untuk fungsi yang dapat tidur (seperti copy_from_user), yang dapat menyebabkan preemption yang sama seperti kasus aslinya. Ketika kami mengirim patch ke hulu, mereka segera memberi tahu kami bahwa ini adalah ide terburuk dalam sejarah. (Kami setuju dengan mereka, tetapi kami juga tidak dapat berdebat dengan kemanjuran tambalan untuk mencegah jank.)

pertengkaran fd dalam suatu proses

Ini jarang terjadi. Jank Anda mungkin tidak disebabkan oleh ini.

Yang mengatakan, jika Anda memiliki beberapa utas dalam suatu proses yang menulis fd yang sama, dimungkinkan untuk melihat pertentangan pada fd ini, namun satu-satunya saat kami melihat ini selama peluncuran Pixel adalah selama pengujian di mana utas berprioritas rendah berusaha untuk menempati semua CPU waktu sementara satu utas prioritas tinggi berjalan dalam proses yang sama. Semua utas menulis ke penanda jejak fd dan utas berprioritas tinggi dapat diblokir pada penanda jejak fd jika utas berprioritas rendah memegang kunci fd dan kemudian didahulukan. Saat pelacakan dinonaktifkan dari utas prioritas rendah, tidak ada masalah kinerja.

Kami tidak dapat mereproduksi ini dalam situasi lain apa pun, tetapi perlu ditunjukkan sebagai penyebab potensial masalah kinerja saat melacak.

Transisi idle CPU yang tidak perlu

Saat berurusan dengan IPC, terutama jalur pipa multi-proses, biasanya terlihat variasi pada perilaku runtime berikut:

  1. Thread A berjalan pada CPU 1.
  2. Thread A membangunkan thread B.
  3. Thread B mulai berjalan pada CPU 2.
  4. Thread A segera tertidur, untuk dibangunkan oleh thread B ketika thread B telah menyelesaikan pekerjaannya saat ini.

Sebuah sumber umum dari overhead adalah antara langkah 2 dan 3. Jika CPU 2 menganggur, itu harus dibawa kembali ke status aktif sebelum utas B dapat berjalan. Bergantung pada SOC dan seberapa dalam idle, ini bisa menjadi puluhan mikrodetik sebelum utas B mulai berjalan. Jika runtime sebenarnya dari setiap sisi IPC cukup dekat dengan overhead, kinerja keseluruhan dari pipeline tersebut dapat dikurangi secara signifikan oleh transisi idle CPU. Tempat paling umum bagi Android untuk melakukan ini adalah di sekitar transaksi pengikat, dan banyak layanan yang menggunakan pengikat akhirnya tampak seperti situasi yang dijelaskan di atas.

Pertama, gunakan fungsi wake_up_interruptible_sync() di driver kernel Anda dan dukung ini dari penjadwal khusus apa pun. Perlakukan ini sebagai persyaratan, bukan petunjuk. Binder menggunakan ini hari ini, dan ini sangat membantu dengan transaksi pengikat sinkron menghindari transisi CPU yang tidak perlu.

Kedua, pastikan waktu transisi cpuidle Anda realistis dan gubernur cpuidle memperhitungkannya dengan benar. Jika SOC Anda masuk dan keluar dari kondisi idle terdalam Anda, Anda tidak akan menghemat daya dengan masuk ke idle terdalam.

Pencatatan

Logging tidak gratis untuk siklus CPU atau memori, jadi jangan spam buffer log. Mencatat siklus biaya di aplikasi Anda (secara langsung) dan di daemon log. Hapus semua log debug sebelum mengirim perangkat Anda.

Masalah I/O

Operasi I/O adalah sumber umum dari jitter. Jika utas mengakses file yang dipetakan memori dan halaman tidak ada di cache halaman, itu akan membuat kesalahan dan membaca halaman dari disk. Ini memblokir utas (biasanya selama 10+ md) dan jika itu terjadi di jalur kritis rendering UI, dapat mengakibatkan jank. Ada terlalu banyak penyebab operasi I/O untuk dibahas di sini, tetapi periksa lokasi berikut saat mencoba meningkatkan perilaku I/O:

  • Layanan Pinner . Ditambahkan di Android 7.0, PinnerService memungkinkan kerangka kerja untuk mengunci beberapa file di cache halaman. Ini menghapus memori untuk digunakan oleh proses lain, tetapi jika ada beberapa file yang diketahui apriori digunakan secara teratur, itu bisa efektif untuk mengunci file-file itu.

    Pada perangkat Pixel dan Nexus 6P yang menjalankan Android 7.0, kami mengunci empat file:
    • /system/framework/arm64/boot-framework.oat
    • /system/framework/oat/arm64/services.odex
    • /system/framework/arm64/boot.oat
    • /system/framework/arm64/boot-core-libart.oat
    File-file ini terus digunakan oleh sebagian besar aplikasi dan system_server, jadi mereka tidak boleh dihapus. Secara khusus, kami telah menemukan bahwa jika ada yang di-page out, mereka akan di-page kembali dan menyebabkan jank saat beralih dari aplikasi kelas berat.
  • Enkripsi . Kemungkinan penyebab lain dari masalah I/O. Kami menemukan enkripsi inline menawarkan kinerja terbaik jika dibandingkan dengan enkripsi berbasis CPU atau menggunakan blok perangkat keras yang dapat diakses melalui DMA. Yang terpenting, enkripsi inline mengurangi jitter yang terkait dengan I/O, terutama jika dibandingkan dengan enkripsi berbasis CPU. Karena pengambilan ke cache halaman sering kali berada di jalur kritis rendering UI, enkripsi berbasis CPU memperkenalkan beban CPU tambahan di jalur kritis, yang menambahkan lebih banyak jitter daripada hanya pengambilan I/O.

    Mesin enkripsi perangkat keras berbasis DMA memiliki masalah yang sama, karena kernel harus menghabiskan siklus mengelola pekerjaan itu bahkan jika pekerjaan penting lainnya tersedia untuk dijalankan. Kami sangat menyarankan vendor SOC mana pun yang membangun perangkat keras baru untuk menyertakan dukungan untuk enkripsi sebaris.

Pengepakan tugas kecil yang agresif

Beberapa penjadwal menawarkan dukungan untuk mengemas tugas-tugas kecil ke dalam satu inti CPU untuk mencoba mengurangi konsumsi daya dengan membuat lebih banyak CPU menganggur lebih lama. Meskipun ini bekerja dengan baik untuk throughput dan konsumsi daya, ini bisa menjadi bencana besar bagi latensi. Ada beberapa utas yang berjalan singkat di jalur kritis rendering UI yang dapat dianggap kecil; jika utas ini tertunda karena dimigrasikan secara perlahan ke CPU lain, itu akan menyebabkan jank. Kami merekomendasikan menggunakan pengepakan tugas kecil dengan sangat konservatif.

Penghancuran cache halaman

Perangkat tanpa memori bebas yang cukup dapat tiba-tiba menjadi sangat lamban saat melakukan operasi yang berjalan lama, seperti membuka aplikasi baru. Jejak aplikasi dapat mengungkapkan bahwa itu secara konsisten diblokir di I/O selama menjalankan tertentu bahkan ketika sering tidak diblokir di I/O. Ini biasanya merupakan tanda kerusakan cache halaman, terutama pada perangkat dengan memori lebih sedikit.

Salah satu cara untuk mengidentifikasi ini adalah dengan mengambil systrace menggunakan tag pagecache dan umpan yang melacak ke skrip di system/extras/pagecache/pagecache.py . pagecache.py menerjemahkan permintaan individual untuk memetakan file ke dalam cache halaman menjadi statistik agregat per file. Jika Anda menemukan bahwa lebih banyak byte file yang telah dibaca daripada ukuran total file itu di disk, Anda pasti memukul halaman cache yang meronta-ronta.

Artinya, set kerja yang diperlukan oleh beban kerja Anda (biasanya satu aplikasi plus system_server) lebih besar daripada jumlah memori yang tersedia untuk cache halaman di perangkat Anda. Akibatnya, karena satu bagian dari beban kerja mendapatkan data yang dibutuhkan di cache halaman, bagian lain yang akan digunakan dalam waktu dekat akan digusur dan harus diambil kembali, sehingga menyebabkan masalah muncul lagi hingga pemuatan telah selesai. Ini adalah penyebab mendasar dari masalah kinerja ketika tidak tersedia cukup memori pada perangkat.

Tidak ada cara yang sangat mudah untuk memperbaiki kerusakan cache halaman, tetapi ada beberapa cara untuk mencoba memperbaikinya pada perangkat tertentu.

  • Gunakan lebih sedikit memori dalam proses yang persisten. Semakin sedikit memori yang digunakan oleh proses persisten, semakin banyak memori yang tersedia untuk aplikasi dan cache halaman.
  • Audit carveouts yang Anda miliki untuk perangkat Anda untuk memastikan Anda tidak perlu menghapus memori dari OS. Kami telah melihat situasi di mana carveout yang digunakan untuk debugging secara tidak sengaja tertinggal dalam konfigurasi kernel pengiriman, menghabiskan puluhan megabita memori. Ini dapat membuat perbedaan antara memukul halaman cache yang meronta-ronta dan tidak, terutama pada perangkat dengan memori lebih sedikit.
  • Jika Anda melihat cache halaman meronta-ronta di system_server pada file penting, pertimbangkan untuk menyematkan file tersebut. Ini akan meningkatkan tekanan memori di tempat lain, tetapi mungkin cukup mengubah perilaku untuk menghindari meronta-ronta.
  • Tune ulang lowmemorykiller untuk mencoba menjaga lebih banyak memori bebas. ambang batas lowmemorykiller didasarkan pada memori bebas absolut dan cache halaman, jadi meningkatkan ambang batas di mana proses pada tingkat oom_adj tertentu dimatikan dapat menghasilkan perilaku yang lebih baik dengan mengorbankan peningkatan kematian aplikasi latar belakang.
  • Coba gunakan ZRAM. Kami menggunakan ZRAM di Pixel, meskipun Pixel memiliki 4GB, karena dapat membantu halaman kotor yang jarang digunakan.