尝试以扩展 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。总共有八个不同的目标平台共享通用的构建步骤。本文概述了我们如何利用缓存任务来减少 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 提交作为缓存键,因为一个新的提交必然会创建一个新的缓存条目。这对于我们的目的来说很好,因为单个构建,尽管在不同的构建代理上运行,但总是基于单个提交运行。

另一个缺失的功能是为每个构建作业创建多个缓存的能力。我们发现自己要处理两个缓存(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')

请注意,这也引入了一个新的 CacheExists 变量,它与 dryRun 参数协同工作。

结果

这些更改实施后,我们看到总构建时间大幅减少。下表显示了 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