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