🚀 VS Code で で入手しましょう!

CI ビルド時間の改善

2020年2月18日 Ethan Dennis, @erdennis13 および João Moreno, @joaomoreno

Visual Studio Code は、多くの可動部分と活発な参加者リストを持つ大規模なプロジェクトです。ビルドと継続的インテグレーションのインフラストラクチャを維持することにより、優れたエンジニアリングプラクティスを維持するために Azure Pipelines を積極的に使用している方法をご紹介しました。このブログ記事では、CI ビルド時間を大幅に短縮するために、Azure Pipelines Artifact Caching Tasks をどのように使用したかについて説明します。

以前のブログ記事で、VS Code が消費する node モジュールをキャッシュするカスタムビルドタスクを使用し、ビルド時にパッケージを解決する代わりに、CI ビルド時間を 33% 短縮した方法について説明しました。このパフォーマンス向上には満足していましたが、構築したキャッシュタスクをどこまでプッシュできるかを確認したいと考えました。

前回 CI エンジニアリングについて話したとき、ターゲットプラットフォームは Windows、macOS、Linux に及びました。今日現在、VS Code は、リモートサーバーコンポーネント用の Arm64 や Alpine Linux など、はるかに多様なプラットフォームをターゲットにしています。合計で 8 つの異なるターゲットがあり、すべて共通のビルドステップを共有しています。この記事では、キャッシュタスクを活用して CI の重複を減らし、ビルド時間をさらに改善する方法について概説します。

改善の余地

それでは、すべてのビルドジョブに共通するステップは正確には何だったのでしょうか?各ビルドターゲットには、同様のステップセットに従うジョブがあります。非常に大まかに言えば、各ジョブは次のことを行う必要があります。

  1. 依存関係の復元
  2. TypeScript と JavaScript の Lint
  3. TypeScript から JavaScript へのコンパイル
  4. ユニットテストスイートの実行
  5. 統合テストスイートの実行
  6. VS Code のパッケージ化

当社のキャッシュタスクは、**依存関係の復元**ステップを高速化するための明白な選択肢でした。たとえば、package-lock.json ファイルがめったに変更されないことを考えると、以前の実行結果をキャッシュできるのに、なぜ高価な npm install ステップを実行するのでしょうか?パッケージのキャッシュについては以前に説明しましたが、この投稿が興味深いのは、他のステップにキャッシュをどのように適用したかです。

Lint とコンパイルはプラットフォームに依存しないため、これらのステップは、すべてエージェントがこの作業を繰り返し実行する代わりに、結果を他のプラットフォーム依存エージェントと共有する単一のビルドエージェントによって簡単に実行できます。Linux ビルドエージェントを作成しましたが、その唯一の責任はまさにこれでした。パッケージの復元、Lint、およびソースコードのコンパイルです。私たちが行う必要があったのは、結果を他のエージェントと共有することだけでした。

すべてをキャッシュする

ビルドエージェント間でキャッシュ結果を共有するために、プラットフォームに依存しないキャッシュが必要でしたが、これは当初、キャッシュタスクではサポートされていませんでした。そのため、オプションの platformIndependent パラメータが Azure Pipelines Artifact Caching Tasks に追加されました。

VS Code が platformIndependent パラメータをどのように使用するかを以下に示します。

- task: 1ESLighthouseEng.PipelineArtifactCaching.RestoreCacheV1.RestoreCache@1
  inputs:
    keyfile: keyfile
    targetfolder: target
    vstsFeed: $(ArtifactFeed)
    platformIndependent: true

node モジュールをキャッシュする場合、package-lock.json ファイルをキャッシュキーとして使用するのが論理的です。このファイルが変更された場合、キャッシュを無効にする必要があります。コンパイル出力をキャッシュする場合、コードベース全体をキャッシュキーとして機能させる必要があります。物事を単純化するために、HEAD コミットをキャッシュキーとして使用することにしました。新しいコミットは必然的に新しいキャッシュエントリを作成するためです。単一のビルドは、ビルドエージェント間で実行されるにもかかわらず、常に単一のコミットで実行されるため、これは私たちの目的に適しています。

もう 1 つの欠落機能は、ビルドジョブごとに複数のキャッシュを作成する機能でした。現在、2 つのキャッシュ (node モジュール、コンパイル) を操作していますが、各キャッシュを個別に処理する方法はありません。キャッシュタスクは、ビルドタスクを楽観的にスキップするために使用できる 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 作業の多くを最終的に重複排除しました。

特定のユースケース (ビルドの再送信) を考えると、最後に 1 つの最適化の余地がありました。テストが不安定であったり、一部のエージェントがランダムに失敗したりする可能性があるため、以前にビルドしたコミットで 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%

VS Code before and after build times

Linux Arm および Linux Alpine ターゲットは、VS Code リモートサーバーコンポーネントのみをビルドするため、元のビルド時間は十分に良好でした。しかし、標準の VS Code クライアントプラットフォームといくつかの共通タスクを共有しているため、共通のビルドエージェントに依存させることにしました。これにより、あるケースではオーバーヘッドが増加したため、ビルド時間がわずかに増加しました。

ビルドの再送信は大幅な改善が見られました。共有エージェントタスクは完全にスキップできるためです。たとえば、macOS の数値は次のとおりです。

プラットフォーム 以前 以後 時間短縮
macOS 68 秒 34 秒 50%

全体として、VS Code の CI ビルド時間が合計で約 50% 削減されたことに非常に興奮しました。最も良いニュースは、ビルド定義からインスピレーションを得て、独自のビルド時間短縮を実現できることです。

ハッピーキャッシング、

Ethan Dennis、デベロッパーサービスシニアソフトウェアエンジニア @erdennis13

João Moreno、VS Code シニアソフトウェアエンジニア @joaomoreno