天窗

回報問題 查看原始碼

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 的情況下流向 Bazel,所以會造成這類副作用,因為它們屬於未註冊的依附元件,因此可能會造成錯誤的漸進式建構作業。

正確實作 SkyFunction 可避免存取資料而非要求依附元件 (例如直接讀取檔案系統) 來存取資料,因為這會導致 Bazel 無法在已讀取的檔案上註冊資料依附元件,進而導致錯誤的漸進式建構作業。

當函式取得足夠的資料來執行工作後,應會傳回表示已完成的非 null 值。

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

  • 密封度。如果函式只會根據其他節點來要求輸入資料,Bazel 可以保證在輸入狀態相同時,會傳回相同的資料。如果所有天空函式具有確定性,這代表整個建構作業也是確定性。
  • 正確且完美的成效增幅。如果記錄了所有函式的所有輸入資料,Bazel 只能將輸入資料變更時需要撤銷的確切節點組撤銷。
  • 平行處理工作數量:由於函式只能藉由要求依附元件的方式彼此互動,因此不依附的函式可以平行執行,且 Bazel 可以確保結果與依序執行時相同。

成效增幅

由於函式只能根據其他節點來存取輸入資料,因此 Bazel 能從輸入檔案到輸出檔案來建立完整的資料流程圖,並使用這項資訊只重新建立確實需要重建的節點:對經過變更的輸入檔案集的反向轉換性關閉。

具體而言,有兩種可能的成效增幅策略:由下往上和由上往下的策略。以下哪種做法最適合取決於依附元件圖表的外觀。

  • 在由下往上撤銷期間,在建構圖形且已知有變更的輸入組合後,所有節點都會失效,而以遞移依附於變更的檔案。當再次建構同一個頂層節點時,這是最佳做法。請注意,由下而失效的功能必須在先前版本的所有輸入檔案上執行 stat(),以判斷這些檔案是否已變更。您可以使用 inotify 或類似機制,瞭解已變更檔案,也可以改善這項作業。

  • 在由上往下撤銷的情況下,系統會檢查頂層節點的遞移性閉合,並且只會保留這些節點的遞移性關閉。如果節點圖較大,但下一項建構作業只需要一小部分的圖表,這種做法會比較好:與由上而下撤銷的不同,它僅僅遵循第二個建構作業的小型圖形,會使第一個建構作業的較大圖形失效。

Bazel 只會執行由下而撤銷的無效作業。

為了進一步提升增幅,Bazel 使用「變更縮減」功能:如果節點無效,但進行重新建構時,發現其新值與舊值相同,就會因這個節點的變更而失效的節點「重新保留」。

舉例來說,如果其中一個程式碼變更 C++ 檔案中的註解,該註解產生的 .o 檔案會相同,因此您不必再次呼叫連接器。

連結增量 / 編譯

這個模型的主要限制是,節點撤銷是完全或完全不存在的事務:當依附元件變更時,依附元件節點一律會從頭開始重建,即使存在更佳的演算法可根據變更調整節點舊值。以下提供幾個實用的範例:

  • 增量連結
  • 當 JAR 檔案中的單一類別檔案變更時,您可以直接修改 JAR 檔案,不必重新建構。

Bazel 為何無法透過原則支援這些機制,原因如下:

  • 成效提升幅度不大。
  • 難以驗證異動結果是否與乾淨的重新建構作業相同,且 Google 的值能夠隨位元重複建構。

目前,透過解開昂貴的建構步驟,並以這種方式重新進行部分重新評估,可以達到足夠的效能。舉例來說,在 Android 應用程式中,您可以將所有類別拆分為多個群組,並分別執行 DEX 處理。如此一來,如果群組中的類別並未變更,DEX 就不需要重做。

對應至 Bazel 概念

以下是 Bazel 用於執行建構作業的金鑰 SkyFunctionSkyValue 實作項目概略摘要:

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

這張圖表做為視覺輔助,顯示建構 Bazel 之後,SkyFunction 實作之間的關係:

SkyFunction 實作關係圖