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 は入力状態が同じであれば同じデータが返されることを保証できます。すべての Sky 関数が確定的である場合、ビルド全体も確定的になります。
- 正確で完全なインクリメンタリティ。すべての関数のすべての入力データが記録されている場合、Bazel は入力データが変更されたときに無効にする必要があるノードセットのみを無効にできます。
- 並列処理。関数は依存関係をリクエストすることでのみ相互にやり取りできるため、相互に依存しない関数は並行して実行できます。また、Bazel では、関数が順番に実行された場合と同じ結果が得られることが保証されます。
インクリメンタリティ
関数は他のノードに依存することでのみ入力データにアクセスできるため、Bazel は入力ファイルから出力ファイルまでの完全なデータフローグラフを構築し、この情報を使用して、実際に再ビルドする必要があるノード(変更された入力ファイルのセットの逆推移閉包)のみを再ビルドできます。
特に、ボトムアップとトップダウンの 2 つの増分戦略があります。どちらが最適かは、依存関係グラフの外観によって異なります。
ボトムアップ無効化では、グラフが構築され、変更された入力のセットが判明した後、変更されたファイルに間接的に依存するすべてのノードが無効になります。これは、同じトップレベル ノードを再度ビルドする場合に最適です。ボトムアップ無効化では、変更されたかどうかを判断するために、前のビルドのすべての入力ファイルに対して
stat()
を実行する必要があります。これは、inotify
などのメカニズムを使用して変更されたファイルを把握することで改善できます。トップダウン無効化中、最上位ノードの推移閉包がチェックされ、推移閉包がクリーンなノードのみが保持されます。これは、ノードグラフが大きいものの、次のビルドで必要なのは小さなサブセットのみである場合に適しています。ボトムアップ無効化では、2 番目のビルドの小さなグラフを走査するトップダウン無効化とは異なり、最初のビルドの大きなグラフが無効になります。
Bazel は、ボトムアップの無効化のみを行います。
さらなる増分化を実現するため、Bazel は変更のプルーニングを使用します。ノードが無効になっていても、再ビルド時に新しい値が古い値と同じであることが判明した場合、このノードの変更により無効になったノードは「復活」します。
これは、たとえば C++ ファイル内のコメントを変更した場合に便利です。この場合、そこから生成される .o
ファイルは同じであるため、リンカーを再度呼び出す必要はありません。
増分リンク / コンパイル
このモデルの主な制限事項は、ノードの無効化が「すべてまたはなし」であるということです。依存関係が変更されると、変更に基づいてノードの古い値を変更するより優れたアルゴリズムが存在する場合でも、依存ノードは常に最初から再ビルドされます。次のような場合に役立ちます。
- 増分リンク
- JAR ファイル内の単一のクラスファイルが変更された場合は、JAR ファイルを最初からビルドし直すのではなく、JAR ファイルをその場で変更できます。
Bazel がこれらのことを原則的にサポートしない理由は 2 つあります。
- パフォーマンスの向上は限定的でした。
- ミューテーションの結果がクリーンな再ビルドの結果と同じであることを検証するのが困難です。Google は、ビット単位で再現可能なビルドを重視しています。
これまでは、費用のかかるビルドステップを分解し、その方法で部分的な再評価を行うことで、十分なパフォーマンスを達成できました。たとえば、Android アプリでは、すべてのクラスを複数のグループに分割し、個別に dex にできます。これにより、グループ内のクラスが変更されていない場合、デコードをやり直す必要がなくなります。
Bazel コンセプトへのマッピング
以下は、Bazel がビルドの実行に使用する主な SkyFunction
と SkyValue
の実装の概要です。
- FileStateValue。
lstat()
の結果。既存のファイルの場合、関数はファイルの変更を検出するために追加情報を計算します。これは Skyframe グラフの最下位ノードであり、依存関係はありません。 - FileValue。ファイルの実際の内容や解決済みパスを気にするすべてのものに使用されます。対応する
FileStateValue
と解決が必要なシンボリック リンク(a/b
のFileValue
はa
の解決済みパスとa/b
の解決済みパスを必要とします)によって異なります。FileValue
とFileStateValue
の区別は重要です。後者は、ファイルの内容が実際に必要でない場合に使用できるためです。たとえば、ファイル システムのグロブ(srcs=glob(["*/*.java"])
など)を評価する場合、ファイルの内容は関係ありません。 - DirectoryListingStateValue。
readdir()
の結果。FileStateValue
と同様に、これは最下位レベルのノードであり、依存関係はありません。 - DirectoryListingValue。ディレクトリのエントリを気にするすべてのものに使用されます。対応する
DirectoryListingStateValue
と、ディレクトリに関連付けられたFileValue
によって異なります。 - PackageValue。BUILD ファイルの解析されたバージョンを表します。関連する
BUILD
ファイルのFileValue
に依存します。また、パッケージ内のグロブ(BUILD
ファイルの内容を内部で表すデータ構造)の解決に使用されるDirectoryListingValue
にも依存します。 - ConfiguredTargetValue。構成済みターゲットを表します。これは、ターゲットの分析中に生成された一連のアクションと、依存する構成済みターゲットに提供される情報の集合のチュープルです。対応するターゲットが含まれている
PackageValue
、直接依存関係のConfiguredTargetValues
、ビルド構成を表す特別なノードによって異なります。 - ArtifactValue。ビルド内のファイル(ソースまたは出力アーティファクト)を表します。アーティファクトはファイルとほぼ同等で、ビルドステップの実際の実行中にファイルを参照するために使用されます。ソースファイルは、関連するノードの
FileValue
に依存し、出力アーティファクトは、アーティファクトを生成するアクションのActionExecutionValue
に依存します。 - ActionExecutionValue。アクションの実行を表します。入力ファイルの
ArtifactValues
に依存します。実行されるアクションは SkyKey 内に含まれるため、SkyKey は小さくする必要があるというコンセプトに反しています。実行フェーズが実行されていない場合、ActionExecutionValue
とArtifactValue
は使用されません。
次の図は、Bazel 自体のビルド後の SkyFunction 実装間の関係を示しています。