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
などのメカニズムを使用して、変更されたファイルを確認することによって改善できます。トップダウンの無効化中に、トップレベル ノードの推移的クロージャがチェックされ、推移的クロージャがクリーンなノードのみが保持されます。これは、ノードグラフが大きい場合に適していますが、次のビルドで必要なのはごく一部です。ボトムアップの無効化では、1 番目のビルドの大きいグラフが無効になりますが、トップダウンの無効化では、2 番目のビルドの小さなグラフのみをたどります。
Bazel はボトムアップの無効化のみを行います。
Bazel では、さらにインクリメンタリティを高めるために変更プルーニングが使用されます。ノードが無効化されたものの、再ビルド時に新しい値が古い値と同じであることが判明した場合、そのノードの変更により無効化されたノードが「復活」されます。
これは、たとえば C++ ファイルのコメントを変更した場合に、そこから生成される .o
ファイルは同じになるため、リンカーを再度呼び出す必要はありません。
増分リンク / コンパイル
このモデルの主な制限は、ノードの無効化がオール オア ナッシングであることです。依存関係が変更されると、変更に基づいてノードの古い値を変更する優れたアルゴリズムが存在しても、依存ノードは常にゼロから再構築されます。これが有用な例を次に示します。
- 増分リンク
- JAR ファイルで 1 つのクラスファイルが変更された場合、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 実装の関係を示しています。