天窗

回報問題 查看來源

Bazel 的平行評估和成效增幅模型。

資料模型

資料模型由下列項目組成:

  • SkyValue。也稱為節點。SkyValues 是不可變更的物件,其中包含在建構期間建構的所有資料和建構的輸入內容。例如輸入檔案、輸出檔案、目標和設定的目標。
  • SkyKey:用於參照 SkyValue 的簡短不可變名稱,例如 FILECONTENTS:/tmp/fooPACKAGE://foo
  • SkyFunction:根據金鑰和相依節點建構節點。
  • 節點圖表。包含節點之間依附元件關係的資料結構。
  • Skyframe。漸進式評估架構 Bazel 的程式碼名稱是依據而定。

評估作業

建構作業包含評估代表建構要求的節點 (這是我們要嘗試的狀態,但目前有許多舊版程式碼)。首先,找到其 SkyFunction,並使用頂層 SkyKey 的鍵呼叫。然後函式會要求評估頂層節點所需的節點,進而產生其他函式叫用,依此類推,直到達到分葉節點 (通常代表檔案系統中的輸入檔案的節點) 為止。最後,我們最後得出頂層 SkyValue 的值、部分副作用 (例如檔案系統的輸出檔案),以及包含建構中節點之間依附元件的有向非循環圖。

如果 SkyFunction 無法事先得知需要執行工作所需的所有節點,便可在多個傳遞中要求 SkyKeys。一個簡單的例子,就是評估輸入檔案節點後變成符號連結:函式會嘗試讀取檔案,確認檔案是符號連結,然後擷取代表符號連結目標的檔案系統節點。不過,這本身可以是符號連結,在此情況下,原始函式也必須擷取其目標。

在程式碼中,函式會由介面 SkyFunction 和由名為 SkyFunction.Environment 的介面提供給其提供的服務表示。函式可執行的操作如下:

  • 呼叫 env.getValue 來要求對其他節點的評估。如果可用節點,系統會傳回其值,否則會傳回 null,函式本身應會傳回 null。在第二種情況下,系統會評估相依節點,然後再次叫用原始節點建構工具,但這次相同的 env.getValue 呼叫會傳回非 null 值。
  • 呼叫 env.getValues() 可要求評估其他多個節點。運作方式大致相同,差別在於相依節點會平行評估。
  • 在叫用期間執行運算
  • 有副作用,例如將檔案寫入檔案系統。需要注意的是,兩個不同功能之間不會介入彼此。一般來說,寫入副作用 (也就是資料從 Bazel 外流出) 並沒有問題,但由於資料是未註冊的依附元件,因此不會流入 Bazel,而沒有任何註冊依附元件,則這類副作用可能導致漸進式建構作業出錯。

SkyFunction 實作不應透過要求依附元件的方式存取資料,而不應透過要求依附元件的方式 (例如直接讀取檔案系統) 來存取,因為這會導致 Bazel 並未在讀取的檔案上登錄資料依附元件,進而導致錯誤的漸進式建構作業。

當函式累積足夠的資料可以執行工作後,就會傳回表示完成的非 null 值。

這項評估策略有許多好處:

  • 密封設計。如果函式只透過依賴其他節點的方式要求輸入資料,Bazel 就能確保輸入狀態相同時,傳回相同資料。如果所有天空函數具有確定性,表示整個構建同樣具有確定性。
  • 正確且完美的成效增幅。如果記錄了所有函式的所有輸入資料,Bazel 只會將輸入資料變更時需要失效的確切節點組合失效。
  • 平行處理工作數量由於函式只能透過要求依附元件的方式彼此互動,因此不仰賴彼此的函式可以平行執行,而 Bazel 可以保證結果會與依序執行的情況相同。

成效增幅

由於函式只能仰賴其他節點來存取輸入資料,因此 Bazel 可以從輸入檔案到輸出檔案來建立完整的資料流程圖,並利用這項資訊僅重建需要重新建構的節點:一組已變更的輸入檔案反向封閉式關閉。

具體來說,有兩種成效增幅策略:由下而上和由上而下。請根據依附關係圖的外觀,選擇一種最佳方式。

  • 在底標失效期間,建立圖表並知道變更的輸入組合後,所有節點都會因變更的檔案而失效。如果我們知道系統會再次建構相同的頂層節點,則這是最佳選擇。請注意,如要使「Bottom Up」撤銷,必須對先前版本的所有輸入檔案執行 stat(),以判斷是否有異動。您可以使用 inotify 或類似機制瞭解已變更的檔案,藉此改善檔案。

  • 在由上而下撤銷作業時,系統會檢查頂層節點的遞移性關閉情形,而且只有這些節點保有遞移性關閉情形。如果我們知道目前的節點圖表很龐大,但只需要在下個版本中只加入一小部分內容即可:採用「Bottom-up 撤銷」會讓第一個建構作業的圖表失效,這與「由上而下撤銷」不同,這與「由上而下撤銷」的情況不同,只會得出說明第二個建構作業的小圖。

我們目前只會執行由上而下無效的動作。

為獲得進一步的成效增幅,我們會使用「變更修剪」:如果節點無效,但重新建構時發現新值與舊值相同,就會「復活」而因這個節點變更而失效的節點。

這種做法很實用,舉例來說,如果有人變更 C++ 檔案中的註解,由該檔案產生的 .o 檔案就會是相同的,因此無需再次呼叫連結器。

增量連結 / 編譯

這個模型的主要限制在於,撤銷節點是萬無一失的:當依附元件變更時,依附節點一律會從頭開始重新建構,即使現有的演算法會根據異動而修改節點的舊值,仍會一律從頭開始。以下列舉幾個實用的範例:

  • 增量連結
  • .jar 中的單一 .class 檔案變更時,我們理論上可以修改 .jar 檔案,而不是從頭開始建構。

為什麼 Bazel 目前以原則方式支援這些功能 (雖然我們對於漸進式連結的支援,但並非在 SkyFrame 中實作),但這確實造成了兩大影響:其效能提升幅度有限,也很難保證變動的結果會與純粹重新建構的結果相同,而且 Google 的建構價值為能夠重複使用的位元。

截至目前,我們只要分解昂貴的建構步驟,進而進行部分重新評估,就能達到良好的效能。這種做法可將應用程式中的所有類別分割成多個群組,然後分別對這些類別執行 DEX 處理。這樣一來,如果群組中的類別不會變更,就不必重做 DEX 處理。

對應 Bazel 概念

以下概略說明 Bazel 用來執行建構作業的部分 SkyFunction 實作項目:

  • FileStateValuelstat() 的結果。對於存在的檔案,我們也會計算其他資訊,以偵測檔案變更。這是 SkyFrame 圖表中最低層級的節點,沒有依附元件。
  • FileValue。用於任何關注檔案實際內容和/或解析路徑的項目。依附於對應的 FileStateValue 和所有需要解析的符號連結 (例如 a/bFileValue 需要 a 的解析路徑和 a/b 的解析路徑)。FileStateValue 之間的差異十分重要,因為在某些情況下 (例如評估檔案系統 glob,例如 srcs=glob(["*/*.java"])) 實際上不需要檔案內容。
  • DirectoryListingValue。基本上就是 readdir() 的結果。取決於與目錄相關聯的 FileValue
  • PackageValue。代表 BUILD 檔案的剖析版本。依附於相關聯 BUILD 檔案的 FileValue,以及任何用於解析套件中 glob 的 DirectoryListingValue (在內部代表 BUILD 檔案內容的資料結構)
  • ConfiguredTargetValue。代表已設定的目標,這是分析目標時產生的一組動作組合,以及提供給依靠此目標所設定目標的資訊。取決於對應目標所在的 PackageValue、直接依附元件的 ConfiguredTargetValues,以及代表建構設定的特殊節點。
  • ArtifactValue。代表建構中的檔案,可以是來源或輸出構件 (構件幾乎等於檔案,可在實際執行建構步驟期間用於參照檔案)。如為來源檔案,則會依附關聯節點的 FileValue,輸出構件時則取決於任何動作產生成果的 ActionExecutionValue
  • ActionExecutionValue。代表動作的執行。取決於其輸入檔案的 ArtifactValues。系統執行的動作目前包含在天空索引鍵中,這與天空金鑰應該很小的概念不同。我們正在設法解決這個差異 (請注意,如未在 SkyFrame 上執行執行階段,就不會使用 ActionExecutionValueArtifactValue)。