天窗

回報問題 查看來源 Nightly · 8.3 · 8.2 · 8.1 · 8.0 · 7.6

Bazel 的平行評估和增量模型。

資料模型

資料模型包含下列項目:

  • SkyValue。也稱為節點。SkyValues 是不可變更的物件,包含建構期間建立的所有資料和建構的輸入內容。例如:輸入檔案、輸出檔案、目標和已設定的目標。
  • SkyKey。用於參照 SkyValue 的簡短不可變動名稱,例如 FILECONTENTS:/tmp/fooPACKAGE://foo
  • SkyFunction。根據節點的鍵和依附節點建構節點。
  • 節點圖。包含節點間相依關係的資料結構。
  • Skyframe:Bazel 所依據的增量評估架構代碼名稱。

評估

評估代表建構要求的節點,即可完成建構。

首先,Bazel 會找出與頂層 SkyKey 的鍵對應的 SkyFunction。接著,函式會要求評估頂層節點所需的節點,進而產生其他 SkyFunction 呼叫,直到抵達葉節點為止。葉節點通常代表檔案系統中的輸入檔案。最後,Bazel 會取得頂層 SkyValue 的值、一些副作用 (例如檔案系統中的輸出檔案),以及建構所涉及節點之間的依附元件有向非循環圖。

如果 SkyFunction 無法預先判斷執行工作所需的所有節點,可以透過多個階段要求 SkyKeys。舉例來說,評估結果為符號連結的輸入檔案節點時,函式會嘗試讀取檔案,發現檔案是符號連結,因此會擷取代表符號連結目標的檔案系統節點。但這本身可以是符號連結,在這種情況下,原始函式也需要擷取目標。

程式碼中的函式會以 SkyFunction 介面表示,並由名為 SkyFunction.Environment 的介面提供服務。函式可執行下列動作:

  • 呼叫 env.getValue,要求評估另一個節點。如果節點可用,則會傳回其值,否則會傳回 null,且函式本身應會傳回 null。在後者中,系統會評估依附節點,然後再次叫用原始節點建構函式,但這次相同的 env.getValue 呼叫會傳回非 null 值。
  • 呼叫 env.getValues(),要求評估多個其他節點。 這項作業的本質與上述相同,但會平行評估依附節點。
  • 在呼叫期間執行運算
  • 有副作用,例如將檔案寫入檔案系統。請務必注意,兩個不同的函式應避免互相干擾。一般來說,寫入副作用 (資料從 Bazel 向外流動) 是可以的,讀取副作用 (資料在沒有已註冊的依附元件的情況下流入 Bazel) 則不行,因為這類副作用是未註冊的依附元件,因此可能會導致增量建構作業不正確。

行為良好的 SkyFunction 實作項目會避免以要求依附元件以外的任何方式存取資料 (例如直接讀取檔案系統),因為這樣會導致 Bazel 無法在讀取的檔案上註冊資料依附元件,進而導致增量建構作業不正確。

函式有足夠的資料可執行工作後,應傳回非 null 值,表示工作完成。

這項評估策略有許多優點:

  • 密封性。如果函式只會透過依附其他節點來要求輸入資料,Bazel 就能保證,只要輸入狀態相同,傳回的資料就會相同。如果所有 Sky 函式都是決定性函式,表示整個建構作業也會是決定性作業。
  • 正確且完美的升幅。如果記錄所有函式的輸入資料,當輸入資料變更時,Bazel 就只會使需要失效的節點集失效。
  • 平行處理。由於函式只能透過要求依附元件的方式彼此互動,因此不互為依附元件的函式可以平行執行,而 Bazel 可確保結果與循序執行時相同。

成效增幅

由於函式只能依附於其他節點來存取輸入資料,因此 Bazel 可以從輸入檔案建構完整的資料流圖,直到輸出檔案為止,並使用這項資訊只重建實際需要重建的節點:變更輸入檔案集的反向遞移閉包。

具體來說,有兩種可能的增量策略:由下而上和由上而下。最佳選項取決於相依性圖表的外觀。

  • 在由下而上的失效期間,系統會建構圖表並瞭解變更的輸入內容集後,使所有以遞移方式依附於變更檔案的節點失效。如果會再次建構相同的頂層節點,這是最佳做法。請注意,如要進行由下而上的失效作業,必須在先前建構作業的所有輸入檔案上執行 stat(),判斷檔案是否已變更。使用 inotify 或類似機制瞭解變更的檔案,即可改善這項問題。

  • 在由上而下的失效期間,系統會檢查頂層節點的遞移閉包,並只保留遞移閉包乾淨的節點。如果節點圖很大,但下一個建構作業只需要一小部分,這時就比較適合使用這種方式:與由上而下失效不同,由下而上失效會使第一個建構作業的較大圖失效,而由上而下失效只會走訪第二個建構作業的小圖。

Bazel 只會從下而上進行失效。

為進一步提升增量,Bazel 會使用變更修剪:如果節點失效,但重建後發現新值與舊值相同,則因這個節點變更而失效的節點會「復活」。

舉例來說,如果有人變更 C++ 檔案中的註解,從該檔案產生的 .o 檔案會相同,因此不需要再次呼叫連結器。

增量連結 / 編譯

這個模型的主要限制是節點失效時,會一併失效所有節點:當依附元件變更時,即使有更好的演算法可根據變更內容變動節點的舊值,依附節點仍一律會從頭重建。以下列舉幾種適合使用這種做法的情況:

  • 增量連結
  • 如果 JAR 檔案中只有一個類別檔案變更,您可以就地修改 JAR 檔案,不必從頭建構。

Bazel 不支援這些項目的原因有二:

  • 效能提升有限。
  • 難以驗證變異結果是否與乾淨重建的結果相同,而 Google 重視可逐位元重複的建構作業。

到目前為止,只要分解成本高昂的建構步驟,並以這種方式達成部分重新評估,就能獲得足夠的效能。舉例來說,在 Android 應用程式中,您可以將所有類別分成多個群組,並分別進行 Dex 處理。這樣一來,如果群組中的類別沒有變更,就不必重新執行 dexing。

對應至 Bazel 概念

以下是 Bazel 用來執行建構作業的關鍵 SkyFunctionSkyValue 實作項目概要:

  • FileStateValuelstat() 的結果。如果是現有檔案,函式也會計算額外資訊,以便偵測檔案變更。這是 Skyframe 圖中的最低層級節點,沒有任何依附元件。
  • FileValue。任何需要檔案實際內容或已解析路徑的項目都會使用這個介面。取決於對應的 FileStateValue 和任何需要解析的符號連結 (例如 a/bFileValue 需要 a 的解析路徑和 a/b 的解析路徑)。FileValueFileStateValue 的區別很重要,因為後者可用於實際不需要檔案內容的情況。舉例來說,評估檔案系統 glob (例如 srcs=glob(["*/*.java"])) 時,檔案內容並不重要。
  • DirectoryListingStateValue。「readdir()」的結果。與 FileStateValue 類似,這是最低層級的節點,沒有任何依附元件。
  • DirectoryListingValue。任何需要目錄項目的項目都會使用這個函式。取決於對應的 DirectoryListingStateValue,以及目錄的相關 FileValue
  • PackageValue。代表 BUILD 檔案的剖析版本。取決於相關聯 BUILD 檔案的 FileValue,以及用於解析套件中 Glob 的任何 DirectoryListingValue (代表 BUILD 檔案內容的內部資料結構)。
  • ConfiguredTargetValue。代表已設定的目標,也就是在分析目標期間產生的一組動作,以及提供給相依已設定目標的資訊。取決於相應目標所在的 PackageValue、直接依附元件的 ConfiguredTargetValues,以及代表建構設定的特殊節點。
  • ArtifactValue。代表建構作業中的檔案,無論是來源或輸出構件。構件幾乎等同於檔案,用於在實際執行建構步驟時參照檔案。來源檔案取決於相關聯節點的 FileValue,輸出構件則取決於產生構件的任何動作的 ActionExecutionValue
  • ActionExecutionValue。代表動作的執行作業。取決於輸入檔案的 ArtifactValues。執行的動作包含在 SkyKey 中,這與 SkyKey 應很小的概念相反。請注意,如果執行階段未執行,系統就不會使用 ActionExecutionValueArtifactValue

為方便瞭解,下圖顯示 Bazel 本身建構後,SkyFunction 實作項目之間的關係:

SkyFunction 實作關係圖