天窗

回報問題 查看原始碼 Nightly · 8.0 7.4 . 7.3 · 7.2 · 7.1 · 7.0 · 6.5

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 應用程式中,您可以將所有類別分割為多個群組,並分別將這些群組解析。這樣一來,如果群組中的類別沒有變更,就不需要重新執行解析作業。

對應至 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 的任何 DirectoryListingValue (代表 BUILD 檔案內容的內部資料結構)。
  • ConfiguredTargetValue。代表已設定的目標,也就是在分析目標時產生的動作集合,以及提供給依附已設定目標的資訊的元組。取決於對應目標所在的 PackageValue、直接依附元件的 ConfiguredTargetValues,以及代表建構設定的特殊節點。
  • ArtifactValue。代表建構中的檔案,無論是來源或輸出構件。構件幾乎等同於檔案,可用於在實際執行建構步驟時參照檔案。來源檔案會依附相關聯節點的 FileValue,而輸出構件會依附產生構件的任何動作的 ActionExecutionValue
  • ActionExecutionValue。代表執行動作。取決於輸入檔案的 ArtifactValues。執行的動作包含在 SkyKey 中,這與 SkyKey 應小型化的概念相違。請注意,如果未執行執行階段,ActionExecutionValueArtifactValue 就不會使用。

下圖為視覺輔助工具,顯示 Bazel 本身建構後,SkyFunction 實作項目之間的關係:

SkyFunction 實作關係圖