Bazel 的平行評估和成效增幅模型。
資料模型
資料模型由下列項目組成:
SkyValue
。也稱為節點。SkyValues
是不可變動的物件, 包含建構期間建立的所有資料,以及 建構過程例如輸入檔案、輸出檔案、目標和設定 目標。SkyKey
。參照SkyValue
的簡短不可變名稱,例如FILECONTENTS:/tmp/foo
或PACKAGE://foo
。SkyFunction
:根據節點的鍵和依附節點建構節點。- 節點圖表。內含依附元件關係的資料結構 節點。
Skyframe
. Bazel 所依據的增量評估架構的程式碼名稱。
評估
建構作業包括評估代表建構要求的節點 (這是我們努力要達成的狀態,但有許多舊版程式碼會妨礙這項作業)。首先,找到其 SkyFunction
,並使用頂層 SkyKey
的鍵呼叫。接著,函式會要求評估所需的節點,以便評估頂層節點,這會導致其他函式呼叫,依此類推,直到葉節點為止 (通常是代表檔案系統中輸入檔案的節點)。最後,我們會取得頂層 SkyValue
的值、一些副作用 (例如檔案系統中的輸出檔案),以及建構作業中涉及的節點之間的依附元件有向無環圖。
如果 SkyFunction
無法事先得知需要執行工作所需的所有節點,便可在多個傳遞中要求 SkyKeys
。一個簡單的例子,就是評估輸入檔案節點後變成符號連結:函式會嘗試讀取檔案,確認檔案是符號連結,然後擷取代表符號連結目標的檔案系統節點。但這本身可以是符號連結,在這種情況下,原始函式也需要擷取目標。
在程式碼中,函式會由介面 SkyFunction
和由名為 SkyFunction.Environment
的介面提供給其提供的服務表示。函式可執行的操作如下:
- 呼叫
env.getValue
來要求對其他節點的評估。如果可用節點,系統會傳回其值,否則會傳回null
,函式本身應會傳回null
。在後者情況下,系統會評估依附的節點,然後再次叫用原始節點建構工具,但這次相同的env.getValue
呼叫會傳回非null
值。 - 呼叫
env.getValues()
可要求評估其他多個節點。這兩種方法的運作方式大致相同,差別在於依附節點會並行評估。 - 在叫用期間執行運算
- 有副作用,例如將檔案寫入檔案系統。請注意,兩個不同的函式不得互相干擾。一般來說,寫入副作用 (也就是資料從 Bazel 外流出) 並沒有問題,但由於資料是未註冊的依附元件,因此不會流入 Bazel,而未註冊依附元件則會導致這類副作用出現錯誤。
SkyFunction
實作不應透過要求依附元件的方式存取資料,而不是要求依附元件的方式 (例如直接讀取檔案系統),否則 Bazel 就不會在讀取的檔案上登錄資料依附元件,進而導致錯誤的漸進式建構作業。
函式取得足夠的資料來執行工作後,應傳回非 null
值,表示已完成。
這項評估策略有許多好處:
- 密封設計。如果函式只會依賴其他節點來要求輸入資料,Bazel 就能保證,如果輸入狀態相同,就會傳回相同的資料。如果所有天空函數具有確定性,表示整個構建同樣具有確定性。
- 正確且完美的增量。如果記錄了所有函式的所有輸入資料,Bazel 只會將輸入資料變更時需要失效的確切節點組合失效。
- 平行處理。由於函式只能透過要求依附元件的方式彼此互動,因此不仰賴彼此的函式可以平行執行,而 Bazel 可以保證結果會與依序執行的情況相同。
成效增幅
由於函式只能仰賴其他節點來存取輸入資料,因此 Bazel 可以從輸入檔案到輸出檔案來建立完整的資料流程圖,並利用這項資訊僅重建需要重新建構的節點:一組已變更的輸入檔案反向封閉式關閉。
具體來說,有兩種成效增幅策略:由下而上和由上而下。最適合的選項取決於依附元件圖的樣貌。
在自底向上的無效化期間,建構圖表並確認變更的輸入集合後,系統會將所有間接依附於變更檔案的節點設為無效。如果我們知道系統會再次建構相同的頂層節點,則這是最佳選擇。請注意,如要使「Bottom Up」撤銷,必須對先前版本的所有輸入檔案執行
stat()
,以判斷是否有異動。您可以使用inotify
或類似機制,瞭解變更檔案的情況,藉此改善此問題。在由上而下撤銷作業時,系統會檢查頂層節點的遞移性關閉情形,而且只有這些節點保有遞移性關閉情形。如果我們知道目前的節點圖表很大,但在下一個版本中只需要其中的一小部分,這會是較好的做法:自下而上的無效化會使第一個版本的大型圖表失效,而自上而下的無效化只會檢查第二個版本的小型圖表。
我們目前只執行自下而上的無效作業。
為獲得進一步的成效增幅,我們會使用「變更裁舊」功能:如果節點無效,但重新建構時發現新值與舊值相同,就會「復活」而因這個節點變更而失效的節點。
這種做法很實用,舉例來說,如果有人變更 C++ 檔案中的註解,由該檔案產生的 .o
檔案就會是相同的,因此無需再次呼叫連結器。
遞增連結/編譯
這個模型的主要限制是,節點的失效情形是全有或全無的:當依附元件變更時,依附節點一律會從頭重建,即使有更好的演算法可根據變更變更節點的舊值,也是如此。以下列舉幾個實用範例:
- 增量連結
- 當
.jar
中的單一.class
檔案變更時,我們理論上可以修改.jar
檔案,而不是從頭開始建構。
為什麼 Bazel 目前以原則方式支援這些功能 (雖然我們對於漸進式連結的支援,但並非在 SkyFrame 中實作) 卻有兩大差異:其效能提升幅度有限,也很難保證變動的結果會與乾淨重新建構的結果相同,而且 Google 的建構價值可是可重複執行。
在此之前,只要分解昂貴的建構步驟,進而進行部分重新評估,我們就能永遠達到良好的效能:將應用程式中的所有類別分割成多個群組,然後分別對這些類別進行 DEX 處理。這樣一來,如果群組中的類別沒有變更,就不需要重新執行解析作業。
對應至 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
,並且會以遞迴方式取決於用於解析套件中 glob 的任何DirectoryListingValue
(代表BUILD
檔案內容的資料結構) - ConfiguredTargetValue。代表已設定的目標,也就是在分析目標時產生的一組動作,以及提供給依賴此目標的已設定目標的資訊。取決於對應目標所在的
PackageValue
、直接依附元件的ConfiguredTargetValues
,以及代表建構設定的特殊節點。 - ArtifactValue。代表建構中的檔案,可以是來源或輸出構件 (構件幾乎等於檔案,可在實際執行建構步驟期間用於參照檔案)。對於來源檔案,取決於相關聯節點的
FileValue
;對於輸出構件,取決於產生構件的任何動作的ActionExecutionValue
。 - ActionExecutionValue。代表執行動作。取決於輸入檔案的
ArtifactValues
。目前執行的動作包含在其 Sky 鍵中,這與 Sky 鍵應小型化的概念相違。我們正在設法解決這個差異 (請注意,如未在 SkyFrame 上執行執行階段,就不會使用ActionExecutionValue
和ArtifactValue
)。