抖動是一種隨機系統行為,會阻止可感知的工作運作。本頁介紹如何辨識和解決與抖動相關的卡頓問題。
應用程式執行緒調度程序延遲
調度程序延遲是抖動最明顯的症狀:應該運行的進程變得可運行,但在相當長的時間內沒有運行。延遲的重要性會根據具體情況而有所不同。例如:
- 應用程式中的隨機輔助線程可能會延遲很多毫秒而不會出現問題。
- 應用程式的 UI 執行緒可能能夠容忍 1-2 毫秒的抖動。
- 作為 SCHED_FIFO 運行的驅動程式 kthreads 如果在運行前可執行 500us,則可能會導致問題。
可運行時間可以在 systrace 中透過執行緒運行段前面的藍色條來標識。可運行時間也可以透過執行緒的sched_wakeup
事件與發出執行緒執行開始訊號的sched_switch
事件之間的時間長度來決定。
運行時間過長的執行緒
應用程式 UI 執行緒運行時間過長可能會導致問題。可運行時間較長的較低層級執行緒通常有不同的原因,但嘗試將 UI 執行緒可運行時間推向零可能需要修復導致較低層級執行緒可運行時間較長的一些相同問題。為了減少延誤:
- 按照熱調節中的說明使用 cpuset。
- 增加 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。這通常是一個簡單的修復(請參閱修復 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.1Ghz 黃金 Kryo 核心移至 1.5GHz 銀 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 上,由於其他中斷的爭用減少,卡頓現像也得到了顯著改善。
這可以透過查看 sched 部分和 irq 部分在 systrace 中識別。 sched 部分顯示已調度的內容,但 irq 部分中的重疊區域意味著在此期間正在運行中斷,而不是正常調度的進程。如果您發現中斷期間花費了大量時間,您的選擇包括:
- 使中斷處理程序更快。
- 首先防止中斷發生。
- 更改中斷的頻率,使其與可能幹擾的其他常規工作異相(如果是常規中斷)。
- 直接設定中斷的CPU親和力,防止其被平衡。
- 設定中斷幹擾的執行緒的 CPU 關聯性以避免中斷。
- 依靠中斷平衡器將中斷移至負載較少的 CPU。
通常不建議設定 CPU 關聯性,但對於特定情況可能很有用。一般來說,預測大多數常見中斷的系統狀態非常困難,但如果您有一組非常特定的條件觸發某些中斷,而係統比正常情況(例如 VR)受到更多限制,則顯式 CPU 親和力可能會是一個很好的解決方案。
長軟中斷
當軟中斷運行時,它會停用搶佔。軟中斷還可以在核心中的許多地方觸發,並且可以在用戶進程內部運行。如果有足夠的軟中斷活動,使用者程序將停止運行軟中斷,並且 ksoftirqd 被喚醒以運行軟中斷並進行負載平衡。通常,這很好。然而,單一非常長的軟中斷可能會對系統造成嚴重破壞。
軟中斷在追蹤的 irq 部分中可見,因此在追蹤時是否可以重現問題,很容易發現它們。由於軟中斷可以在用戶進程內運行,因此不良軟中斷也可能無明顯原因地表現為用戶進程內的額外運行時間。如果您看到這種情況,請檢查 irq 部分,看看是否是軟中斷造成的。
驅動程式禁用搶佔或 IRQ 的時間過長
禁用搶佔或中斷時間過長(數十毫秒)會導致卡頓。通常,卡頓表現為執行緒變得可運行但未在特定 CPU 上執行,即使可執行執行緒的優先權(或 SCHED_FIFO)明顯高於其他執行緒。
一些指導方針:
- 如果可執行執行緒為 SCHED_FIFO 並且正在執行執行緒為 SCHED_OTHER,則正在執行執行緒已停用搶佔或中斷。
- 如果可運行執行緒的優先權 (100) 明顯高於正在執行的執行緒 (120),則如果可執行執行緒未在兩個 jiffies 內執行,則正在執行的執行緒可能已停用搶佔或中斷。
- 如果可運行執行緒和正在運行的執行緒具有相同的優先權,則如果可運行執行緒在 20 毫秒內沒有運行,則正在運行的執行緒可能已停用搶佔或中斷。
請記住,執行中斷處理程序會阻止您服務其他中斷,這也會停用搶佔。
識別違規區域的另一個選項是使用 preemptirqsoff 追蹤器(請參閱使用動態 ftrace )。此追蹤器可以更深入地了解不間斷區域的根本原因(例如函數名稱),但需要更多侵入性工作才能啟用。雖然它可能會對性能產生更大的影響,但絕對值得嘗試。
工作隊列的錯誤使用
中斷處理程序通常需要執行可以在中斷上下文之外運行的工作,從而可以將工作分配給核心中的不同執行緒。驅動程式開發人員可能會注意到核心有一個非常方便的系統範圍非同步任務功能,稱為工作佇列,並且可能將其用於與中斷相關的工作。
然而,工作佇列幾乎總是這個問題的錯誤答案,因為它們總是 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 爭用
這是很少見的。您的卡頓可能不是由這個引起的。
也就是說,如果進程中有多個執行緒寫入相同的fd,則可能會看到該fd 上的爭用,但是我們在Pixel 啟動過程中看到這種情況的唯一一次是在低優先級線程試圖佔用所有CPU 的測試期間。當單一高優先權執行緒在同一進程中執行時的時間。所有執行緒都在寫入追蹤標記 fd,如果低優先權執行緒持有 fd 鎖並隨後被搶佔,則高優先權執行緒可能會在追蹤標記 fd 上被阻塞。當從低優先權執行緒停用追蹤時,不存在效能問題。
我們無法在任何其他情況下重現此問題,但值得指出的是,這是追蹤時導致效能問題的潛在原因。
不必要的 CPU 閒置轉換
在處理 IPC(尤其是多進程管道)時,通常會看到以下運行時行為的變化:
- 執行緒 A 在 CPU 1 上運作。
- 執行緒A喚醒執行緒B。
- 執行緒 B 開始在 CPU 2 上運作。
- 執行緒 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 可讓框架鎖定頁面快取中的某些檔案。這會刪除任何其他進程使用的內存,但如果事先已知某些檔案會定期使用,則對這些檔案進行 mlock 可能會很有效。
在運行 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
- 加密。 I/O 問題的另一個可能原因。我們發現,與基於 CPU 的加密或使用可透過 DMA 存取的硬體區塊相比,內聯加密可提供最佳效能。最重要的是,內聯加密減少了與 I/O 相關的抖動,尤其是與基於 CPU 的加密相比。由於對頁面快取的提取通常位於 UI 渲染的關鍵路徑中,因此基於 CPU 的加密會在關鍵路徑中引入額外的 CPU 負載,這會比 I/O 提取增加更多的抖動。
基於 DMA 的硬體加密引擎也有類似的問題,因為即使其他關鍵工作可以運行,核心也必須花費週期來管理該工作。我們強烈建議任何 SOC 供應商建置新硬體以支援內聯加密。
積極的小任務打包
一些調度程序支援將小任務打包到單一 CPU 核心上,以嘗試透過讓更多 CPU 保持更長時間空閒來降低功耗。雖然這對於吞吐量和功耗來說效果很好,但對於延遲來說可能是災難性的。 UI渲染的關鍵路徑中有幾個可以認為很小的短運行線程;如果這些執行緒因為緩慢遷移到其他CPU而被延遲,就會導致卡頓。我們建議非常保守地使用小任務打包。
頁面快取抖動
沒有足夠可用記憶體的裝置在執行長時間運行的操作(例如開啟新應用程式)時可能會突然變得極其緩慢。應用程式的追蹤可能會顯示它在特定運行期間始終被 I/O 阻塞,即使它通常不會被阻塞。這通常是頁面快取抖動的跡象,尤其是在記憶體較少的裝置上。
識別此問題的一種方法是使用 pagecache 標記來取得 systrace,並將該追蹤提供給位於system/extras/pagecache/pagecache.py
腳本。 pagecache.py 將把檔案對應到頁面快取的單一請求轉換為每個檔案的聚合統計資料。如果您發現讀取的檔案位元組數多於磁碟上該檔案的總大小,則肯定會遇到頁面快取抖動。
這表示您的工作負載(通常是單一應用程式加上 system_server)所需的工作集大於裝置上頁面快取可用的記憶體量。結果,當工作負載的一部分在頁面快取中獲取其所需的資料時,不久的將來將使用的另一部分將被驅逐並且必須再次獲取,導致問題再次發生,直到載入已完成。當設備上沒有足夠的可用記憶體時,這是導致效能問題的根本原因。
沒有萬無一失的方法可以修復頁面快取抖動,但有幾種方法可以嘗試在給定裝置上改進此問題。
- 在持久進程中使用更少的記憶體。持久進程使用的記憶體越少,可供應用程式和頁面快取使用的記憶體就越多。
- 審核您的裝置的剝離情況,以確保您不會不必要地從作業系統中刪除記憶體。我們見過這樣的情況:用於調試的剝離被意外地留在了發布的內核配置中,從而消耗了數十兆的記憶體。這可能會造成頁面快取抖動與否的區別,特別是在記憶體較少的裝置上。
- 如果您發現 system_server 中的關鍵檔案出現頁面快取抖動,請考慮固定這些檔案。這會增加其他地方的記憶體壓力,但它可能足以修改行為以避免抖動。
- 重新調整 lowmemorykiller 以嘗試保留更多可用記憶體。 lowmemorykiller 的閾值是基於絕對可用記憶體和頁面緩存,因此增加給定 oom_adj 層級的進程被終止的閾值可能會導致更好的行為,但代價是後台應用程式死亡增加。
- 嘗試使用 ZRAM。儘管 Pixel 有 4GB,但我們在 Pixel 上使用 ZRAM,因為它可以幫助處理很少使用的髒頁。