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()
を呼び出して、他の複数のノードの評価をリクエストします。依存ノードが並行して評価される点を除き、基本的には同じです。- 呼び出し時に計算を行う
- ファイル システムへのファイルの書き込みなど、副作用が生じる。2 つの異なる関数が互いに足を踏み合わせないように注意する必要があります。一般に、書き込みの副作用(Bazel から外部にデータが流れる場合)は問題ありませんが、読み取りの副作用(データが登録された依存関係なしで Bazel に流入する)は問題ありません。依存関係が登録されていないため、誤った増分ビルドが発生する可能性があります。
適切に動作した SkyFunction
の実装により、依存関係のリクエスト以外の方法(ファイル システムを直接読み取るなど)でデータにアクセスすることを回避できます。これは、読み取られたファイルに対するデータ依存関係を Bazel が登録せず、増分ビルドが正しく行われないためです。
関数がジョブの実行に十分なデータを取得したら、完了を示す null
以外の値を返す必要があります。
この評価戦略には、いくつかの利点があります。
- 密閉性。関数が他のノードに依存する方法のみによって入力データをリクエストする場合、Bazel は、入力状態が同じであれば同じデータが返されることを保証できます。空の関数がすべて確定的である場合、ビルド全体も確定的になります。
- 正確かつ完全なインクリメンタリティすべての関数のすべての入力データが記録されている場合、Bazel は、入力データの変更時に無効化する必要があるノードのセットのみを無効にできます。
- 並列処理。関数は依存関係のリクエストによってのみ相互にやり取りできるため、相互に依存しない関数は並列に実行でき、Bazel は、順番に実行した場合と同じ結果を保証できます。
インクリメンタリティ
関数は他のノードに依存することによってのみ入力データにアクセスできるため、Bazel は、入力ファイルから出力ファイルまでの完全なデータフロー グラフを構築し、この情報を使用して実際に再構築が必要なノードのみを再ビルドできます。
具体的には、ボトムアップ戦略とトップダウン戦略の 2 つのインクリメンタリティ戦略が存在します。どちらが最適かは、依存関係グラフがどのように表示されるかによって異なります。
ボトムアップの無効化では、グラフが作成され、変更された入力のセットが判明した後、変更されたファイルに推移的に依存するすべてのノードが無効になります。これは、同じトップレベル ノードを再度構築する場合に最適です。ボトムアップの無効化では、以前のビルドのすべての入力ファイルで
stat()
を実行して、変更済みかどうかを確認する必要があります。これは、inotify
または同様のメカニズムを使用して変更されたファイルを知ることで改善できます。トップダウンの無効化の際に、トップレベル ノードの推移的クロージャがチェックされ、推移的クロージャがクリーンなノードのみが保持されます。これは、ノードグラフが大きいものの、次のビルドで必要なのはごく一部の場合です。ボトムアップの無効化では、最初のビルドの大きなグラフが無効になります。これとは異なり、トップダウンの無効化では 2 番目のビルドの小さなグラフを見るだけです。
Bazel はボトムアップの無効化のみを行います。
さらにインクリメンタリティを高めるために、Bazel は変更プルーニングを使用します。ノードが無効化されたものの、再ビルド時に新しい値が古い値と同じであることが判明した場合、このノードの変更により無効化されたノードが「復活」します。
これは、たとえば C++ ファイルのコメントを変更した場合、そのコメントから生成される .o
ファイルは同じになるため、リンカーを再度呼び出す必要はありません。
増分リンク / コンパイル
このモデルの主な制限は、ノードの無効化がオール オア ナッシングであることです。依存関係が変更されると、変更に基づいてノードの古い値を変更する優れたアルゴリズムがあっても、依存ノードは常にゼロから再構築されます。これが有用な例をいくつか示します。
- 増分リンク
- JAR ファイル内の単一のクラスファイルが変更された場合、JAR ファイルを最初から作成し直すのではなく、インプレースで変更できます。
Bazel が原則的に上記の処理をサポートしていない理由は次の 2 つです。
- パフォーマンスの向上は限定的でした。
- ミューテーションの結果がクリーンな再ビルドの結果と同じであることを検証するのは困難であり、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 の解決に使用されるDirectoryListingValue
(BUILD
ファイルのコンテンツを内部で表すデータ構造)にも推移的に依存します。 - ConfiguredTargetValue。構成済みのターゲットを表します。これは、ターゲットの分析中に生成された一連のアクションと、依存する構成済みターゲットに提供される情報のタプルです。
PackageValue
に応じて、対応するターゲットが存在する場所、直接依存関係のConfiguredTargetValues
、ビルド構成を表す特別なノードになります。 - ArtifactValue。ビルド内のファイル(ソースまたは出力アーティファクト)を表します。アーティファクトはファイルとほぼ同等で、ビルドステップの実際の実行時にファイルを参照するために使用されます。ソースファイルは関連するノードの
FileValue
に依存し、出力アーティファクトはアーティファクトを生成するアクションのActionExecutionValue
に依存します。 - ActionExecutionValue。アクションの実行を表します。入力ファイルの
ArtifactValues
によって異なります。実行するアクションは SkyKey 内に含まれます。これは、SkyKey を小さくする必要があるという概念に反します。なお、実行フェーズを実行しない場合、ActionExecutionValue
とArtifactValue
は使用されません。
次の図に、Bazel 自体をビルドした後の SkyFunction 実装の関係を示します。