RFC-0084:在 zx_info_task_runtime_t 新增更多指標

RFC-0084:在 zx_info_task_runtime_t 中新增更多指標
狀態已接受
區域
  • 核心
說明

在 zx_info_task_runtime_t 中新增其他指標,以便對媒體效能問題進行偵錯。

問題
Gerrit 變更
作者
審查人員
提交日期 (年-月-日)2021-03-09
審查日期 (年-月-日)2021-04-06

摘要

ZX_INFO_TASK_RUNTIME 主題提供一種方法,可擷取工作在 CPU 上執行或排入執行佇列的總時間。為了診斷排程瓶頸,我們建議您新增其他精細的執行階段資訊,特別是等待頁面錯誤和等待核心互斥鎖所需的時間。

提振精神

即時工作有截止期限。有時,工作未能如期完成,是因為工作在核心中遭到阻斷的時間過長。如要對這些情況進行偵錯,瞭解工作遭到封鎖的原因會有所幫助。舉例來說,如果工作有 10 毫秒的期限,但在等待頁面錯誤時耗費了 11 毫秒,我們可以推斷,由於頁面錯誤太慢,因此錯過了期限。

具體來說,我們希望改善 audio_core 等媒體子系統產生的診斷資訊。在 audio_core 中,每項 Mixer 工作都必須在 10 毫秒內完成。如果任務耗時超過 10 毫秒,使用者會聽到音訊中斷的情形。我們最近發現,由於頁面錯誤 (需要將 audio_core 可執行頁面頁面回頁) 和核心堆積區互斥鎖的競爭,導致未能如期完成作業。這些問題無法透過快照診斷,我們必須記錄追蹤記錄,這項程序相當繁瑣,而且有時會因為在本機重現問題的難度而受阻 (因為錯誤可能只會在特定環境中透過特定應用程式觸發)。這些是對媒體成效造成嚴重負面影響的重要問題。

我們的目標是從核心匯出足夠的診斷資訊,以便我們從快照中診斷這些問題,而不需要深入研究執行追蹤記錄。這份文件建議在 zx_info_task_runtime_t 中新增統計資料,以便更完整地回答「為什麼這個工作無法執行?」的問題。

Zircon 執行緒期限的背景資訊

Zircon 期限設定檔包含三個部分:期間、容量和期限。當執行緒指派了期限設定檔時,Zircon 會保證每個期間,執行緒會在每個期間開始的期限內,分配至 CPU 的容量。請參考以下範例:

  • 週期 = 10 毫秒
  • 容量 = 2 毫秒
  • 期限 = 5 毫秒

每隔 10 毫秒,執行緒會在接下來的 5 毫秒內分配 2 毫秒的 CPU。有兩種情況會導致您錯過截止日期:

  1. 工作排程延後。舉例來說,如果下一個週期開始時間為 T,但工作未在 T+4 毫秒內排定,則工作就無法在期限內完成 (應在 T+3 毫秒內排定)。核心可以偵測這類未達期限的情況,但在實際情況中,除非排程器有錯誤或超訂閱,否則不應發生這種情況。

  2. 工作需要超過 2 毫秒才能完成。因為核心不瞭解工作界限,因此無法得知這類情況何時發生。舉例來說,如果工作排定在 T+1 毫秒,並且總共執行 1 毫秒,然後封鎖 9 毫秒,核心就無法得知工作是否錯過了截止時間 (因為它遭到某項內容封鎖),或是工作要求 2 毫秒,但只需要 1 毫秒即可完成 (然後休眠 9 毫秒以等待下一個週期)。

我們的目標是協助您診斷第二種未如期提交的情況。目前,如果工作耗費的時間過長,可以查詢 ZX_INFO_TASK_RUNTIME,瞭解在使用者空間中,CPU 執行工作耗費了多少時間。如果 cpu_time 大於工作預期的執行時間,工作就會知道自己執行的時間過長。不過,如果 cpu_time 只佔總工作時間的一小部分,則表示工作在核心中花費的時間較多,可能會遭到封鎖而無法執行。這項 RFC 的目標是協助我們瞭解這段時間花費在哪些地方。

設計空間

我們的目標是回答「為什麼這個工作無法執行?」這個問題。我們必須做出幾項決定:

  1. 答案的完整度應為何?具體來說,我們應該列出工作遭到封鎖的所有原因,還是只列出幾個看似重要的原因?

  2. 我們應該以何種精細程度回答這個問題?舉例來說,我們應該只回報使用者層級可理解的簡單事件,例如「blocked on zx_channel_read」?還是應該納入 Zircon 目前實作方式特有的較低層級事件?

  3. 如果我們要回報 N 個統計資料,應該要求這些 N 個統計資料不重疊,還是允許重疊 (或許以任意方式)?

最簡單、最直接的方法,就是列舉幾個我們在意的事件,並產生這些事件的統計資料。由於媒體子系統發生頁面錯誤和核心鎖定競爭的問題,我們可能會產生「頁面錯誤所花費的時間」和「核心鎖定所花費的時間」的統計資料。

這兩項統計資料可能無法捕捉到其他潛在問題。因此,完整的解決方案很有吸引力。其中一個想法是列舉執行緒可進入核心的所有方式。這包括 N 個硬體中斷 (計時器和裝置中斷) 和 K 個軟體中斷 (系統呼叫和錯誤)。接著,我們會產生 N+K 統計資料,每種中斷類型各一個。當執行緒進入核心時,計時器就會啟動,並在控制權傳回使用者空間執行緒時停止。不過,使用者層級已可計算「在系統呼叫 X 中花費的時間」,因此讓核心計算這項資訊是多餘的。

另一個想法是產生統計資料「在核心模式下耗費的 CPU 作業時間」和「在 X 上耗費的封鎖時間」,其中 X 是一組核心原始碼,例如「核心鎖定」或「通道」。隨著核心原始碼集合隨時間變更,這個想法就會出現膨脹和流失的風險。

回顧一下,我們真正想要的是從實際裝置中擷取追蹤記錄。理想情況下,我們會持續將追蹤記錄記錄到循環緩衝區,並在觸及 TRACE_ALERT 後上傳該緩衝區。

設計

雖然完整的解決方案是理想的做法,但設計和建構這類解決方案可能需要很長的時間。我們需要立即診斷實際環境中的效能退化問題。因此,我建議新增兩個指定指標來解決當前問題:等待頁面錯誤所需的時間,以及等待核心鎖定所需的時間。

關於上述設計空間問題:

  1. 我們不會追求完整性。

  2. 精細度為任意值 (我們會記錄認為必要的任何內容)

  3. 統計資料可能會以任意方式重疊

核心變更

// This struct contains a partial breakdown of time spent by this task since
// creation. The breakdown is not complete and individual fields may overlap:
// there is no expectation that these fields should sum to an equivalent
// "wall time".
typedef struct zx_info_task_runtime {
  // Existing fields
  zx_duration_t cpu_time;
  zx_duration_t queue_time;

  // New fields below here

  // The total amount of time this task and its children spent handling page faults.
  zx_duration_t page_fault_time;

  // The total amount of time this task and its children spent waiting on contended
  // kernel locks.
  zx_duration_t lock_contention_time;

} zx_info_task_runtime_t;

這兩個欄位都會依執行緒計算,然後跨程序和工作進行加總,就像目前 cpu_timequeue_time 的做法一樣。請注意,媒體子系統不需要每個程序和每個工作的匯總資料,但為了與 zx_info_task_runtime_t 中的現有欄位保持一致,我們在此提供這項資料。

頁面錯誤和核心鎖定機制有許多種。page_fault_time 代表處理所有類型的網頁錯誤所花費的總時間。由於核心會隨著時間的推移和新架構的支援,新增或移除某些類型的頁面錯誤,因此我們會涵蓋所有頁面錯誤,以免需要說明涵蓋哪些頁面錯誤子集,這可能會很困難。

lock_contention_time 會涵蓋所有爭用鎖定。不過,我們刻意將「爭用」一詞的定義留空,讓核心隨著時間演進,以便在評估爭用情況的成本與回報爭用時間的好處之間取得平衡。如需進一步討論,請參閱「實作」一節。

使用者空間如何診斷未達期限

有了這些新欄位,使用者空間就能使用以下程式碼診斷未達期限的問題:

for (;;) {
  zx_object_get_info(current_thread, ZX_TASK_RUNTIME_INFO, &start_info, ...)
  deadline_task()
  if (current_time() > deadline) {
    zx_object_get_info(current_thread, ZX_TASK_RUNTIME_INFO, &end_info, ...)
    // ...
    // report stats from (end_info - start_info)
    // ...
  }
}

實作

page_fault_time 會計算所有頁面錯誤處理程序所需的總時間。在目前的實作中,這包括 vmm_page_fault_handlervmm_accessed_fault_handler

lock_contention_time 會計算 Mutex::AcquireContendedMutexBrwLock::Block 所需的總時間。這個方法已可存取目前的 Threadcurrent_ticks()。實作方式不會涵蓋旋轉鎖定。雖然旋轉鎖可能會發生爭用,但我們目前會忽略旋轉鎖,因為評估旋轉鎖的爭用情況可能會非常耗時。

為盡量減少額外負擔,我們會將這些時間長度記錄為刻度計數,並在 zx_object_get_info 系統呼叫期間轉換為 zx_duration_t。實作項目的其他詳細資料將遵循 cpu_timequeue_time 使用的現有模式。原型實作項目可在 fxrev.dev/469818 取得。

成效

我們會執行 Zircon 互斥鎖基準測試,確認沒有回歸現象。我們會在原始硬體 (x86 和 ARM) 上執行這些基準測試。此外,為確認虛擬化環境中沒有回歸現象,我們會在 QEMU (x86 和 ARM) 上執行這些基準測試。

回溯相容性

zx_info_task_runtime_t 結構體會進行版本控制,類似於其他 zx_info_* 結構體 (如需範例,請參閱 fxrev.dev/406754)。

安全性考量

ZX_INFO_TASK_RUNTIME 是可洩漏受檢查工作相關資訊的旁路。舉例來說,page_fault_time 可用於測量工作內存存取模式。為緩解這類外洩問題,ZX_INFO_TASK_RUNTIME 主題已要求 ZX_RIGHT_INSPECT。任何擁有該權限的使用者,都會被視為可存取工作私人資料。

ZX_INFO_TASK_RUNTIME 也可能會間接洩漏其他工作相關資訊。舉例來說,如果工作知道自己的 page_fault_time,可能就能推斷其他工作的記憶體存取模式。同樣地,如果工作知道等待爭用核心鎖所花費的時間,可能就能推斷其他工作如何使用共用核心資源。日後,我們可能會使用低解析度計時器建構 zx_info_task_runtime_t。這不一定能防止時間攻擊,但可以限制其效力。

另一種防護措施是限制 zx_info_task_runtime_t 存取特殊開發人員版本。不過,這會大幅限制這項功能的實用性:我們經常無法在開發環境中重現效能錯誤。我們需要可在正式版中啟用的解決方案。

為了完全避免這個側管道,我們需要將指標回報和指標檢查分開為不同的功能。舉例來說,如果我們持續將追蹤記錄記錄到循環緩衝區,並在觸發 TRACE_ALERT 後將該緩衝區上傳至特殊管道或埠,則觸發 TRACE_ALERT 的工作就不需要取得追蹤記錄的存取權,進而消除側邊管道。如先前所述,這類解決方案的設計和建構作業需要花費很長的時間,但我們現在需要立即解決問題。

隱私權注意事項

無。

說明文件

您需要更新 Zircon 系統呼叫說明文件,加入新的 zx_info_task_runtime_t 欄位。

既有技術與參考資料

在 Linux 中,最密切相關的先前技術為 getrusage,可回報使用者和系統 CPU 時間,以及頁面錯誤、I/O 作業和內容切換的計數。Windows 有 GetThreadTimes,可回報使用者和系統 CPU 時間。硬體效能計數器 (例如 x86 上的 RDPMC) 會提供類似的資訊,並有類似的安全性疑慮

測試

我們會透過 audio_core 記錄新的執行階段資訊,以便手動測試 (我們已為原型版本實作這項操作:請參閱 fxrev.dev/469819)。

我們會更新 cpu_timequeue_time 的現有測試,以便測試舊版 zx_info_task_runtime_t (將命名為 zx_info_task_runtime_v1_t)。此外,Zircon 的 abi_type_validator.h 也會更新,以驗證舊版和新版 ABI。這麼做可確保 ABI 回溯相容性。

為這項功能新增整合測試並不容易,因為沒有 API 可強制核心發生鎖定爭用或觸發頁面錯誤 (除了程序終止分段錯誤)。