ビルドのパフォーマンスの内訳

問題を報告する ソースを表示

Bazel は複雑で、ビルドの過程でさまざまなことを行い、その一部はビルドのパフォーマンスに影響する可能性があります。このページでは、これらの Bazel のコンセプトの一部を、ビルド パフォーマンスへの影響にマッピングする方法について説明します。あまり詳しくありませんが、指標の抽出によってビルド パフォーマンスの問題を検出する方法と、それらの修正方法について説明します。これにより、ビルド パフォーマンスの回帰を調査する際にこれらのコンセプトを応用できることを期待します。

クリーンビルドと増分ビルド

クリーンビルドとは、すべてをゼロからビルドするもので、インクリメンタル ビルドは完了済みの作業を再利用します。

クリーンビルドと増分ビルドを個別に確認することをおすすめします。特に、Bazel のキャッシュの状態に依存する指標(ビルド リクエスト サイズ指標など)を収集または集計する場合は、2 つの異なるユーザー エクスペリエンスを表します。クリーンビルドをゼロから開始する場合(コールド キャッシュが原因で時間がかかる場合)と比較すると、開発者がコードを反復処理していくにつれて増分ビルドがはるかに頻繁に行われます(通常、キャッシュはすでにウォームであるため)。

BEP の CumulativeMetrics.num_analyses フィールドを使用してビルドを分類できます。num_analyses <= 1 の場合は、クリーンビルドです。そうでない場合は、大まかに増分ビルドと分類できます。ユーザーは異なるフラグやターゲットに切り替えて、実質的にクリーンなビルドにすることができます。インクリメンタリティのより厳密な定義は、たとえば、読み込まれるパッケージの数(PackageMetrics.packages_loaded)など、ヒューリスティックの形式をとる必要があります。

決定論的なビルド指標は、ビルド パフォーマンスのプロキシとして使用されます

特定の指標(非 Bazel の CPU 時間やリモート クラスタ上のキュー時間など)は非確定的であるため、ビルドのパフォーマンスを測定するのが難しい場合があります。そのため、Bazel が行う作業量の代わりとして決定論的な指標を使用すると、パフォーマンスが影響を受けやすくなります。

ビルド リクエストのサイズはビルド パフォーマンスに大きく影響する場合があります。ビルドが大きいほど、ビルドグラフの分析と構築でより多くの作業を行うことになります。ビルドの有機的成長は開発に伴うものです。依存関係の追加/作成が必要になると、その分複雑さも増し、ビルドコストも高くなります。

この問題をさまざまなビルドフェーズに分割し、各フェーズで行われる作業のプロキシ指標として次の指標を使用できます。

  1. PackageMetrics.packages_loaded: 正常に読み込まれたパッケージの数。ここでの回帰は、読み込みフェーズで追加の BUILD ファイルの読み取りと解析を行うためにより多くの作業が必要であることを示しています。

    • これは多くの場合、依存関係が追加され、推移的なクロージャを読み込む必要があるためです。
    • query / cquery を使用して、新しい依存関係が追加された場所を見つけます。
  2. TargetMetrics.targets_configured: ビルドで構成されているターゲットとアスペクトの数を表します。回帰は、構成されたターゲット グラフの作成と走査におけるより多くの作業を表します。

    • これは多くの場合、依存関係が追加され、推移的なクロージャのグラフを作成する必要があることが原因です。
    • cquery を使用して、新しい依存関係が追加された場所を見つけます。
  3. ActionSummary.actions_created: ビルドで作成されたアクションを表し、回帰はアクション グラフの構築におけるより多くの処理を表します。これには、実行されなかった未使用アクションも含まれます。

  4. ActionSummary.actions_executed: 実行されたアクションの数。回帰は、これらのアクションの実行における多くの作業を直接表します。

    • BEP は、最も実行されたアクション タイプを示すアクション統計 ActionData を書き出します。デフォルトでは上位 20 個のアクション タイプが収集されますが、--experimental_record_metrics_for_all_mnemonics を渡して、実行されたすべてのアクション タイプについてこのデータを収集できます。
    • これは、どのような種類のアクションが実行されたのかを把握する際に役立ちます(さらに)。
  5. BuildGraphSummary.outputArtifactCount: 実行されたアクションによって作成されたアーティファクトの数。

    • 実行するアクションの数が増えない場合は、ルールの実装が変更された可能性があります。

これらの指標は、すべてローカル キャッシュの状態に影響されるため、こうした指標を抽出するビルドをクリーンビルドにすることをおすすめします。

これらの指標のいずれかで回帰が発生すると、実時間、CPU 時間、メモリ使用量の回帰を伴う可能性があります。

ローカル リソースの使用

Bazel は、ローカルマシンでさまざまなリソース(ビルドグラフの分析と実行の主導、ローカル アクションの実行の両方)を消費します。これは、ビルドのパフォーマンスにおけるマシンのパフォーマンスや可用性、およびその他のタスクに影響する可能性があります。

利用時間

おそらく、ノイズを最も受けやすい指標は(ビルドごとに大きく異なる可能性があります)、特に経過時間、CPU 時間、システム時間です。bazel-bench を使用して、これらの指標のベンチマークを取得できます。十分な数の --runs を使用すると、測定値の統計的有意性を高めることができます。

  • 実時間は、実際の経過時間です。

    • 経過時間の回帰のみの場合は、JSON トレース プロファイルを収集して、差異を探すことをおすすめします。それ以外の場合は、経過時間に影響を与えた他の回帰指標を調査する方が効率的です。
  • CPU 時間は、CPU がユーザーコードを実行するのに費やした時間です。

    • CPU 時間が 2 つのプロジェクト commit 間で回帰している場合は、Starlark CPU プロファイルを収集することをおすすめします。また、--nobuild を使用して、ビルドを分析フェーズに制限することもできます。分析フェーズでは、CPU の負荷が高い作業のほとんどが実行されます。
  • システム時間とは、カーネルの CPU が費やした時間です。

    • システムの回帰が発生すると、ほとんどの場合、Bazel がファイル システムからファイルを読み取るときに I/O と相関関係があります。

システム全体の負荷のプロファイリング

Bazel 6.0 で導入された --experimental_collect_load_average_in_profiler フラグを使用すると、JSON トレース プロファイラは呼び出し中にシステム負荷の平均を収集します。

システム負荷の平均を含むプロファイル

図 1. システム負荷の平均を含むプロファイル。

Bazel の呼び出し中の負荷が高い場合は、Bazel のマシンでの並列ローカル アクションが多すぎる可能性があります。特にコンテナ環境(#16512 まで)では、--local_cpu_resources--local_ram_resources の調整を検討することをおすすめします。

Bazel メモリ使用量のモニタリング

Bazel のメモリ使用量を取得するには、主に 2 つのソース(Bazel、infoBEP)があります。

  • bazel info used-heap-size-after-gc: System.gc() を呼び出した後に使用されるメモリの量(バイト単位)。

    • Bazel ベンチでは、この指標のベンチマークも提供しています。
    • また、peak-heap-sizemax-heap-sizeused-heap-sizecommitted-heap-size もありますが(ドキュメントを参照)、関連性は低くなります。
  • BEPMemoryMetrics.peak_post_gc_heap_size: JVM ヒープのピークサイズの GC 後のバイトサイズ(GC を強制しようとする --memory_profile 設定が必要)。

メモリ使用量の回帰は、通常、ビルド リクエスト サイズの指標の回帰が原因です。これは、依存関係の追加やルール実装の変更が原因であることがよくあります。

Bazel のメモリ フットプリントをきめ細かく分析するには、ルールに組み込みの Memory Profiler を使用することをおすすめします。

永続ワーカーのメモリ プロファイリング

永続ワーカーはビルドを大幅に高速化できますが(特に解釈される言語)、メモリ フットプリントが問題になることがあります。Bazel はワーカーに関する指標を収集します。特に、WorkerMetrics.WorkerStats.worker_memory_in_kb フィールドには、ワーカーが使用しているメモリの量(ニーモニック)が表示されます。

また、JSON トレース プロファイラは、--experimental_collect_system_network_usage フラグ(Bazel 6.0 の新機能)を渡すことで、呼び出し中に永続ワーカーメモリの使用状況を収集します。

ワーカーのメモリ使用量を含むプロファイル

図 2. ワーカーのメモリ使用量を含むプロファイル。

--worker_max_instances の値を小さくする(デフォルトは 4)と、永続ワーカーが使用するメモリ量を削減できます。Google は Bazel のリソース マネージャーとスケジューラをより積極的に改善し、今後このような微調整が必要になる頻度を下げるよう取り組んでいます。

リモートビルドのネットワーク トラフィックをモニタリングする

リモート実行では、Bazel はアクションの実行の結果として作成されたアーティファクトをダウンロードします。そのため、ネットワーク帯域幅はビルドのパフォーマンスに影響する可能性があります。

ビルドのリモート実行を使用している場合は、BEPNetworkMetrics.SystemNetworkStats proto を使用して呼び出しの際にネットワーク トラフィックをモニタリングできます(--experimental_collect_system_network_usage を渡す必要があります)。

さらに、JSON トレース プロファイルでは、--experimental_collect_system_network_usage フラグ(Bazel 6.0 の新機能)を渡すことで、ビルドの過程にあるシステム全体のネットワーク使用状況を確認できます。

システム全体におけるネットワークの使用状況を含むプロファイル

図 3. システム全体のネットワーク使用状況を含むプロファイル。

リモート実行を使用する際のネットワーク使用量が多く、フラットな場合は、ネットワークがビルドのボトルネックであることを示しています。まだ使用していない場合は、--remote_download_minimal を渡して、バイトなしでビルドを有効にすることを検討してください。これにより、不要な中間アーティファクトのダウンロードを回避し、ビルドを高速化できます。

もう一つの方法は、ダウンロード帯域幅を節約するようにローカル ディスク キャッシュを構成することです。