Mengidentifikasi Jank Terkait Jitter

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

Penundaan penjadwal thread aplikasi

Penundaan penjadwal adalah gejala jitter yang paling jelas: Suatu proses yang seharusnya dijalankan dibuat dapat dijalankan tetapi tidak berjalan dalam jangka waktu yang lama. Arti penting dari penundaan ini bervariasi tergantung pada konteksnya. Misalnya:

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

Waktu yang dapat dijalankan dapat diidentifikasi di systrace dengan bilah biru sebelum segmen thread yang sedang berjalan. Waktu yang dapat dijalankan juga dapat ditentukan oleh lamanya waktu antara peristiwa sched_wakeup untuk sebuah thread dan peristiwa sched_switch yang menandakan dimulainya eksekusi thread.

Utas yang berjalan terlalu panjang

Thread UI aplikasi yang dijalankan terlalu lama dapat menyebabkan masalah. Thread tingkat rendah dengan waktu eksekusi yang lama umumnya memiliki penyebab berbeda, namun upaya untuk mendorong waktu eksekusi thread UI ke nol mungkin memerlukan perbaikan beberapa masalah yang sama yang menyebabkan thread tingkat rendah memiliki waktu eksekusi yang lama. Untuk mengurangi penundaan:

  1. Gunakan cpuset seperti yang dijelaskan dalam Pelambatan termal .
  2. Tingkatkan nilai CONFIG_HZ.
    • Secara historis, nilainya telah ditetapkan ke 100 pada platform arm dan arm64. Namun, ini adalah kecelakaan sejarah dan bukan merupakan nilai yang baik untuk digunakan sebagai perangkat interaktif. CONFIG_HZ=100 berarti jiffy memiliki panjang 10 md, yang berarti penyeimbangan beban antar CPU mungkin memerlukan waktu 20 md (dua jiffies). Hal ini secara signifikan dapat menyebabkan jank pada sistem yang dimuat.
    • Perangkat terbaru (Nexus 5X, Nexus 6P, Pixel, dan Pixel XL) telah dikirimkan dengan CONFIG_HZ=300. Hal ini akan menghasilkan biaya listrik yang dapat diabaikan sekaligus meningkatkan waktu pengoperasian secara signifikan. Jika Anda melihat peningkatan signifikan dalam konsumsi daya atau masalah kinerja setelah mengubah CONFIG_HZ, kemungkinan salah satu driver Anda menggunakan pengatur waktu berdasarkan jiffies mentah, bukan milidetik, dan mengubahnya menjadi jiffies. Ini biasanya merupakan perbaikan yang mudah (lihat patch 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 menemukan bahwa ini menawarkan kinerja nyata dan pengurangan daya karena penurunan overhead RCU.

Dengan dua perubahan itu saja, perangkat akan terlihat jauh lebih baik untuk waktu menjalankan thread UI saat dimuat.

Menggunakan sys.use_fifo_ui

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

Peringatan : Jangan gunakan opsi ini pada konfigurasi CPU heterogen kecuali Anda memiliki penjadwal RT yang sadar kapasitas. Dan, pada saat ini, TIDAK ADA PENJADWAL RT PENGIRIMAN SAAT INI YANG SADAR KAPASITAS . Kami sedang mengerjakan satu untuk EAS, namun belum tersedia. Penjadwal RT default hanya didasarkan pada prioritas RT dan apakah CPU sudah memiliki thread RT dengan prioritas yang sama atau lebih tinggi.

Hasilnya, penjadwal RT default akan dengan senang hati memindahkan thread UI Anda yang berjalan relatif lama dari inti besar berfrekuensi tinggi ke inti kecil dengan frekuensi minimum jika thread FIFO dengan prioritas lebih tinggi kebetulan aktif di inti besar yang sama. Hal ini akan menyebabkan 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 thread UI dan RenderThread (dua thread paling penting bagi UI) dari aplikasi teratas dan menjadikan thread tersebut SCHED_FIFO, bukan SCHED_OTHER. Ini secara efektif menghilangkan jitter dari UI dan RenderThreads; jejak yang kami kumpulkan dengan mengaktifkan opsi ini menunjukkan waktu yang dapat dijalankan dalam urutan mikrodetik, bukan milidetik.

Namun, karena penyeimbang beban RT tidak memperhatikan kapasitas, terjadi penurunan kinerja startup aplikasi sebesar 30% karena thread 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 setara dalam operasi massal dan pengurangan 10-15% pada waktu frame 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 dikaitkan langsung dengan CPU 0 yang kewalahan dengan interupsi. Misalnya, jika thread mdss_fb0 dijadwalkan pada CPU 0, ada kemungkinan lebih besar untuk melakukan jank karena interupsi yang dipicu oleh tampilan segera sebelum pemindaian. mdss_fb0 akan berada di tengah-tengah pekerjaannya dengan tenggat waktu yang sangat ketat, dan kemudian akan kehilangan waktu bagi pengendali interupsi MDSS. Awalnya, kami mencoba memperbaikinya dengan mengatur afinitas CPU dari thread mdss_fb0 ke CPU 1-3 untuk menghindari perselisihan dengan interupsi, namun 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 pada CPU yang sama karena berkurangnya pertentangan dari interupsi lainnya.

Hal ini dapat diidentifikasi di systrace dengan melihat bagian sched dan juga bagian irq. Bagian sched menunjukkan apa yang telah dijadwalkan, tetapi wilayah yang tumpang tindih di bagian irq berarti interupsi sedang berjalan selama waktu tersebut dan bukan proses yang dijadwalkan secara normal. Jika Anda melihat banyak waktu yang dibutuhkan selama interupsi, pilihan Anda meliputi:

  • Jadikan pengendali interupsi lebih cepat.
  • Cegah terjadinya interupsi sejak awal.
  • Ubah frekuensi interupsi agar tidak sefase dengan pekerjaan reguler lainnya yang mungkin mengganggu (jika interupsi reguler).
  • Atur afinitas CPU terhadap interupsi secara langsung dan cegah agar tidak seimbang.
  • Atur afinitas CPU dari thread yang diganggu oleh interupsi untuk menghindari interupsi.
  • Andalkan penyeimbang interupsi untuk memindahkan interupsi ke CPU yang bebannya lebih sedikit.

Menyetel afinitas CPU umumnya tidak disarankan tetapi dapat berguna untuk kasus tertentu. Secara umum, terlalu sulit untuk memprediksi keadaan sistem untuk sebagian besar interupsi umum, namun 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 berjalan, ia menonaktifkan preemption. softirqs juga dapat dipicu di banyak tempat di dalam kernel dan dapat dijalankan di dalam proses pengguna. Jika terdapat aktivitas softirq yang cukup, proses pengguna akan berhenti menjalankan softirqs, dan ksoftirqd bangun untuk menjalankan softirqs dan memuat seimbang. Biasanya, ini baik-baik saja. Namun, satu softirq yang sangat panjang dapat merusak sistem.


softirq terlihat dalam bagian irq dari penelusuran, sehingga mudah dikenali jika masalah dapat direproduksi saat penelusuran. 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 menjadi penyebabnya.

Pengemudi membiarkan preemption atau IRQ dinonaktifkan terlalu lama

Menonaktifkan preemption atau interupsi terlalu lama (puluhan milidetik) akan mengakibatkan jank. Biasanya, jank bermanifestasi sebagai thread yang dapat dijalankan tetapi tidak berjalan pada CPU tertentu, meskipun thread yang dapat dijalankan memiliki prioritas yang jauh lebih tinggi (atau SCHED_FIFO) dibandingkan thread lainnya.

Beberapa pedoman:

  • Jika thread yang dapat dijalankan adalah SCHED_FIFO dan thread yang berjalan adalah SCHED_OTHER, thread yang berjalan akan menonaktifkan preemption atau interupsi.
  • Jika thread yang dapat dijalankan memiliki prioritas yang jauh lebih tinggi (100) daripada thread yang sedang berjalan (120), thread yang sedang berjalan kemungkinan besar memiliki preemption atau interupsi yang dinonaktifkan jika thread yang dapat dijalankan tidak berjalan dalam dua jiffies.
  • Jika thread yang dapat dijalankan dan thread yang sedang berjalan memiliki prioritas yang sama, thread yang sedang berjalan kemungkinan besar menonaktifkan preemption atau interupsi jika thread yang dapat dijalankan tidak berjalan dalam waktu 20 md.

Ingatlah bahwa menjalankan pengendali 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 lebih luas mengenai akar penyebab wilayah yang tidak pernah terputus (seperti nama fungsi), namun memerlukan upaya yang lebih invasif untuk mengaktifkannya. Meskipun mungkin memiliki dampak kinerja yang lebih besar, hal ini patut untuk dicoba.

Penggunaan antrian kerja yang salah

Penangan interupsi sering kali perlu melakukan pekerjaan yang dapat dijalankan di luar konteks interupsi, sehingga memungkinkan pekerjaan untuk disalurkan ke thread berbeda di kernel. Pengembang driver mungkin memperhatikan bahwa kernel memiliki fungsionalitas tugas asinkron di 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 berupa SCHED_OTHER. Banyak interupsi perangkat keras berada pada jalur kinerja kritis dan harus segera dijalankan. Antrean kerja tidak memiliki jaminan kapan akan dijalankan. Setiap kali kami melihat antrean kerja di jalur kinerja kritis, hal tersebut menjadi sumber jank sporadis, apa pun perangkatnya. Di Pixel, dengan prosesor andalan, kami melihat bahwa satu antrean kerja dapat tertunda hingga 7 ms 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 thread terpisah harus membuat kthread SCHED_FIFO mereka sendiri. Untuk bantuan melakukan hal ini dengan fungsi kthread_work, lihat patch ini.

Pertentangan kunci kerangka

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

Pertentangan kunci pengikat

Secara historis, pengikat memiliki satu kunci global. Jika thread yang menjalankan transaksi pengikat didahului sambil menahan kunci, tidak ada thread lain yang dapat melakukan transaksi pengikat hingga thread asli melepaskan kuncinya. Ini buruk; pertikaian pengikat dapat memblokir segala sesuatu di sistem, termasuk mengirimkan pembaruan UI ke tampilan (utas UI berkomunikasi dengan SurfaceFlinger melalui pengikat).

Android 6.0 menyertakan beberapa patch untuk memperbaiki perilaku ini dengan menonaktifkan preemption sambil menahan kunci pengikat. Ini aman hanya karena kunci pengikat harus ditahan selama beberapa mikrodetik dari waktu proses sebenarnya. Hal ini secara signifikan meningkatkan kinerja dalam situasi yang tidak terduga dan mencegah pertikaian dengan mencegah sebagian besar peralihan penjadwal saat kunci pengikat ditahan. Namun, preemption tidak dapat dinonaktifkan selama seluruh waktu proses memegang kunci pengikat, artinya preemption diaktifkan untuk fungsi yang dapat tidur (seperti copy_from_user), yang dapat menyebabkan preemption yang sama seperti kasus aslinya. Saat kami mengirimkan patch tersebut ke hulu, mereka segera memberi tahu kami bahwa ini adalah ide terburuk dalam sejarah. (Kami setuju dengan mereka, namun kami juga tidak dapat membantah kemanjuran patch tersebut dalam mencegah jank.)

fd pertentangan dalam suatu proses

Ini jarang terjadi. Kemacetan Anda mungkin bukan disebabkan oleh hal ini.

Meskipun demikian, jika Anda memiliki beberapa thread dalam suatu proses penulisan fd yang sama, pertikaian dapat terlihat pada fd ini, namun satu-satunya saat kami melihat hal ini selama kemunculan Pixel adalah saat pengujian di mana thread berprioritas rendah berusaha menempati semua CPU waktu sementara satu thread prioritas tinggi berjalan dalam proses yang sama. Semua thread menulis ke penanda penelusuran fd dan thread berprioritas tinggi dapat diblokir di penanda penelusuran fd jika thread berprioritas rendah menahan kunci fd dan kemudian didahului. Saat pelacakan dinonaktifkan dari thread prioritas rendah, tidak ada masalah kinerja.

Kami tidak dapat mereproduksi hal ini dalam situasi lain, namun hal ini perlu diperhatikan sebagai penyebab potensial masalah kinerja saat penelusuran.

Transisi idle CPU yang tidak diperlukan

Saat menangani IPC, khususnya pipeline multi-proses, variasi pada perilaku runtime berikut biasanya terlihat:

  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.

Sumber overhead yang umum adalah antara langkah 2 dan 3. Jika CPU 2 menganggur, maka harus dikembalikan ke keadaan aktif sebelum thread B dapat berjalan. Bergantung pada SOC dan seberapa dalam keadaan idle, ini mungkin memerlukan waktu puluhan mikrodetik sebelum thread B mulai berjalan. Jika runtime sebenarnya dari masing-masing sisi IPC cukup dekat dengan overhead, kinerja keseluruhan pipeline tersebut dapat dikurangi secara signifikan karena transisi idle CPU. Tempat paling umum bagi Android untuk melakukan hal 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 mana pun. Perlakukan ini sebagai persyaratan, bukan petunjuk. Binder menggunakan ini saat ini, dan ini sangat membantu dalam transaksi pengikat sinkron untuk menghindari transisi idle 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 tidak akan menghemat daya dengan masuk ke kondisi idle terdalam.

Pencatatan

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

Masalah I/O

Operasi I/O adalah sumber jitter yang umum. Jika thread mengakses file yang dipetakan memori dan halaman tersebut tidak ada dalam cache halaman, maka thread tersebut akan mengalami kesalahan dan membaca halaman tersebut dari disk. Ini memblokir thread (biasanya selama 10+ ms) dan jika ini terjadi di jalur kritis rendering UI, dapat mengakibatkan jank. Ada terlalu banyak penyebab operasi I/O untuk dibahas di sini, namun periksa lokasi berikut ketika mencoba meningkatkan perilaku I/O:

  • Layanan Pinner . Ditambahkan di Android 7.0, PinnerService memungkinkan kerangka kerja untuk mengunci beberapa file di cache halaman. Tindakan ini menghilangkan memori untuk digunakan oleh proses lainnya, namun jika ada beberapa file yang diketahui secara apriori digunakan secara teratur, maka akan efektif untuk mengunci file tersebut.

    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-menerus digunakan oleh sebagian besar aplikasi dan system_server, jadi file-file ini tidak boleh di-page out. Secara khusus, kami menemukan bahwa jika salah satu dari halaman tersebut dikeluarkan, halaman tersebut akan dimasukkan kembali dan menyebabkan jank saat beralih dari aplikasi kelas berat.
  • Enkripsi . Kemungkinan penyebab lain 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 menyebabkan beban CPU tambahan di jalur kritis, yang menambah lebih banyak jitter daripada sekadar pengambilan I/O.

    Mesin enkripsi perangkat keras berbasis DMA memiliki masalah serupa, karena kernel harus menghabiskan banyak siklus untuk mengelola pekerjaan tersebut bahkan jika pekerjaan penting lainnya tersedia untuk dijalankan. Kami sangat menyarankan vendor SOC mana pun yang membuat perangkat keras baru untuk menyertakan dukungan enkripsi inline.

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 hal ini berfungsi dengan baik untuk throughput dan konsumsi daya, hal ini dapat menimbulkan bencana besar terhadap latensi. Ada beberapa thread yang berjalan pendek di jalur kritis rendering UI yang dapat dianggap kecil; jika thread ini tertunda karena dimigrasikan secara perlahan ke CPU lain, hal ini akan menyebabkan jank. Kami merekomendasikan penggunaan pengepakan tugas kecil dengan sangat hati-hati.

Cache halaman dihancurkan

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

Salah satu cara untuk mengidentifikasi hal ini adalah dengan mengambil systrace menggunakan tag pagecache dan memasukkan jejak tersebut 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 tersebut pada disk, Anda pasti mengalami penghancuran cache halaman.

Artinya, rangkaian kerja yang diperlukan oleh beban kerja Anda (biasanya satu aplikasi ditambah server_sistem) lebih besar daripada jumlah memori yang tersedia untuk cache halaman di perangkat Anda. Akibatnya, ketika satu bagian dari beban kerja mendapatkan data yang diperlukan di cache halaman, bagian lain yang akan digunakan dalam waktu dekat akan dikeluarkan dan harus diambil lagi, sehingga menyebabkan masalah terjadi 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 ukiran yang Anda miliki untuk perangkat Anda untuk memastikan Anda tidak menghapus memori dari OS secara tidak perlu. Kami telah melihat situasi di mana ukiran yang digunakan untuk debugging secara tidak sengaja tertinggal dalam konfigurasi kernel pengiriman, sehingga menghabiskan puluhan megabyte memori. Hal ini dapat membuat perbedaan antara menghancurkan cache halaman dan tidak, terutama pada perangkat dengan memori lebih sedikit.
  • Jika Anda melihat cache halaman rusak di system_server pada file penting, pertimbangkan untuk menyematkan file tersebut. Hal ini akan meningkatkan tekanan memori di tempat lain, namun hal ini dapat mengubah perilaku untuk menghindari kerusakan.
  • Sesuaikan kembali lowmemorykiller untuk mencoba menghemat lebih banyak memori. ambang batas lowmemorykiller didasarkan pada memori bebas absolut dan cache halaman, sehingga meningkatkan ambang batas saat proses pada tingkat oom_adj tertentu dihentikan dapat menghasilkan perilaku yang lebih baik dengan mengorbankan peningkatan kematian aplikasi di latar belakang.
  • Coba gunakan ZRAM. Kami menggunakan ZRAM di Pixel, meskipun Pixel memiliki 4GB, karena dapat membantu halaman kotor yang jarang digunakan.