改進 CI 構建時間
2020 年 2 月 18 日,作者:Ethan Dennis (@erdennis13) 和 João Moreno (@joaomoreno)
Visual Studio Code 是一個包含許多活動部件和活躍參與者的大型專案。我們展示了如何積極使用 Azure Pipelines 來維護我們的構建和持續整合基礎設施,從而保持良好的工程實踐。在這篇部落格文章中,我們將討論如何使用 Azure Pipelines Artifact Caching Tasks(Azure Pipelines Artifact 快取任務)來大幅縮短我們的 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 步驟,而不是快取上次執行的結果呢?既然我們之前討論過快取包,那麼這篇文章的有趣之處在於我們如何將快取應用於其他步驟。
由於 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
快取節點模組時,使用 package-lock.json 檔案作為快取鍵是合乎邏輯的。當此檔案更改時,我們必須使快取失效。快取編譯輸出時,整個程式碼庫必須充當快取鍵。為了簡化起見,我們決定使用 HEAD commit 作為快取鍵,因為新的 commit 不可避免地會建立一個新的快取條目。這適用於我們的目的,因為單個構建(儘管跨構建代理執行)始終針對單個 commit 執行。
另一個缺失的功能是每個構建作業建立多個快取的能力。我們現在發現自己正在處理兩個快取(節點模組、編譯),但無法單獨定址每個快取。快取任務會輸出一個名為 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