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 ファイルをその場で変更できます。
Bazel がこれらのことを原則としてサポートしていない理由は 2 つあります。
- パフォーマンスの向上は限定的でした。
- ミューテーションの結果がクリーンな再ビルドの結果と同じであることを検証することが難しく、Google はビット単位で再現可能なビルドを重視しています。
これまで、コストの高いビルドステップを分解し、部分的な再評価を行うことで、十分なパフォーマンスを実現できました。たとえば、Android アプリでは、すべてのクラスを複数のグループに分割して、それぞれを個別に dex できます。これにより、グループ内のクラスが変更されていない場合、dexing をやり直す必要がなくなります。
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 実装間の関係を示しています。