Bazel 的平行評估和增量模型。
資料模型
資料模型包含下列項目:
SkyValue
。也稱為節點。SkyValues
是不可變更的物件,包含建構期間建立的所有資料和建構的輸入內容。例如:輸入檔案、輸出檔案、目標和已設定的目標。SkyKey
。用於參照SkyValue
的簡短不可變動名稱,例如FILECONTENTS:/tmp/foo
或PACKAGE://foo
。SkyFunction
。根據節點的鍵和依附節點建構節點。- 節點圖。包含節點間相依關係的資料結構。
Skyframe
:Bazel 所依據的增量評估架構代碼名稱。
評估
建構作業包括評估代表建構要求的節點 (這是我們努力達成的狀態,但有許多舊版程式碼阻礙)。系統會先找出 SkyFunction
,然後使用頂層 SkyKey
的鍵呼叫該 SkyFunction
。接著,函式會要求評估頂層節點所需的節點,進而導致其他函式呼叫,依此類推,直到抵達葉節點為止 (通常是代表檔案系統中輸入檔案的節點)。最後,我們會得到頂層 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
或類似機制瞭解變更的檔案。在由上而下的失效期間,系統會檢查頂層節點的遞移閉包,並只保留遞移閉包乾淨的節點。如果我們知道目前的節點圖很大,但下一個建構版本只需要一小部分,這時就更適合使用這種方式:與由上而下失效不同,由下而上失效會使第一個建構版本的大型圖失效,而由上而下失效只會走訪第二個建構版本的小型圖。
我們目前只會從底部向上進行失效。
為進一步提升增量,我們使用變更修剪:如果節點失效,但重建後發現新值與舊值相同,則因這個節點變更而失效的節點會「復活」。
舉例來說,如果有人變更 C++ 檔案中的註解,從該檔案產生的 .o
檔案會相同,因此我們不需要再次呼叫連結器。
增量連結 / 編譯
這個模型的主要限制是節點失效時,會一併失效所有項目:即使有更好的演算法可根據變更項目變動節點的舊值,依附節點仍一律會從頭重建。以下列舉幾個實用範例:
- 增量連結
- 如果
.jar
中的單一.class
檔案發生變更,理論上我們可以修改.jar
檔案,而不必從頭建構。
Bazel 目前無法以有原則的方式支援這些項目 (我們對漸進式連結提供某種程度的支援,但並未在 Skyframe 中實作),原因有二:我們只獲得有限的效能提升,而且很難保證變異的結果與乾淨重建的結果相同,而 Google 重視可逐位元重複的建構作業。
到目前為止,我們一律可以分解耗費資源的建構步驟,並以這種方式達成部分重新評估,從而獲得足夠良好的效能:將應用程式中的所有類別分成多個群組,並分別對這些群組執行 Dexing。這樣一來,如果群組中的類別沒有變更,就不必重新執行 dexing。
對應至 Bazel 概念
以下是 Bazel 用來執行建構作業的部分 SkyFunction
實作項目概略總覽:
- FileStateValue。
lstat()
的結果。對於現有檔案,我們也會計算額外資訊,以便偵測檔案變更。這是 Skyframe 圖表中的最低層級節點,沒有任何依附元件。 - FileValue。任何需要檔案實際內容和/或已解析路徑的項目都會使用這個屬性。取決於對應的
FileStateValue
和任何需要解析的符號連結 (例如a/b
的FileValue
需要a
的解析路徑和a/b
的解析路徑)。FileStateValue
之間的區別很重要,因為在某些情況下 (例如評估檔案系統 glob (例如srcs=glob(["*/*.java"])
)),實際上不需要檔案內容。 - DirectoryListingValue。基本上是
readdir()
的結果。取決於與目錄相關聯的FileValue
。 - PackageValue。代表 BUILD 檔案的剖析版本。取決於相關聯
BUILD
檔案的FileValue
,以及用於解析套件中 Blob (在內部代表BUILD
檔案內容的資料結構) 的任何DirectoryListingValue
- ConfiguredTargetValue。代表已設定的目標,也就是在分析目標時產生的一組動作,以及提供給依附於此目標的已設定目標的資訊。取決於
PackageValue
對應目標所在的ConfiguredTargetValues
、直接依附元件的ConfiguredTargetValues
,以及代表建構設定的特殊節點。 - ArtifactValue。代表建構中的檔案,無論是來源或輸出成果 (成果幾乎等同於檔案,用於在實際執行建構步驟時參照檔案)。如果是來源檔案,則取決於相關聯節點的
FileValue
;如果是輸出構件,則取決於產生構件的動作ActionExecutionValue
。 - ActionExecutionValue。代表動作的執行作業。取決於輸入檔案的
ArtifactValues
。目前執行的動作包含在 sky 鍵中,這與 sky 鍵應很小的概念相反。我們正努力解決這項差異 (請注意,如果我們未在 Skyframe 上執行執行階段,則ActionExecutionValue
和ArtifactValue
未使用)。