Mengidentifikasi jank terkait jitter

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

Penundaan penjadwal thread aplikasi

Penundaan penjadwal adalah gejala jitter yang paling jelas: Proses yang seharusnya dijalankan dibuat dapat dijalankan, tetapi tidak berjalan selama beberapa waktu yang signifikan. Signifikansi penundaan bervariasi sesuai dengan konteksnya. Contoh:

  • Thread helper acak di aplikasi mungkin dapat tertunda selama beberapa milidetik tanpa masalah.
  • UI thread aplikasi mungkin dapat mentoleransi jitter 1-2 md.
  • Kthread driver yang berjalan sebagai SCHED_FIFO dapat menyebabkan masalah jika dapat dijalankan selama 500 md sebelum berjalan.

Waktu yang dapat dijalankan dapat diidentifikasi di systrace dengan batang biru yang mendahului segmen thread yang sedang berjalan. Waktu yang dapat dijalankan juga dapat ditentukan oleh durasi waktu antara peristiwa sched_wakeup untuk thread dan peristiwa sched_switch yang menandakan awal eksekusi thread.

Thread yang berjalan terlalu lama

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

  1. Gunakan cpuset seperti yang dijelaskan dalam Throttle termal.
  2. Tingkatkan nilai CONFIG_HZ.
    • Secara historis, nilai telah ditetapkan ke 100 di platform arm dan arm64. Namun, ini adalah kecelakaan sejarah dan bukan nilai yang baik untuk digunakan pada perangkat interaktif. CONFIG_HZ=100 berarti jiffy berdurasi 10 md, yang berarti bahwa load balancing antara CPU mungkin memerlukan waktu 20 md (dua jiffies) untuk terjadi. Hal ini dapat berkontribusi secara signifikan pada jank pada sistem yang dimuat.
    • Perangkat terbaru (Nexus 5X, Nexus 6P, Pixel, dan Pixel XL) telah dikirim dengan CONFIG_HZ=300. Hal ini seharusnya memiliki biaya daya yang dapat diabaikan sekaligus meningkatkan waktu yang dapat dijalankan secara signifikan. Jika Anda melihat peningkatan konsumsi daya atau masalah performa yang signifikan setelah mengubah CONFIG_HZ, kemungkinan salah satu driver Anda menggunakan timer berdasarkan jiffies mentah, bukan milidetik, dan mengonversinya menjadi jiffies. Masalah ini biasanya mudah diperbaiki (lihat patch yang memperbaiki masalah timer kgsl di Nexus 5X dan 6P saat mengonversi ke CONFIG_HZ=300).
    • Terakhir, kami telah bereksperimen dengan CONFIG_HZ=1000 di Nexus/Pixel dan menemukan bahwa hal ini menawarkan performa dan pengurangan daya yang signifikan karena overhead RCU yang menurun.

Dengan dua perubahan tersebut saja, perangkat akan terlihat jauh lebih baik untuk waktu yang dapat dijalankan thread UI saat dimuat.

Menggunakan sys.use_fifo_ui

Anda dapat mencoba mendorong waktu thread UI yang dapat dijalankan ke nol dengan menetapkan properti sys.use_fifo_ui ke 1.

Peringatan: Jangan gunakan opsi ini pada konfigurasi CPU heterogen kecuali jika Anda memiliki penjadwal RT yang mempertimbangkan kapasitas. Dan, saat ini, TIDAK ADA PENJADWAL RT YANG SEDANG DIKIRIM DAN DIDEKATI KAPASITAS. Kami sedang mengerjakannya untuk EAS, tetapi belum tersedia. Penjadwal RT default didasarkan sepenuhnya pada prioritas RT dan apakah CPU sudah memiliki thread RT dengan prioritas yang sama atau lebih tinggi.

Akibatnya, penjadwal RT default akan dengan senang hati memindahkan thread UI yang berjalan relatif lama dari big core frekuensi tinggi ke sedikit core pada frekuensi minimum jika kthread FIFO prioritas lebih tinggi kebetulan aktif di big core yang sama. Hal ini akan menyebabkan regresi performa yang signifikan. Karena opsi ini belum digunakan di perangkat Android yang dikirim, jika Anda ingin menggunakannya, hubungi tim performa Android untuk membantu memvalidasinya.

Saat sys.use_fifo_ui diaktifkan, ActivityManager melacak thread UI dan RenderThread (dua thread yang paling penting untuk UI) dari aplikasi teratas dan membuat thread tersebut menjadi SCHED_FIFO, bukan SCHED_OTHER. Tindakan ini secara efektif menghilangkan jitter dari UI dan RenderThreads; rekaman aktivitas yang telah kami kumpulkan dengan opsi ini diaktifkan menunjukkan waktu yang dapat dijalankan dalam urutan mikrodetik, bukan milidetik.

Namun, karena load balancer RT tidak mengetahui kapasitas, terjadi pengurangan performa startup aplikasi sebesar 30% karena thread UI yang bertanggung jawab untuk memulai aplikasi akan dipindahkan dari core Kryo emas 2,1 GHz ke core Kryo perak 1,5 GHz. Dengan load balancer RT yang mempertimbangkan kapasitas, kami melihat performa yang setara dalam operasi massal dan pengurangan waktu render persentil ke-95 dan ke-99 sebesar 10-15% di banyak benchmark UI kami.

Mengganggu traffic

Karena platform ARM hanya mengirimkan interupsi ke CPU 0 secara default, sebaiknya gunakan pengimbang IRQ (irqbalance atau msm_irqbalance di platform Qualcomm).

Selama pengembangan Pixel, kami melihat jank yang dapat langsung diatribusikan ke CPU 0 yang kelebihan beban dengan interupsi. Misalnya, jika thread mdss_fb0 dijadwalkan di CPU 0, ada kemungkinan yang jauh lebih besar untuk mengalami jank karena interupsi yang dipicu oleh layar hampir segera sebelum pemindaian. mdss_fb0 akan berada di tengah-tengah pekerjaannya sendiri dengan batas waktu yang sangat ketat, lalu akan kehilangan beberapa waktu untuk pengendali gangguan MDSS. Awalnya, kami mencoba memperbaikinya dengan menetapkan afinitas CPU thread mdss_fb0 ke CPU 1-3 untuk menghindari pertentangan dengan interupsi, tetapi kemudian kami menyadari bahwa kami belum mengaktifkan msm_irqbalance. Dengan msm_irqbalance diaktifkan, jank terlihat meningkat meskipun mdss_fb0 dan gangguan MDSS berada di CPU yang sama karena berkurangnya pertentangan dari gangguan lain.

Hal ini dapat diidentifikasi di systrace dengan melihat bagian sched serta bagian irq. Bagian sched menunjukkan apa yang telah dijadwalkan, tetapi wilayah yang tumpang-tindih di bagian irq berarti interupsi berjalan selama waktu tersebut, bukan proses yang dijadwalkan secara normal. Jika Anda melihat bagian waktu yang signifikan dihabiskan selama gangguan, opsi Anda mencakup:

  • Membuat pengendali interupsi lebih cepat.
  • Mencegah gangguan agar tidak terjadi.
  • Ubah frekuensi interupsi agar tidak sefase dengan pekerjaan reguler lainnya yang mungkin mengganggu (jika ini adalah interupsi reguler).
  • Tetapkan afinitas CPU interupsi secara langsung dan cegah agar tidak diimbangi.
  • Tetapkan afinitas CPU thread yang terganggu oleh interupsi untuk menghindari interupsi.
  • Mengandalkan pengimbang interupsi untuk memindahkan interupsi ke CPU yang lebih sedikit dimuat.

Menetapkan afinitas CPU umumnya tidak direkomendasikan, tetapi dapat berguna untuk kasus tertentu. Secara umum, sulit untuk memprediksi status sistem untuk gangguan yang paling umum, tetapi jika Anda memiliki kumpulan kondisi yang sangat spesifik yang memicu gangguan tertentu saat sistem lebih dibatasi daripada normal (seperti VR), afinitas CPU eksplisit mungkin merupakan solusi yang baik.

Softirq yang panjang

Saat berjalan, softirq akan menonaktifkan preemptif. softirq juga dapat dipicu di banyak tempat dalam kernel dan dapat berjalan di dalam proses pengguna. Jika ada cukup aktivitas softirq, proses pengguna akan berhenti menjalankan softirq, dan ksoftirqd akan aktif untuk menjalankan softirq dan melakukan load balancing. Biasanya, hal ini tidak masalah. Namun, satu softirq yang sangat panjang dapat merusak sistem.


softirq terlihat dalam bagian irq dari rekaman aktivitas, sehingga mudah ditemukan jika masalah dapat direproduksi saat merekam aktivitas. Karena softirq dapat berjalan dalam proses pengguna, softirq yang buruk juga dapat muncul sebagai runtime tambahan di dalam proses pengguna tanpa alasan yang jelas. Jika Anda melihatnya, periksa bagian irq untuk melihat apakah softirq yang menjadi penyebabnya.

Driver membiarkan preemption atau IRQ dinonaktifkan terlalu lama

Menonaktifkan preempt atau gangguan terlalu lama (puluhan milidetik) akan menyebabkan jank. Biasanya, jank muncul sebagai thread yang dapat dijalankan, tetapi tidak berjalan di CPU tertentu, meskipun thread yang dapat dijalankan memiliki prioritas yang jauh lebih tinggi (atau SCHED_FIFO) daripada thread lainnya.

Beberapa panduan:

  • Jika thread yang dapat dijalankan adalah SCHED_FIFO dan thread yang berjalan adalah SCHED_OTHER, thread yang berjalan telah menonaktifkan preemptif 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 telah menonaktifkan preemption atau interupsi 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 telah menonaktifkan preemptif atau interupsi jika thread yang dapat dijalankan tidak berjalan dalam waktu 20 md.

Perlu diingat bahwa menjalankan pengendali interupsi akan mencegah Anda melayani interupsi lain, yang juga menonaktifkan preemptif.


Opsi lain untuk mengidentifikasi region yang melanggar adalah dengan pelacak preemptirqsoff (lihat Menggunakan ftrace dinamis). Pelacak ini dapat memberikan insight yang jauh lebih besar tentang penyebab utama region yang tidak dapat diganggu (seperti nama fungsi), tetapi memerlukan lebih banyak tindakan invasif untuk mengaktifkannya. Meskipun mungkin memiliki lebih banyak dampak performa, hal ini pasti patut dicoba.

Penggunaan antrean tugas yang salah

Pengendali interupsi sering kali perlu melakukan pekerjaan yang dapat berjalan di luar konteks interupsi, sehingga pekerjaan dapat didistribusikan ke thread yang berbeda di kernel. Developer driver mungkin melihat bahwa kernel memiliki fungsi tugas asinkron seluruh sistem yang sangat praktis yang disebut workqueues dan mungkin menggunakannya untuk pekerjaan terkait interupsi.

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

Sebagai ganti workqueue, driver yang perlu menangani pekerjaan seperti interupsi di dalam thread terpisah harus membuat kthread SCHED_FIFO-nya sendiri. Untuk bantuan melakukannya dengan fungsi kthread_work, lihat patch ini.

Pertentangan kunci framework

Persaingan kunci framework dapat menjadi sumber jank atau masalah performa lainnya. Hal ini biasanya disebabkan oleh kunci ActivityManagerService, tetapi juga dapat dilihat di kunci lain. Misalnya, kunci PowerManagerService dapat memengaruhi performa layar. Jika Anda melihatnya di perangkat, tidak ada perbaikan yang baik karena hanya dapat ditingkatkan melalui peningkatan arsitektur pada framework. Namun, jika Anda mengubah kode yang berjalan di dalam system_server, Anda harus menghindari menahan kunci dalam waktu lama, terutama kunci ActivityManagerService.

Pertentangan kunci binder

Secara historis, binder memiliki satu kunci global. Jika thread yang menjalankan transaksi binder didahului saat memegang kunci, tidak ada thread lain yang dapat melakukan transaksi binder hingga thread asli melepaskan kunci. Hal ini buruk; pertentangan binder dapat memblokir semua hal dalam sistem, termasuk mengirim update UI ke layar (thread UI berkomunikasi dengan SurfaceFlinger melalui binder).

Android 6.0 menyertakan beberapa patch untuk meningkatkan perilaku ini dengan menonaktifkan preemption saat menahan kunci binder. Hal ini aman hanya karena kunci binder harus ditahan selama beberapa mikrodetik runtime sebenarnya. Hal ini secara drastis meningkatkan performa dalam situasi yang tidak diperebutkan dan mencegah pertentangan dengan mencegah sebagian besar pengalihan penjadwal saat kunci binder ditahan. Namun, preemption tidak dapat dinonaktifkan untuk seluruh runtime yang 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. Saat kami mengirimkan patch ke upstream, mereka langsung memberi tahu kami bahwa ini adalah ide terburuk dalam sejarah. (Kami setuju dengan mereka, tetapi kami juga tidak dapat membantah kemanjuran patch untuk mencegah jank.)

Persaingan fd dalam proses

Hal ini jarang terjadi. Jank Anda mungkin tidak disebabkan oleh hal ini.

Meskipun demikian, jika Anda memiliki beberapa thread dalam proses yang menulis fd yang sama, Anda mungkin melihat pertentangan pada fd ini, tetapi satu-satunya waktu kita melihatnya selama pengaktifan Pixel adalah selama pengujian saat thread prioritas rendah mencoba mengisi semua waktu CPU saat satu thread prioritas tinggi berjalan dalam proses yang sama. Semua thread menulis ke fd penanda rekaman aktivitas dan thread prioritas tinggi dapat diblokir di fd penanda rekaman aktivitas jika thread prioritas rendah memegang kunci fd, lalu didahului. Saat pelacakan dinonaktifkan dari thread prioritas rendah, tidak ada masalah performa.

Kami tidak dapat mereproduksinya dalam situasi lain, tetapi hal ini perlu dinyatakan sebagai potensi penyebab masalah performa saat pelacakan.

Transisi CPU tidak ada aktivitas yang tidak diperlukan

Saat menangani IPC, terutama pipeline multiproses, Anda akan sering melihat variasi pada perilaku runtime berikut:

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

Sumber overhead yang umum adalah antara langkah 2 dan 3. Jika CPU 2 tidak ada aktivitas, CPU tersebut harus dikembalikan ke status aktif sebelum thread B dapat berjalan. Bergantung pada SOC dan seberapa dalam waktu tunggu, ini bisa jadi puluhan mikrodetik sebelum thread B mulai berjalan. Jika runtime sebenarnya dari setiap sisi IPC cukup dekat dengan overhead, performa keseluruhan pipeline tersebut dapat dikurangi secara signifikan oleh transisi CPU yang tidak ada aktivitas. Tempat paling umum bagi Android untuk mencapai hal ini adalah di sekitar transaksi binder, dan banyak layanan yang menggunakan binder akhirnya terlihat seperti situasi yang dijelaskan di atas.

Pertama, gunakan fungsi wake_up_interruptible_sync() di driver kernel Anda dan dukung ini dari penjadwal kustom. Perlakukan ini sebagai persyaratan, bukan petunjuk. Binder menggunakannya saat ini, dan hal ini sangat membantu transaksi binder sinkron untuk menghindari transisi CPU yang tidak diperlukan.

Kedua, pastikan waktu transisi cpuidle Anda realistis dan pengontrol cpuidle memperhitungkannya dengan benar. Jika SOC Anda mengalami thrashing masuk dan keluar dari status tidak ada aktivitas terdalam, Anda tidak akan menghemat daya dengan beralih ke tidak ada aktivitas terdalam.

Logging

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

Masalah I/O

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

  • PinnerService. Ditambahkan di Android 7.0, PinnerService memungkinkan framework mengunci beberapa file di cache halaman. Tindakan ini akan menghapus memori untuk digunakan oleh proses lain, tetapi jika ada beberapa file yang diketahui secara apriori digunakan secara rutin, tindakan mlock pada file tersebut dapat efektif.

    Di 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 ini terus digunakan oleh sebagian besar aplikasi dan system_server, sehingga file ini tidak boleh di-paging. Secara khusus, kami mendapati bahwa jika salah satu dari hal tersebut di-page out, hal tersebut akan di-page kembali dan menyebabkan jank saat beralih dari aplikasi berat.
  • Enkripsi. Kemungkinan penyebab lain masalah I/O. Kami mendapati bahwa enkripsi inline menawarkan performa terbaik jika dibandingkan dengan enkripsi berbasis CPU atau menggunakan blok hardware yang dapat diakses melalui DMA. Yang paling penting, 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 akan menyebabkan beban CPU tambahan di jalur kritis, yang menambahkan lebih banyak jitter daripada pengambilan I/O.

    Mesin enkripsi hardware berbasis DMA memiliki masalah serupa, karena kernel harus menghabiskan siklus untuk mengelola pekerjaan tersebut meskipun pekerjaan penting lainnya tersedia untuk dijalankan. Sebaiknya vendor SOC yang membuat hardware baru menyertakan dukungan untuk enkripsi inline.

Pengemasan tugas kecil yang agresif

Beberapa penjadwal menawarkan dukungan untuk memaketkan tugas kecil ke satu core CPU untuk mencoba mengurangi konsumsi daya dengan membuat lebih banyak CPU tidak ada aktivitas lebih lama. Meskipun berfungsi baik untuk throughput dan konsumsi daya, hal ini dapat berdampak buruk pada latensi. Ada beberapa thread yang berjalan singkat 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. Sebaiknya gunakan pengepakan tugas kecil secara sangat konservatif.

Thrashing cache halaman

Perangkat tanpa memori bebas yang cukup mungkin tiba-tiba menjadi sangat lambat saat melakukan operasi yang berjalan lama, seperti membuka aplikasi baru. Rekaman aktivitas aplikasi dapat menunjukkan bahwa aplikasi tersebut secara konsisten diblokir di I/O selama eksekusi tertentu meskipun sering kali tidak diblokir di I/O. Hal ini biasanya merupakan tanda thrashing cache halaman, terutama pada perangkat dengan lebih sedikit memori.

Salah satu cara untuk mengidentifikasinya adalah dengan mengambil systrace menggunakan tag pagecache dan memberi feed rekaman aktivitas tersebut ke skrip di system/extras/pagecache/pagecache.py. pagecache.py menerjemahkan setiap permintaan untuk memetakan file ke dalam cache halaman menjadi statistik per file agregat. Jika Anda mendapati bahwa lebih banyak byte file telah dibaca daripada total ukuran file tersebut di disk, Anda pasti mengalami thrashing cache halaman.

Artinya, set kerja yang diperlukan oleh beban kerja Anda (biasanya satu aplikasi ditambah system_server) lebih besar dari jumlah memori yang tersedia untuk cache halaman di perangkat Anda. Akibatnya, saat satu bagian dari beban kerja mendapatkan data yang diperlukan di cache halaman, bagian lain yang akan digunakan dalam waktu dekat akan dihapus dan harus diambil lagi, sehingga masalah akan terjadi lagi hingga pemuatan selesai. Hal ini adalah penyebab mendasar masalah performa saat memori yang tersedia di perangkat tidak memadai.

Tidak ada cara yang pasti untuk memperbaiki thrashing cache halaman, tetapi ada beberapa cara untuk mencoba memperbaikinya di perangkat tertentu.

  • Menggunakan lebih sedikit memori dalam proses persisten. Makin sedikit memori yang digunakan oleh proses persisten, makin banyak memori yang tersedia untuk aplikasi dan cache halaman.
  • Audit pemisahan yang Anda miliki untuk perangkat guna memastikan Anda tidak menghapus memori dari OS secara tidak perlu. Kami telah melihat situasi saat carveout yang digunakan untuk proses debug secara tidak sengaja tertinggal dalam konfigurasi kernel pengiriman, yang menghabiskan puluhan megabyte memori. Hal ini dapat membuat perbedaan antara mengalami thrashing cache halaman dan tidak, terutama pada perangkat dengan memori yang lebih sedikit.
  • Jika Anda melihat cache halaman yang mengalami thrashing di system_server pada file penting, pertimbangkan untuk menyematkan file tersebut. Hal ini akan meningkatkan tekanan memori di tempat lain, tetapi dapat memodifikasi perilaku cukup untuk menghindari thrashing.
  • Sesuaikan ulang lowmemorykiller untuk mencoba mengosongkan lebih banyak memori. Batas lowmemorykiller didasarkan pada memori bebas absolut dan cache halaman, sehingga meningkatkan batas saat proses di tingkat oom_adj tertentu dihentikan dapat menghasilkan perilaku yang lebih baik dengan mengorbankan peningkatan penghentian aplikasi latar belakang.
  • Coba gunakan ZRAM. Kami menggunakan ZRAM di Pixel, meskipun Pixel memiliki 4 GB, karena dapat membantu halaman kotor yang jarang digunakan.