找出卡頓相關的抖動

抖動是隨機系統行為,會導致可感知的工作無法執行。本頁說明如何找出並解決與抖動相關的卡頓問題。

應用程式執行緒排程器延遲

調度器延遲是抖動最明顯的症狀:應執行的程序已設為可執行,但在一段相當長的時間內未執行。延遲時間的重要性會因脈絡而異。例如:

  • 應用程式中的隨機輔助執行緒可能會延遲數毫秒,但不會造成問題。
  • 應用程式的 UI 執行緒可能可以容許 1 到 2 毫秒的抖動。
  • 如果驅動程式 kthread 以 SCHED_FIFO 執行,且在執行前可執行 500us,可能會造成問題。

在 systrace 中,您可以透過執行緒執行區段前方的藍色長條,找出可執行時間。可執行時間也可以由執行緒的 sched_wakeup 事件和 sched_switch 事件之間的時間長度決定,該事件會用來通知執行緒開始執行。

執行時間過長的執行緒

應用程式 UI 執行緒可執行的時間過長可能會導致問題。執行時間過長的低階執行緒通常有不同的原因,但嘗試將 UI 執行緒的執行時間推向零時,可能需要修正導致低階執行緒執行時間過長的部分問題。如要減少延遲時間,請採取下列做法:

  1. 請按照「熱節流」一文的說明使用 cpuset。
  2. 提高 CONFIG_HZ 值。
    • 在 arm 和 arm64 平台上,這個值一向設為 100。不過,這只是歷史上的意外,並非用於互動式裝置的理想值。CONFIG_HZ=100 表示 jiffy 長度為 10 毫秒,也就是說,CPU 之間的負載平衡可能需要 20 毫秒 (兩個 jiffy) 才能完成。這可能會大幅影響已載入系統的 jank。
    • 近期推出的裝置 (Nexus 5X、Nexus 6P、Pixel 和 Pixel XL) 已搭載 CONFIG_HZ=300。這應該會產生微不足道的電力成本,同時大幅改善可執行時間。如果在變更 CONFIG_HZ 後,電力消耗量大幅增加或發生效能問題,很可能是其中一個驅動程式使用以原始 jiffies 為單位的計時器,而非毫秒,並將其轉換為 jiffies。這通常很容易解決 (請參閱修補程式,該修補程式可修正 Nexus 5X 和 6P 在轉換為 CONFIG_HZ=300 時的 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.1 GHz 的金色 Kryo 核心移至 1.5 GHz 的銀色 Kryo 核心。有了容量感知的 RT 負載平衡器,我們發現大量作業的效能相當,且在許多 UI 基準測試中,95 和 99 百分位數的幀時間減少了 10% 至 15%。

中斷流量

由於 ARM 平台預設只會將中斷傳送至 CPU 0,因此建議使用 IRQ 平衡器 (在 Qualcomm 平台上為 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 也明顯改善。

您可以查看 systrace 中的 sched 部分和 irq 部分,找出這類問題。sched 區段會顯示已排定的項目,但 irq 區段中的重疊區域表示在該時間點執行的是中斷程序,而非正常排定的程序。如果您發現中斷期間耗費大量時間,可以採取以下做法:

  • 加快中斷處理程序的速度。
  • 避免發生中斷情形。
  • 變更中斷的頻率,使其與可能造成干擾的其他例行工作 (如果是中斷) 不同步。
  • 直接設定中斷的 CPU 親和性,並防止其平衡。
  • 設定中斷干擾的執行緒的 CPU 親和性,以免發生中斷。
  • 請依賴中斷平衡器,將中斷移至負載較低的 CPU。

一般來說,我們不建議設定 CPU 親和性,但在特定情況下,這項設定可能會很有用。一般來說,要預測系統在大多數常見中斷的狀態,實在太難。不過,如果您有非常明確的條件組合,可觸發系統比平常更受限的特定中斷 (例如 VR),明確的 CPU 親和性可能就是不錯的解決方案。

長時間的軟中斷

軟中斷執行時會停用預取。軟中斷也可以在核心內的許多位置觸發,並在使用者程序中執行。如果有足夠的 softirq 活動,使用者程序就會停止執行 softirq,而 ksoftirqd 會喚醒並執行 softirq,並進行負載平衡。通常這不會造成問題。不過,單一非常長的軟中斷可能會對系統造成嚴重破壞。


軟式 IRQ 會顯示在追蹤的 irq 區段中,因此如果問題可以在追蹤期間重現,您就能輕鬆找出問題。由於軟中斷可在使用者程序中執行,因此軟中斷異常也會導致使用者程序中出現額外的執行時間,而沒有明顯的原因。如果發現這種情況,請檢查 irq 部分,看看是否是 softirq 造成的問題。

驅動程式讓預取或 IRQ 停用太久

停用優先權或中斷時間過長 (數十毫秒) 會導致卡頓。一般來說,資源浪費會導致執行緒變成可執行,但不會在特定 CPU 上執行,即使可執行的執行緒優先順序 (或 SCHED_FIFO) 明顯高於其他執行緒亦然。

以下提供一些指南:

  • 如果可執行的執行緒為 SCHED_FIFO,而執行中的執行緒為 SCHED_OTHER,則執行中的執行緒已停用先占或中斷。
  • 如果可執行緒的優先順序 (100) 明顯高於執行中緒 (120),如果可執行緒未在兩個 jiffies 內執行,執行中緒可能會停用預取或中斷。
  • 如果可執行的執行緒和執行中的執行緒具有相同的優先順序,如果可執行的執行緒未在 20 毫秒內執行,執行中的執行緒可能會停用先占或中斷。

請注意,執行中斷處理常式會導致您無法處理其他中斷,也會停用預取。


另一種找出違規區域的方法是使用預防 IRQ 關閉追蹤器 (請參閱「使用動態 ftrace」)。這項追蹤程式可深入瞭解不可中斷區域 (例如函式名稱) 的根本原因,但需要進行更多侵入性工作才能啟用。雖然這可能會對效能造成更大影響,但絕對值得一試。

工作佇列使用方式有誤

中斷處理常常需要執行可在中斷內容外執行的工作,讓工作可分派至核心中的不同執行緒。驅動程式開發人員可能會注意到核心具有非常方便的系統層級非同步工作功能,稱為 workqueues,並可能將其用於中斷相關工作。

不過,工作佇列幾乎總是這個問題的錯誤答案,因為它們一律都是 SCHED_OTHER。許多硬體中斷都位於效能關鍵路徑,因此必須立即執行。工作佇列無法保證何時執行。無論裝置為何,只要在效能關鍵路徑中看到工作佇列,就會導致不時出現的卡頓情形。在 Pixel 上,如果裝置處於負載狀態,則單一工作佇列可能會延遲最多 7 毫秒,這取決於排程器行為和系統上執行的其他項目。

需要在個別執行緒中處理類似中斷的工作時,驅動程式應建立自己的 SCHED_FIFO kthread,而非工作佇列。如要瞭解如何使用 kthread_work 函式執行這項操作,請參閱這個修正程式

架構鎖定爭用

架構鎖定爭用情況可能會導致卡頓或其他效能問題。這通常是 ActivityManagerService 鎖定造成的,但也可能出現在其他鎖定中。舉例來說,PowerManagerService 鎖定可能會影響螢幕的效能。如果您在裝置上看到這個問題,就無法有效修正,因為這只能透過改善架構架構來改善。不過,如果您要修改在 system_server 中執行的程式碼,請務必避免長時間保留鎖定,尤其是 ActivityManagerService 鎖定。

Binder 鎖定爭用

過去,Binder 只有一個全域鎖定機制。如果執行繫結器交易的執行緒在持有鎖定時遭到搶先,則在原始執行緒釋放鎖定之前,其他執行緒都無法執行繫結器交易。這會造成不良影響,因為 Binder 爭用可能會阻斷系統中的所有作業,包括將 UI 更新內容傳送至螢幕 (UI 執行緒會透過 Binder 與 SurfaceFlinger 通訊)。

Android 6.0 包含了幾個修正程式,可在保留繫結器鎖定時停用預取,以改善這項行為。這項做法之所以安全,是因為繫結器鎖定機制應在實際執行期間的幾微秒內保持鎖定狀態。這項做法可大幅改善無競爭情況下的效能,並在繫結器鎖定時防止大部分排程器切換,進而避免競爭。不過,在整個持有繫結器鎖定機制的執行階段中,無法停用預取權,這表示可讓可休眠的函式 (例如 copy_from_user) 啟用預取權,這可能會導致與原始情況相同的預取權。當我們將修補程式傳送至上游時,他們立即告訴我們,這是史上最糟糕的想法。(我們同意他們的說法,但也無法否認修補程式能有效防止卡頓現象)。

程序內的 fd 競爭

這種情況很少發生。這可能不是造成 jank 的原因。

不過,如果在同一個程序中有多個執行緒寫入相同的 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 最常發生此問題的情況是與繫結器交易有關,許多使用繫結器的服務最終都會出現上述情況。

首先,請在核心驅動程式中使用 wake_up_interruptible_sync() 函式,並透過任何自訂排程器支援此函式。請將這項規定視為必要條件,而非提示。Binder 目前使用這個功能,這對同步 Binder 交易非常有幫助,可避免不必要的 CPU 閒置轉換。

其次,請確保 CPU 閒置轉換時間合理,且 CPU 閒置節流器能正確考量這些時間。如果 SOC 在最深層的閒置狀態中發生抖動,您就無法透過進入最深層的閒置狀態來節省電力。

記錄

記錄不會占用 CPU 週期或記憶體,因此請勿濫用記錄緩衝區。在應用程式 (直接) 和記錄守護程式中記錄費用週期。寄送裝置前,請移除所有偵錯記錄。

I/O 問題

I/O 作業是卡頓的常見來源。如果執行緒存取記憶體對應檔案,而該頁面不在頁面快取中,則會發生錯誤,並從磁碟讀取該頁面。這會阻斷執行緒 (通常為 10 毫秒以上),如果發生在 UI 轉譯的關鍵路徑中,可能會導致卡頓。導致 I/O 作業的因素太多,無法在此一一討論,但在嘗試改善 I/O 行為時,請檢查下列位置:

  • PinnerService。PinnerService 是在 Android 7.0 中新增的功能,可讓架構鎖定頁面快取中的部分檔案。這麼做可移除記憶體,供其他程序使用,但如果有某些檔案已知會經常使用,則可以有效地將這些檔案鎖定。

    在搭載 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) 所需的工作組,大於裝置上可用於頁面快取的記憶體量。因此,當工作負載的一部分在網頁快取中取得所需資料時,即將在近期內使用的另一部分會遭到淘汰,並必須重新擷取,導致問題再次發生,直到載入作業完成為止。當裝置記憶體不足時,這就是導致效能問題的根本原因。

雖然沒有萬無一失的方法可以修正頁面快取衝突,但您可以嘗試透過幾種方式改善特定裝置上的情況。

  • 在持續性程序中減少記憶體用量。持久性程序使用的記憶體越少,應用程式和頁面快取可用的記憶體就越多。
  • 請稽核裝置的切割區,確保您不會不必要地從 OS 移除記憶體。我們曾遇到以下情況:用於偵錯的切割區意外保留在發布內核設定中,耗用數十 MB 的記憶體。這可能會影響是否觸發頁面快取衝突,特別是在記憶體較少的裝置上。
  • 如果您在 system_server 中看到關鍵檔案的頁面快取發生衝突,請考慮將這些檔案固定。這會增加其他地方的記憶體壓力,但可能會修改足以避免資源耗盡的行為。
  • 重新調整 lowmemorykiller,盡量釋出更多記憶體。lowmemorykiller 的門檻會根據絕對可用記憶體和頁面快取,因此提高特定 oom_adj 等級程序終止門檻的話,雖然可能會改善行為,但也會導致背景應用程式終止次數增加。
  • 請嘗試使用 ZRAM。即使 Pixel 有 4 GB 記憶體,我們仍會在 Pixel 上使用 ZRAM,因為這項技術有助於處理鮮少使用的髒頁。