改进 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 重复并进一步改进我们的构建时间。
改进空间
那么,所有构建作业中究竟有哪些共同步骤?每个构建目标都有一个作业,该作业遵循一组类似的步骤。从非常高的层面来看,每个作业都必须
- 恢复依赖项
- Lint TypeScript 和 JavaScript
- 将 TypeScript 编译为 JavaScript
- 运行单元测试套件
- 运行集成测试套件
- 打包 VS Code
我们的缓存任务是加速恢复依赖项步骤的显而易见的选择。例如,当 package-lock.json
文件很少更改时,为什么还要运行昂贵的 npm install
步骤,而不缓存先前运行的结果呢?由于我们之前已经讨论过缓存包,因此这篇文章的有趣之处在于我们如何将缓存应用于其他步骤。
由于 linting 和编译是平台无关的,因此这些步骤可以很容易地由单个构建代理运行,该代理会与其他平台相关的代理共享其结果,而不是让所有代理重复执行这项工作。我们创建了一个 Linux 构建代理,其唯一职责正是如此:恢复包、lint 和编译源代码。我们所要做的就是与其他代理共享结果。
缓存一切
为了在构建代理之间共享缓存结果,我们需要平台无关的缓存,而缓存任务最初不支持平台无关的缓存。因此,向 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 commit 作为缓存键,因为新的 commit 不可避免地会创建新的缓存条目。这对我们的目的来说效果很好,因为单个构建,尽管跨构建代理运行,但始终在单个 commit 上运行。
另一个缺失的功能是每个构建作业创建多个缓存的能力。我们现在发现自己要处理两个缓存(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 工作中的重复,这些工作现在可以只执行一次并在特定于平台的代理之间共享。
考虑到一个特定的用例:构建重新提交,仍然有一个最终的优化空间。我们有时必须在先前构建的 commit 上重新触发 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% |
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