在 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
  3. 将 TypeScript 编译为 JavaScript
  4. 运行单元测试套件
  5. 运行集成测试套件
  6. 打包 VS Code

我们的缓存任务是加速还原依赖项步骤的显而易见的选择。例如,既然 package-lock.json 文件很少更改,为什么还要运行耗时的 npm install 步骤,而不能缓存先前运行的结果?由于我们之前讨论过包缓存,因此本文的有趣之处在于我们如何将缓存应用于其他步骤。

由于 linting 和编译与平台无关,因此这些步骤可以轻松地由单个构建代理运行,该代理将其结果与其他平台相关的代理共享,而不是让所有代理重复执行此工作。我们创建了一个 Linux 构建代理,其唯一职责正是如此:还原包、linting 和编译源代码。我们所要做的就是与其他代理共享结果。

缓存一切

为了跨构建代理共享缓存结果,我们需要平台独立的缓存,而缓存任务最初不支持这一点。因此,向 Azure Pipelines Artifact Caching Tasks 中添加了一个可选的 platformIndependent 参数。

以下是 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 指的是哪个缓存。再次,向 Azure Pipelines Artifact Caching Tasks 中添加了另一个可选的 alias 参数。

以下是我们如何使用 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 分钟——如果缓存存在,共享代理只会放弃控制权,这完全是徒劳的。因此,Azure Pipelines Artifact Caching Tasks 中再次添加了一个新的可选 dryRun 参数,它允许我们在不还原缓存包的情况下检查其是否存在——这有效地为我们的构建重新提交节省了 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