VS Codeのエージェントモードを拡張するには、を試してください!

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が消費するnodeモジュールをキャッシュするカスタムビルドタスクを使用することで達成されました。このパフォーマンス向上には満足していましたが、私たちが構築したキャッシュタスクをどこまで押し進めることができるかを確認したかったのです。

前回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作業の多くを重複排除し、一度だけ実行してプラットフォーム固有のエージェント間で共有できるようになりました。

特定のユースケース、つまりビルドの再送信を考えると、最後の最適化の余地がまだありました。テストが不安定であったり、一部のエージェントがランダムに失敗したりするため、以前にビルドされたコミットで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%削減されたことを大変嬉しく思います!最良のニュースは、私たちのビルド定義からインスピレーションを得て、独自のビルド時間改善を実現できることです。

Happy Caching,

Ethan Dennis、Developer Services シニアソフトウェアエンジニア @erdennis13

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