Bazel 的平行評估和增量模型。
資料模型
資料模型由下列項目組成:
SkyValue
。又稱為節點。SkyValues
是不可變動的物件,其中包含建構程序期間建構的所有資料及輸入內容。例如輸入檔案、輸出檔案、目標和設定的目標。SkyKey
。用於參照SkyValue
的簡短不可變動名稱,例如FILECONTENTS:/tmp/foo
或PACKAGE://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 用於執行建構作業的金鑰 SkyFunction
和 SkyValue
實作項目概略摘要:
- FileStateValue。
lstat()
的結果。對於存在的檔案,該函式也會計算其他資訊,以偵測檔案的變更。這是 SkyFrame 圖中的最低層級的節點,並且沒有依附元件。 - FileValue。用於關注檔案實際內容或解析路徑的任何項目。取決於對應的
FileStateValue
和任何需要解析的符號連結 (例如a/b
的FileValue
需要a
的解析路徑以及a/b
的解析路徑)。請務必區分FileValue
和FileStateValue
之間的差別,因為在不需要檔案內容的情況下,可以使用後者。例如,評估檔案系統 glob (例如srcs=glob(["*/*.java"])
) 時,檔案內容不相關。 - DirectoryListingStateValue。
readdir()
的結果。和FileStateValue
一樣,這是最低層級的節點,沒有依附元件。 - DirectoryListingValue。用於關注目錄項目的任何內容。取決於對應的
DirectoryListingStateValue
,以及目錄的相關FileValue
。 - PackageValue。代表 BUILD 檔案的剖析版本。依附於相關聯
BUILD
檔案的FileValue
,以及任何用於解析套件 glob (在內部代表BUILD
檔案內容的資料結構) 上的DirectoryListingValue
。 - ConfiguredTargetValue 中。代表已設定的目標,這是在分析目標期間產生的一組動作,以及提供給相依設定目標的資訊。取決於對應目標所在的
PackageValue
、直接依附元件的ConfiguredTargetValues
,以及代表建構設定的特殊節點。 - ArtifactValue。代表建構作業中的檔案,可以是來源或輸出成果。成果幾乎等同於檔案,可在實際執行建構步驟時用來參照檔案。來源檔案取決於關聯節點的
FileValue
,而輸出構件取決於產生成果的任何動作的ActionExecutionValue
。 - ActionExecutionValue。代表動作的執行。取決於輸入檔案的
ArtifactValues
。系統執行的動作包含在其 SkyKey 中,與 SkyKey 應較小的概念相反。請注意,如果未執行執行階段,則未使用ActionExecutionValue
和ArtifactValue
。
這張圖表做為視覺輔助,顯示建構 Bazel 之後,SkyFunction 實作之間的關係: