现已推出!阅读 10 月份的新功能和修复。

改进 CI 构建时间

2020 年 2 月 18 日,作者:Ethan Dennis,@erdennis13 和 João Moreno,@joaomoreno

Visual Studio Code 是一个大型项目,包含大量移动部件和活跃的参与者列表。我们已经展示了我们如何积极使用 Azure Pipelines 来保持良好的工程实践,并维护我们的构建和持续集成基础设施。在这篇博文中,我们将讨论我们如何使用Azure Pipelines Artifact Caching 任务来显著减少我们的 CI 构建时间。

我们在之前的博文中描述了我们如何将 CI 构建时间减少了 33%。这是通过使用自定义构建任务来缓存 VS Code 使用的节点模块来实现的,而不是在构建时解析这些包。虽然我们对这种性能提升感到满意,但我们想看看我们能够将构建的缓存任务推进到什么程度。

当我们上次讨论我们的 CI 工程时,我们的目标平台涵盖了 Windows、macOS 和 Linux。截至今日,VS Code 的目标平台更加多样化,例如其远程服务器组件的 Arm64 和 Alpine Linux。总的来说,我们有八个不同的目标,它们都共享通用的构建步骤。这篇文章概述了我们如何利用缓存任务来减少 CI 重复工作,并进一步改进我们的构建时间。

改进的空间

那么,所有构建作业的通用步骤到底是什么呢?每个构建目标都有一个遵循类似步骤集的作业。在非常高的层面上,每个作业必须

  1. 还原依赖项
  2. Lint TypeScript 和 JavaScript
  3. 将 TypeScript 编译为 JavaScript
  4. 运行单元测试套件
  5. 运行集成测试套件
  6. 打包 VS Code

我们的缓存任务是加快还原依赖项步骤的明显选择。例如,为什么要运行昂贵的npm install步骤,当你可以在package-lock.json文件很少更改的情况下缓存之前运行的结果?由于我们之前已经讨论过缓存包,所以这篇文章的有趣之处在于我们如何将缓存应用于其他步骤。

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

缓存所有内容

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

以下是 VS Code 如何使用platformIndependent参数

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

在缓存节点模块时,使用package-lock.json文件作为缓存键是合理的。当此文件更改时,我们必须使缓存失效。在缓存编译输出时,整个代码库必须充当缓存键。为了简化操作,我们决定使用 HEAD 提交作为缓存键,因为新的提交必然会创建新的缓存条目。这对我们的目的来说很好,因为单个构建,尽管跨构建代理运行,但始终在单个提交上运行。

另一个缺失的功能是能够为每个构建作业创建多个缓存。现在我们发现自己正在处理两个缓存(节点模块、编译),却没有办法单独解决每个缓存。缓存任务输出一个名为CacheRestored的环境变量,可用于乐观地跳过构建任务。这个环境变量在与单个缓存交互的构建中非常有效,但在使用多个缓存时效果不佳,让我们想知道CacheRestored指的是哪个缓存。再一次,向 Azure Pipelines Artifact Caching 任务添加了另一个可选的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 任务添加了一个新的可选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