CIビルド時間の改善
2020年2月18日 Ethan Dennis (@erdennis13) および João Moreno (@joaomoreno)
Visual Studio Code は、多くの可動部分と活発な参加者リストを持つ大規模なプロジェクトです。私たちは、ビルドおよび継続的インテグレーションのインフラストラクチャを維持することで、優れたエンジニアリングプラクティスを維持するために Azure Pipelines を積極的に使用していることを示してきました。このブログ記事では、Azure Pipelines Artifact Caching Tasks を使用して CI ビルド時間を大幅に短縮した方法について説明します。
以前のブログ記事で、CI ビルド時間を33%削減した方法について説明しました。これは、ビルド時にパッケージを解決するのではなく、VS Code が使用するノードモジュールをキャッシュするカスタムビルドタスクを使用することで達成されました。このパフォーマンス向上に満足していましたが、構築したキャッシングタスクをさらにどれだけ推し進めることができるかを確認したいと考えていました。
前回 CI エンジニアリングについて話したとき、対象プラットフォームは Windows、macOS、Linux に及んでいました。現在、VS Code は、そのリモートサーバーコンポーネント向けに Arm64 や Alpine Linux など、はるかに多様なプラットフォームをターゲットとしています。合計で、すべての共通ビルドステップを共有する8つの異なるターゲットがあります。この記事では、キャッシングタスクを活用して CI の重複を減らし、ビルド時間をさらに改善した方法について概説します。
改善の余地
では、すべてのビルドジョブに共通するステップとは正確には何だったのでしょうか? 各ビルドターゲットには、同様の一連のステップに従うジョブがあります。大まかに言えば、各ジョブは次のことを行う必要があります。
- 依存関係の復元
- TypeScript と JavaScript のLint
- TypeScript を JavaScript にコンパイル
- 単体テストスイートの実行
- 統合テストスイートの実行
- VS Code のパッケージ化
キャッシングタスクは、依存関係の復元ステップを高速化するための明白な選択肢でした。たとえば、`package-lock.json` ファイルがめったに変更されないことを考えると、以前の実行結果をキャッシュできるのに、なぜコストのかかる `npm install` ステップを実行するのでしょうか? 以前にパッケージのキャッシュについて説明しましたが、この記事の興味深い点は、他のステップにキャッシュを適用した方法です。
Lint とコンパイルはプラットフォームに依存しないため、すべてのエージェントがこの作業を繰り返し実行するのではなく、単一のビルドエージェントがその結果を他のプラットフォーム依存のエージェントと共有することで、それらのステップを簡単に実行できます。私たちは、パッケージの復元、Lint、ソースコードのコンパイルという唯一の責任を持つ Linux ビルドエージェントを作成しました。私たちがしなければならなかったのは、その結果を他のエージェントと共有することだけでした。
すべてをキャッシュする
ビルドエージェント間でキャッシュ結果を共有するために、プラットフォームに依存しないキャッシュが必要でしたが、これは当初キャッシングタスクではサポートされていませんでした。そこで、オプションの `platformIndependent` パラメーターが Azure Pipelines Artifact Caching Tasks に追加されました。
VS Code が `platformIndependent` パラメーターを使用する方法を次に示します。
- task: 1ESLighthouseEng.PipelineArtifactCaching.RestoreCacheV1.RestoreCache@1
inputs:
keyfile: keyfile
targetfolder: target
vstsFeed: $(ArtifactFeed)
platformIndependent: true
ノードモジュールをキャッシュする場合、`package-lock.json` ファイルをキャッシュキーとして使用するのは論理的です。このファイルが変更された場合、キャッシュを無効にする必要があります。コンパイル出力をキャッシュする場合、コードベース全体がキャッシュキーとして機能する必要があります。物事を単純化するために、新しいコミットは必然的に新しいキャッシュエントリを作成するため、HEAD コミットをキャッシュキーとして使用することにしました。ビルドエージェント間で実行されるにもかかわらず、単一のビルドは常に単一のコミット上で実行されるため、これは私たちの目的に合致しています。
もう1つの欠けていた機能は、ビルドジョブごとに複数のキャッシュを作成する機能でした。私たちは現在、個々のキャッシュにアクセスする方法がないまま、2つのキャッシュ (ノードモジュール、コンパイル) をやりくりしていることに気づきました。キャッシングタスクは、ビルドタスクを最適にスキップするために使用できる `CacheRestored` という環境変数を出力します。この環境変数は、単一のキャッシュと対話するビルドではうまく機能しますが、複数のキャッシュではそれほどではないため、`CacheRestored` がどのキャッシュを参照しているのか疑問に思いました。再び、オプションの `alias` パラメーターが Azure Pipelines Artifact Caching Tasks に追加されました。
そして、`alias` パラメーターの使用方法を次に示します。
- task: 1ESLighthouseEng.PipelineArtifactCaching.RestoreCacheV1.RestoreCache@1
inputs:
keyfile: "yarn.lock"
targetfolder: "node_modules"
vstsFeed: "$(ArtifactFeed)"
alias: "Packages"
- script: |
yarn install
displayName: Install Dependencies
condition: ne(variables['CacheRestored-Packages'], 'true')
ここでは、`Packages` というエイリアスが環境変数出力に追加され、単一のビルドジョブで NPM パッケージとコンパイル出力をキャッシュできるようになりました。これで、一度だけ実行してプラットフォーム固有のエージェント間で共有できる CI 作業の多くがようやく重複排除されました。
特定のユースケース、つまりビルドの再提出に関して、まだ最終的な最適化の余地がありました。テストが不安定であったり、一部のエージェントがランダムに失敗したりする場合があるため、以前にビルドされたコミットで VS Code ビルドを再トリガーする必要があることがあります。理想的には、共有エージェントは共通コードを復元または再コンパイルせず、プラットフォーム依存のエージェントにその作業を任せます。私たちが気づいた問題は、コンパイルキャッシュパッケージが非常に大きく、それらを復元するのに約8分かかるということでした。共有エージェントはそのキャッシュが存在すれば単に制御を譲るため、すべてが無駄でした。そこで、新しいオプションの `dryRun` パラメーターが Azure Pipelines Artifact Caching Tasks に再び追加されました。これにより、キャッシュパッケージの存在を復元せずに確認できるようになり、ビルドの再提出から効果的に8分短縮されました。
ビルドで `dryRun` パラメーターを使用すると、次のようになります。
- task: 1ESLighthouseEng.PipelineArtifactCaching.RestoreCacheV1.RestoreCache@1
inputs:
keyfile: commit
targetfolder: output
vstsFeed: "$(ArtifactFeed)"
dryRun: true
- script: |
npm run compile install
displayName: Install Dependencies
condition: ne(variables['CacheExists'], 'true')
これにより、`dryRun` パラメーターと連携する新しい `CacheExists` 変数も導入されたことに注目してください。
結果
これらの変更が実装されると、総ビルド時間が大幅に短縮されました。以下の表は、VS Code がターゲットとする各プラットフォームの総ビルド時間の変化を示しています。
プラットフォーム | 以前 | 以後 | 時間短縮 |
---|---|---|---|
Windows | 58分 | 44分 | 24% |
Windows 32 | 59分 | 46分 | 22% |
Linux | 38分 | 23分 | 39% |
macOS | 68分 | 42分 | 38% |
Linux Arm | 22分 | 21分 | 5% |
Linux Alpine | 23分 | 26分 | -13% |
Linux Arm および Linux Alpine ターゲットは、VS Code リモートサーバーコンポーネントのみをビルドするため、元のビルド時間は十分でした。しかし、標準の VS Code クライアントプラットフォームと一部の共通タスクを共有しているため、それらを共通ビルドエージェントに依存させることにしました。これにより、1つのケースでオーバーヘッドが増加したため、ビルド時間がわずかに増加しました。
共有エージェントタスクを完全にスキップできるため、ビルドの再提出は劇的に改善されました。たとえば、macOS の数値は次のとおりです。
プラットフォーム | 以前 | 以後 | 時間短縮 |
---|---|---|---|
macOS | 68秒 | 34秒 | 50% |
全体として、VS Code の CI ビルド時間が合わせて約50%短縮されたことを非常に嬉しく思います!最も良いニュースは、私たちのビルド定義からヒントを得て、ご自身のビルド時間の改善を実現できることです。
ハッピーキャッシング、
Ethan Dennis、開発者サービス シニアソフトウェアエンジニア @erdennis13
João Moreno、VS Code シニアソフトウェアエンジニア @joaomoreno