識別與抖動相關的 Jank

抖動是阻止可感知工作運行的隨機系統行為。本頁介紹如何識別和解決與抖動相關的卡頓問題。

應用程序線程調度程序延遲

調度程序延遲是抖動最明顯的症狀:應該運行的進程變為可運行但在相當長的時間內沒有運行。延遲的重要性因上下文而異。例如:

  • 應用程序中的隨機助手線程可能會延遲數毫秒而不會出現問題。
  • 應用程序的 UI 線程可能能夠容忍 1-2ms 的抖動。
  • 以 SCHED_FIFO 運行的驅動程序 kthread 如果在運行前可運行 500us,則可能會導致問題。

可運行時間可以在 systrace 中通過線程運行段前面的藍條來標識。可運行時間也可以由線程的sched_switch sched_wakeup之間的時間長度來確定。

運行時間過長的線程

運行時間過長的應用程序 UI 線程可能會導致問題。具有較長可運行時間的低級線程通常有不同的原因,但嘗試將 UI 線程可運行時間推向零可能需要修復一些導致低級線程具有較長可運行時間的相同問題。為了減少延遲:

  1. 使用熱節流中所述的 cpuset。
  2. 增加 CONFIG_HZ 值。
    • 從歷史上看,該值在 arm 和 arm64 平台上已設置為 100。然而,這是歷史的偶然,對於交互式設備來說並不是一個很好的價值。 CONFIG_HZ=100 表示一個 jiffy 為 10ms 長,這意味著 CPU 之間的負載均衡可能需要 20ms(兩個 jiffy)發生。這可能會顯著導致加載系統上的卡頓。
    • 最近的設備(Nexus 5X、Nexus 6P、Pixel 和 Pixel XL)出廠時 CONFIG_HZ=300。這應該具有可以忽略不計的電力成本,同時顯著提高可運行時間。如果您在更改 CONFIG_HZ 後確實看到功耗或性能問題顯著增加,則很可能您的驅動程序之一正在使用基於原始 jiffies 而不是毫秒的計時器並轉換為 jiffies。這通常是一個簡單的修復(請參閱在轉換為 CONFIG_HZ=300 時修復 Nexus 5X 和 6P 上的 kgsl 計時器問題的補丁)。
    • 最後,我們在 Nexus/Pixel 上對 CONFIG_HZ=1000 進行了實驗,發現由於 RCU 開銷減少,它提供了顯著的性能和功耗降低。

僅通過這兩個更改,設備在負載下的 UI 線程可運行時間應該看起來更好。

使用 sys.use_fifo_ui

您可以通過將sys.use_fifo_ui屬性設置為 1 來嘗試將 UI 線程可運行時間驅動為零。

警告:除非您有容量感知 RT 調度程序,否則請勿在異構 CPU 配置上使用此選項。並且,目前,沒有當前正在發貨的 RT 調度程序是容量感知的。我們正在為 EAS 開發一款,但尚未推出。默認的 RT 調度程序純粹基於 RT 優先級以及 CPU 是否已經具有相同或更高優先級的 RT 線程。

因此,如果更高優先級的 FIFO kthread 恰好在同一個大核心上喚醒,默認 RT 調度程序會很高興地將您運行相對較長的 UI 線程從高頻大核心移動到最低頻率的小核心。這將引入顯著的性能倒退。由於此選項尚未在出貨的 Android 設備上使用,如果您想使用它,請與 Android 性能團隊聯繫以幫助您驗證它。

啟用sys.use_fifo_ui ,ActivityManager 會跟踪頂級應用程序的 UI 線程和 RenderThread(兩個最關鍵的 UI 線程),並使這些線程成為 SCHED_FIFO 而不是 SCHED_OTHER。這有效地消除了 UI 和 RenderThreads 的抖動;我們使用此選項收集的跟踪顯示以微秒而不是毫秒為單位顯示可運行時間。

但是,由於 RT 負載均衡器沒有容量感知,應用程序啟動性能降低了 30%,因為負責啟動應用程序的 UI 線程將從 2.1Ghz 金色 Kryo 內核移至 1.5GHz 銀色 Kryo 內核.借助容量感知 RT 負載均衡器,我們在許多 UI 基準測試中看到批量操作的性能相當,並且第 95 和第 99 個百分位幀時間減少了 10-15%。

中斷流量

由於 ARM 平台默認只向 CPU 0 提供中斷,因此我們建議使用 IRQ 平衡器(高通平台上的 irqbalance 或 msm_irqbalance)。

在 Pixel 開發過程中,我們看到了可能直接歸因於 CPU 0 中斷的卡頓。例如,如果mdss_fb0線程被安排在 CPU 0 上,則由於幾乎在掃描輸出之前由顯示器觸發的中斷,出現卡頓的可能性要大得多。 mdss_fb0將在其自己的工作中處於非常緊迫的截止日期,然後它會浪費一些時間給 MDSS 中斷處理程序。最初,我們嘗試通過將 mdss_fb0 線程的 CPU 親和性設置為 CPU 1-3 來解決此問題,以避免與中斷發生爭用,但後來我們意識到我們尚未啟用 msm_irqbalance。啟用 msm_irqbalance 後,即使 mdss_fb0 和 MDSS 中斷都在同一個 CPU 上,由於其他中斷的爭用減少,jank 也得到了顯著改善。

這可以通過查看 sched 部分和 irq 部分在 systrace 中識別。 sched 部分顯示了已調度的內容,但 irq 部分中的重疊區域意味著在此期間正在運行中斷而不是正常調度的進程。如果您看到在中斷期間花費了大量時間,您的選擇包括:

  • 使中斷處理程序更快。
  • 首先防止中斷發生。
  • 將中斷的頻率更改為與可能干擾的其他常規工作不同相(如果它是常規中斷)。
  • 直接設置中斷的 CPU 親和性,防止它被平衡。
  • 設置中斷正在干擾的線程的 CPU 親和性以避免中斷。
  • 依靠中斷平衡器將中斷移動到負載較少的 CPU。

通常不建議設置 CPU 親和性,但在特定情況下可能很有用。一般來說,對於最常見的中斷,很難預測系統的狀態,但是如果您有一組非常具體的條件會觸發某些系統比正常情況更受限制的中斷(例如 VR),那麼明確的 CPU 親和性可能是一個很好的解決方案。

長軟中斷

當 softirq 運行時,它會禁用搶占。 softirqs 也可以在內核中的許多地方觸發,並且可以在用戶進程內部運行。如果有足夠的軟中斷活動,用戶進程將停止運行軟中斷,並且 ksoftirqd 喚醒以運行軟中斷並進行負載平衡。通常,這很好。然而,一個非常長的軟中斷會對系統造成嚴重破壞。


softirq 在跟踪的 irq 部分中是可見的,因此如果在跟踪時可以重現問題,它們很容易被發現。因為軟中斷可以在用戶進程中運行,一個壞的軟中斷也可以在用戶進程內部表現為額外的運行時間,沒有明顯的原因。如果你看到了,請檢查 irq 部分,看看是否應該歸咎於軟中斷。

驅動程序使搶占或 IRQ 禁用時間過長

禁用搶占或中斷太長時間(幾十毫秒)會導致卡頓。通常,卡頓表現為線程變為可運行但不在特定 CPU 上運行,即使可運行線程的優先級(或 SCHED_FIFO)明顯高於其他線程。

一些指導方針:

  • 如果可運行線程為 SCHED_FIFO 且正在運行的線程為 SCHED_OTHER,則正在運行的線程已禁用搶占或中斷。
  • 如果可運行線程的優先級 (100) 明顯高於運行線程 (120),則如果可運行線程未在兩個 jiffies 內運行,則運行線程可能已禁用搶占或中斷。
  • 如果可運行線程和正在運行的線程具有相同的優先級,則如果可運行線程在 20ms 內沒有運行,則運行線程可能已禁用搶占或中斷。

請記住,運行中斷處理程序會阻止您為其他中斷提供服務,這也會禁用搶占。


識別違規區域的另一個選項是使用 preemptirqsoff 跟踪器(請參閱使用動態 ftrace )。此跟踪器可以更深入地了解不可中斷區域(例如函數名稱)的根本原因,但需要更多的侵入性工作才能啟用。雖然它可能會對性能產生更大的影響,但絕對值得一試。

錯誤使用工作隊列

中斷處理程序通常需要執行可以在中斷上下文之外運行的工作,從而可以將工作外包給內核中的不同線程。驅動程序開發人員可能會注意到內核有一個非常方便的系統範圍異步任務功能,稱為工作隊列,並且可能將其用於與中斷相關的工作。

然而,工作隊列幾乎總是這個問題的錯誤答案,因為它們總是 SCHED_OTHER。許多硬件中斷處於性能的關鍵路徑中,必須立即運行。工作隊列無法保證它們何時運行。每次我們在性能的關鍵路徑中看到一個工作隊列時,它都是零星卡頓的來源,無論設備如何。在配備旗艦處理器的 Pixel 上,我們看到如果設備負載不足,單個工作隊列可能會延遲長達 7 毫秒,具體取決於調度程序行為和系統上運行的其他內容。

需要在單獨的線程中處理類似中斷的工作的驅動程序應該創建自己的 SCHED_FIFO kthread,而不是工作隊列。有關使用 kthread_work 函數執行此操作的幫助,請參閱此補丁

框架鎖爭用

框架鎖爭用可能是卡頓或其他性能問題的根源。它通常是由 ActivityManagerService 鎖引起的,但也可以在其他鎖中看到。例如,PowerManagerService 鎖會影響屏幕的性能。如果您在設備上看到此問題,則沒有好的解決方法,因為它只能通過框架的架構改進來改進。但是,如果您正在修改在 system_server 內部運行的代碼,那麼避免長時間持有鎖非常重要,尤其是 ActivityManagerService 鎖。

活頁夾鎖爭用

從歷史上看,binder 有一個全局鎖。如果運行綁定器事務的線程在持有鎖時被搶占,則在原始線程釋放鎖之前,其他線程無法執行綁定器事務。這是不好的; binder 爭用會阻塞系統中的所有內容,包括向顯示器發送 UI 更新(UI 線程通過 binder 與 SurfaceFlinger 通信)。

Android 6.0 包含幾個補丁,通過在持有活頁夾鎖時禁用搶占來改善此行為。這只是安全的,因為綁定器鎖應該在實際運行時保持幾微秒。這顯著提高了無競爭情況下的性能,並通過在保持綁定鎖時阻止大多數調度程序切換來防止爭用。但是,無法在持有 binder 鎖的整個運行時禁用搶占,這意味著對可能休眠的函數(例如 copy_from_user)啟用了搶占,這可能會導致與原始情況相同的搶占。當我們將補丁發送到上游時,他們立即告訴我們這是歷史上最糟糕的想法。 (我們同意他們的觀點,但我們也無法與補丁在防止卡頓方面的功效爭論。)

進程內的 fd 爭用

這是罕見的。您的卡頓可能不是由此引起的。

也就是說,如果您在一個進程中有多個線程寫入同一個 fd,則可能會在該 fd 上看到爭用,但是我們在 Pixel 啟動期間唯一一次看到這種情況是在低優先級線程試圖佔用所有 CPU 的測試期間單個高優先級線程在同一進程中運行的時間。所有線程都在寫入跟踪標記 fd,如果低優先級線程持有 fd 鎖然後被搶占,則高優先級線程可能會在跟踪標記 fd 上被阻塞。當從低優先級線程禁用跟踪時,沒有性能問題。

我們無法在任何其他情況下重現這一點,但值得指出的是跟踪時性能問題的潛在原因。

不必要的 CPU 空閒轉換

在處理 IPC,尤其是多進程管道時,通常會看到以下運行時行為的變化:

  1. 線程 A 在 CPU 1 上運行。
  2. 線程 A 喚醒線程 B。
  3. 線程 B 開始在 CPU 2 上運行。
  4. 線程 A 立即進入睡眠狀態,當線程 B 完成當前工作時,線程 B 將其喚醒。

開銷的常見來源是步驟 2 和步驟 3。如果 CPU 2 空閒,則必須在線程 B 運行之前將其帶回活動狀態。根據 SOC 和空閒的深度,這可能是線程 B 開始運行之前的幾十微秒。如果 IPC 每一端的實際運行時間與開銷足夠接近,則 CPU 空閒轉換會顯著降低該管道的整體性能。 Android 最常遇到這種情況的地方是 binder 事務,許多使用 binder 的服務最終看起來像上面描述的情況。

首先,在內核驅動程序中使用wake_up_interruptible_sync()函數,並從任何自定義調度程序中支持它。將此視為要求,而不是提示。 Binder 今天使用它,它對同步 binder 事務有很大幫助,避免了不必要的 CPU 空閒轉換。

其次,確保您的 cpuidle 轉換時間是現實的,並且 cpuidle 調控器正確地考慮了這些。如果您的 SOC 在您的最深空閒狀態中進進出出,您將無法通過進入最深空閒狀態來節省電量。

日誌記錄

對於 CPU 週期或內存,日誌記錄不是免費的,因此不要向日誌緩衝區發送垃圾郵件。在您的應用程序(直接)和日誌守護程序中記錄成本週期。在運送您的設備之前刪除所有調試日誌。

輸入輸出問題

I/O 操作是常見的抖動源。如果一個線程訪問一個內存映射文件並且該頁面不在頁面緩存中,它會出錯並從磁盤讀取該頁面。這會阻塞線程(通常持續 10 毫秒以上),如果它發生在 UI 渲染的關鍵路徑中,可能會導致卡頓。導致 I/O 操作的原因太多,此處無法討論,但在嘗試改進 I/O 行為時請檢查以下位置:

  • 平納服務。在 Android 7.0 中添加的 PinnerService 使框架能夠鎖定頁面緩存中的一些文件。這將刪除內存以供任何其他進程使用,但如果有一些事先已知的文件可以定期使用,則可以有效地鎖定這些文件。

    在運行 Android 7.0 的 Pixel 和 Nexus 6P 設備上,我們鎖定了四個文件:
    • /system/framework/arm64/boot-framework.oat
    • /system/framework/oat/arm64/services.odex
    • /system/framework/arm64/boot.oat
    • /system/framework/arm64/boot-core-libart.oat
    大多數應用程序和 system_server 一直在使用這些文件,因此不應將它們調出。特別是,我們發現如果其中任何一個被調出,它們將被調回並在從重量級應用程序切換時導致卡頓。
  • 加密。 I/O 問題的另一個可能原因。我們發現,與基於 CPU 的加密或使用可通過 DMA 訪問的硬件塊相比,內聯加密提供了最佳性能。最重要的是,內聯加密減少了與 I/O 相關的抖動,尤其是與基於 CPU 的加密相比。由於對頁面緩存的提取通常位於 UI 渲染的關鍵路徑中,因此基於 CPU 的加密會在關鍵路徑中引入額外的 CPU 負載,這會增加比 I/O 提取更多的抖動。

    基於 DMA 的硬件加密引擎也有類似的問題,因為即使其他關鍵工作可以運行,內核也必須花費周期來管理該工作。我們強烈建議任何 SOC 供應商構建新硬件以包括對內聯加密的支持。

積極的小任務打包

一些調度程序支持將小任務打包到單個 CPU 內核上,以嘗試通過使更多 CPU 更長時間保持空閒來降低功耗。雖然這對吞吐量和功耗很有效,但它對延遲可能是災難性的。 UI渲染的關鍵路徑中有幾個短時間運行的線程,可以認為是很小的;如果這些線程在緩慢遷移到其他 CPU 時被延遲,則會導致卡頓。我們建議非常保守地使用小任務打包。

頁面緩存抖動

沒有足夠可用內存的設備在執行長時間運行的操作(例如打開新應用程序)時可能會突然變得非常緩慢。應用程序的跟踪可能會顯示它在特定運行期間始終在 I/O 中被阻塞,即使它通常不會在 I/O 中被阻塞。這通常是頁面緩存抖動的標誌,尤其是在內存較少的設備上。

識別此問題的一種方法是使用 pagecache 標記獲取系統跟踪,並將該跟踪提供給位於system/extras/pagecache/pagecache.py的腳本。 pagecache.py 將將文件映射到頁面緩存的單個請求轉換為每個文件的聚合統計信息。如果您發現讀取的文件字節數超過了磁盤上該文件的總大小,那麼您肯定會遇到頁面緩存抖動。

這意味著您的工作負載(通常是單個應用程序加上 system_server)所需的工作集大於設備上頁面緩存可用的內存量。結果,隨著工作負載的一部分在頁面緩存中獲得了它需要的數據,另一部分將在不久的將來使用將被驅逐,並且必須再次提取,導致問題再次出現,直到加載已完成。當設備上沒有足夠的內存可用時,這是性能問題的根本原因。

沒有萬無一失的方法來修復頁面緩存抖動,但有幾種方法可以嘗試在給定設備上改進它。

  • 在持久化進程中使用更少的內存。持久進程使用的內存越少,應用程序和頁面緩存可用的內存就越多。
  • 審核您對設備的分割,以確保您不會從操作系統中不必要地刪除內存。我們已經看到用於調試的分割意外地留在了內核配置中,消耗了數十兆字節的內存。這可以在命中頁面緩存抖動和不命中頁面緩存之間產生差異,尤其是在內存較少的設備上。
  • 如果您在 system_server 中看到關鍵文件上的頁面緩存抖動,請考慮固定這些文件。這將增加其他地方的內存壓力,但它可能會修改行為足以避免顛簸。
  • 重新調整 lowmemorykiller 以嘗試保持更多可用內存。 lowmemorykiller 的閾值基於絕對可用內存和頁面緩存,因此增加給定 oom_adj 級別的進程被殺死的閾值可能會導致更好的行為,但會增加後台應用程序的死亡。
  • 嘗試使用 ZRAM。我們在 Pixel 上使用 ZRAM,即使 Pixel 有 4GB,因為它可以幫助處理很少使用的髒頁。