提高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使用的节点模块,而不是在构建时解析包。虽然我们对这种性能提升感到满意,但我们想看看我们还能进一步推动我们构建的缓存任务。
当我们上次讨论我们的CI工程时,我们的目标平台涵盖了Windows、macOS和Linux。截至今天,VS Code的目标平台更加多样化,例如用于其远程服务器组件的Arm64和Alpine Linux。总的来说,我们有八个不同的目标,它们都共享相同的构建步骤。这篇文章概述了我们如何利用缓存任务来减少CI重复并进一步改善我们的构建时间。
改进空间
那么,所有构建作业中常见的步骤到底是什么?每个构建目标都有一个遵循类似步骤的作业。从高层次来看,每个作业必须:
- 恢复依赖项
- Lint TypeScript 和 JavaScript
- 将TypeScript编译为JavaScript
- 运行单元测试套件
- 运行集成测试套件
- 打包 VS Code
我们的缓存任务是加速恢复依赖项步骤的明显选择。例如,为什么要在package-lock.json
文件很少更改的情况下运行昂贵的npm install
步骤,而不是缓存之前运行的结果呢?由于我们之前已经讨论过缓存包的问题,这篇文章有趣的地方在于我们如何将缓存应用到其他步骤中。
由于代码检查和编译是平台无关的,这些步骤可以很容易地由一个单一的构建代理来执行,该代理会将其结果与其他平台相关的代理共享,而不是让所有代理重复执行这项工作。我们创建了一个Linux构建代理,其唯一职责正是如此:恢复包、检查代码并编译源代码。我们所要做的就是将结果与其他代理共享。
缓存所有内容
为了在构建代理之间共享缓存结果,我们需要平台无关的缓存,这在最初是不被缓存任务支持的。因此,在Azure Pipelines Artifact Caching Tasks中添加了一个可选的platformIndependent
参数。
以下是VS Code如何使用platformIndependent
参数:
- task: 1ESLighthouseEng.PipelineArtifactCaching.RestoreCacheV1.RestoreCache@1
inputs:
keyfile: keyfile
targetfolder: target
vstsFeed: $(ArtifactFeed)
platformIndependent: true
在缓存节点模块时,使用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针对的每个平台的总构建时间的变化:
Platform | Before | After | Time Savings |
---|---|---|---|
Windows | 58 min | 44 min | 24% |
Windows 32 | 59 min | 46 min | 22% |
Linux | 38 min | 23 min | 39% |
macOS | 68 min | 42 min | 38% |
Linux Arm | 22 min | 21 min | 5% |
Linux Alpine | 23 min | 26 min | -13% |
Linux Arm 和 Linux Alpine 目标仅构建 VS Code 远程服务器组件,因此它们的原始构建时间已经足够好。但由于它们与标准的 VS Code 客户端平台共享一些共同任务,我们决定让它们依赖于共同的构建代理。这导致在某些情况下由于开销增加而略微增加了构建时间。
由于可以完全跳过共享代理任务,重新提交的构建有了显著的改进。以下是一些macOS的示例数据:
Platform | Before | After | Time Savings |
---|---|---|---|
macOS | 68s | 34s | 50% |
总的来说,我们非常高兴看到VS Code的CI构建时间总共减少了约50%!最好的消息是,你可以从我们的构建定义中汲取灵感,以实现你自己构建时间的改进。
缓存愉快,
Ethan Dennis,开发者服务高级软件工程师 @erdennis13
若昂·莫雷诺,VS Code 高级软件工程师 @joaomoreno