Sansan Builders Box

Sansanのものづくりを支える技術やデザイン、プロダクトマネジメントの情報を発信

Bitrise のキャッシュを利用して、全体のビルド時間を 25% 高速化した話 🚀

こんにちは。Sansan iOS アプリエンジニアの中川です。

前回に引き続き、モバイルアプリの CI / CD に関するテーマを取り扱いたいと思います。
今回は CI サービスとして Bitrise を利用されている方を対象に Tips を共有します。

buildersbox.corp-sansan.com

課題感

下の Bitrise のビルド一覧を見てもらえれば、一目瞭然ですが、iOS アプリで 20 分を超えるビルドがちらほら出てくるようになっていました。

Sansan では Bitrise の ELITE プランを利用していますが、 iOS アプリと Android アプリのどちらのビルドも同じビルドキューに入るので、 3 並列でビルドが実施できるとはいえ、ビルド時間が長いとビルドが滞留してしまう可能性も上がってしまいます。

f:id:ynakagawa33:20190731112554p:plain
20 分を超えるビルドがちらほら出ている iOS アプリのビルド

片や Android アプリは長くても iOS アプリの半分の 10 分でビルドが完了していますs。

iOS アプリのビルドが滞留して、 Android アプリエンジニアから苦情が来てしまっては申し訳が立たないので、ビルド時間の短縮に挑戦しようと考えました。

f:id:ynakagawa33:20190731112453p:plain
長くても 10 分の Android アプリのビルド

解決策の模索

では、短縮できそうな部分はどこでしょうか?
Bitrise のビルドログには最後にどのステップにどのくらいの時間がかかったのか出力されているので、その内容を確認しましょう。

f:id:ynakagawa33:20190731113548p:plain
各ステップにかかった時間が出力されている bitrise summary

bitrise summary を見てみると、明らかに遅い 3 つのステップが見つかります。
iOS アプリをビルドするための環境セットアップを行っている Bootstrap ステップと iOS アプリのテストを実行している xcode-test ステップと iOS アプリの ipa を作成している xcode-archive ステップです。
このうち、 xcode-test ステップと xcode-archive ステップは Bitrise から提供しているものを利用しているため、手を入れられそうな箇所が少なさそうなのと、純粋に Xcode での iOS アプリのビルドを早くしたほうが効果的と判断しました。

Bootstrap ステップを深堀りをしてみましょう。
下記のビルドログは実際に出力されるものから途中経過のログといった説明に不要なログを取り除いたものです。
このログを元に改善ポイントを探してみます。

+------------------------------------------------------------------------------+
[REDACTED] (0) Bootstrap                                                                [REDACTED]
+------------------------------------------------------------------------------+
[REDACTED] id: script                                                                   [REDACTED]
[REDACTED] version: 1.1.5                                                               [REDACTED]
[REDACTED] collection: https://github.com/bitrise-io/bitrise-steplib.git                [REDACTED]
[REDACTED] toolkit: bash                                                                [REDACTED]
[REDACTED] time: 2019-07-09T06:06:00Z                                                   [REDACTED]
+------------------------------------------------------------------------------+
[REDACTED]                                                                              [REDACTED]
+ brew update
+ brew reinstall rbenv
+ echo 'eval "$(rbenv init -)"'
+ echo 'export PATH="$HOME/.rbenv/bin:$PATH"'
+ source /Users/vagrant/.bash_profile
+ curl -fsSL https://github.com/rbenv/rbenv-installer/raw/master/bin/rbenv-doctor
+ bash
+ rbenv install 2.6.3
+ sh ./Scripts/bootstrap.sh
Generating Configuration Settings File...
Generated Configuration Settings File :)
Installing dependencies...
Warning: the running version of Bundler (1.17.2) is older than the version that created the lockfile (1.17.3). We suggest you upgrade to the latest version of Bundler by running `gem install bundler`.
Fetching gem metadata from https://rubygems.org/........
Fetching CFPropertyList 3.0.0
Installing CFPropertyList 3.0.0
Fetching ZenTest 4.11.2
Installing ZenTest 4.11.2
Fetching RubyInline 3.12.4
Installing RubyInline 3.12.4
Fetching concurrent-ruby 1.1.5
Installing concurrent-ruby 1.1.5
Fetching i18n 0.9.5
Installing i18n 0.9.5
Fetching minitest 5.11.3
Installing minitest 5.11.3
Fetching thread_safe 0.3.6
Installing thread_safe 0.3.6
Fetching tzinfo 1.2.5
Installing tzinfo 1.2.5
Fetching activesupport 4.2.11.1
Installing activesupport 4.2.11.1
Fetching atomos 0.1.3
Installing atomos 0.1.3
Using bundler 1.17.2
Fetching claide 1.0.2
Installing claide 1.0.2
Fetching fuzzy_match 2.0.4
Installing fuzzy_match 2.0.4
Fetching nap 1.1.0
Installing nap 1.1.0
Fetching cocoapods-core 1.6.1
Installing cocoapods-core 1.6.1
Fetching cocoapods-deintegrate 1.0.4
Installing cocoapods-deintegrate 1.0.4
Fetching cocoapods-downloader 1.2.2
Installing cocoapods-downloader 1.2.2
Fetching cocoapods-plugins 1.0.0
Installing cocoapods-plugins 1.0.0
Fetching cocoapods-search 1.0.0
Installing cocoapods-search 1.0.0
Fetching cocoapods-stats 1.1.0
Installing cocoapods-stats 1.1.0
Fetching netrc 0.11.0
Installing netrc 0.11.0
Fetching cocoapods-trunk 1.3.1
Installing cocoapods-trunk 1.3.1
Fetching cocoapods-try 1.1.0
Installing cocoapods-try 1.1.0
Fetching colored2 3.1.2
Installing colored2 3.1.2
Fetching escape 0.0.4
Installing escape 0.0.4
Fetching fourflusher 2.2.0
Installing fourflusher 2.2.0
Fetching gh_inspector 1.1.3
Installing gh_inspector 1.1.3
Fetching molinillo 0.6.6
Installing molinillo 0.6.6
Fetching ruby-macho 1.4.0
Installing ruby-macho 1.4.0
Fetching nanaimo 0.2.6
Installing nanaimo 0.2.6
Fetching xcodeproj 1.8.2
Installing xcodeproj 1.8.2
Fetching cocoapods 1.6.1
Installing cocoapods 1.6.1
Fetching dotenv 2.7.2
Installing dotenv 2.7.2
Fetching osx_keychain 1.0.2
Installing osx_keychain 1.0.2
Fetching cocoapods-keys 2.1.0
Installing cocoapods-keys 2.1.0
Bundle complete! 2 Gemfile dependencies, 35 gems now installed.
Bundled gems are installed into `./vendor/bundler`
Updating spec repo `master`
  $ /usr/local/bin/git -C /Users/vagrant/.cocoapods/repos/master fetch origin --progress
  remote: Enumerating objects: 12067           
  remote: Enumerating objects: 16504, done.          
  remote: Counting objects: 100% (16504/16504), done.                
  remote: Compressing objects: 100% (5182/5182), done.        
  remote: Total 28571 (delta 13188), reused 13777 (delta 10939), pack-reused 12067        
  Receiving objects: 100% (28571/28571), 3.14 MiB [REDACTED] 17.97 MiB/s, done.
  Resolving deltas: 100% (18838/18838), completed with 2777 local objects.
  From https://github.com/CocoaPods/Specs
     52b73bb13d5..6560482ee3b  master     -> origin/master
  $ /usr/local/bin/git -C /Users/vagrant/.cocoapods/repos/master rev-parse --abbrev-ref HEAD
  master
  $ /usr/local/bin/git -C /Users/vagrant/.cocoapods/repos/master reset --hard origin/master
  Checking out files: 100% (3028/3028), done.
  HEAD is now at 6560482ee3b [Add] JWHelperKit 1.1.3
warning: inexact rename detection was skipped due to too many files.
warning: you may want to set your diff.renameLimit variable to at least 2823 and retry the command.

CocoaPods 1.7.3 is available.
To update use: `gem install cocoapods`

For more information, see https://blog.cocoapods.org and the CHANGELOG for this version at https://github.com/CocoaPods/CocoaPods/releases/tag/1.7.3

Analyzing dependencies
Downloading dependencies
Installing Crashlytics (3.12.0)
Installing DZNSegmentedControl (1.3.3)
Installing Fabric (1.9.0)
Installing HMSegmentedControl (1.5.3)
Installing MXPagerView (0.2.1)
Installing MXParallaxHeader (0.6.1)
Installing MXSegmentedPager (3.3.0)
Installing R.swift (5.0.3)
Installing R.swift.Library (5.0.1)
Installing Sourcery (0.16.1)
Installing SwiftLint (0.31.0)
Generating Pods project
Integrating client project
Sending stats
Pod installation complete! There are 7 dependencies from the Podfile and 11 total pods installed.

Updated git hooks.
Git LFS initialized.
Installed dependencies :)
Initializing AppConfiguration.swift...
No config file provided or it does not exist. Using command line arguments.
Scanning sources...
Found 0 types.
Loading templates...
Loaded 1 templates.
Generating code...
Finished.
Processing time 0.011717081069946289 seconds
Initialized AppConfiguration.swift :)
[REDACTED]                                                                              [REDACTED]
+---+---------------------------------------------------------------+----------+
[REDACTED] [32;1m✓[0m [REDACTED] [32;1mBootstrap[0m                                                     [REDACTED] 347 sec  [REDACTED]
+---+---------------------------------------------------------------+----------+

✂️ 都度、再インストールしている rbenv

+ brew update
+ brew reinstall rbenv

こちら、 Swift5 と Swift4.2 で並行してビルドできるようにワークフローを作っていた際にスタックによって、 rbenv があったりなかったりしていた問題をワークフローで吸収するために実行していたコマンドです。
Sansan iOS アプリでは Swift5 対応が無事、完了し、リリースされたので、 rbenv が最初からインストールされているスタックしか利用しなくなったため、やめられそうです。

✂️ 都度、インストールしている ruby 2.6.3

rbenv install 2.6.3

Bitrise から提供されている最新のスタックを利用しても、rbenv に ruby 2.6.3 はインストールされていないので、個別にインストールしてあげる必要があります。
こちらへの解決策は次の2つが考えられそうです。

  • Bitrise にインストールされているバージョンの ruby を利用する
  • ruby 2.6.3 を Bitrise のキャッシュに追加して、キャッシュに存在している限りはインストールを実行しない

前者はスタックによって変わる可能性もあり、その度に開発で利用する ruby のバージョンを変更するのは運用コストが高く、ワークフローごとに柔軟にスタックを変えられるメリットを享受できなくなってしまいます。後者の場合、キャッシュに追加してしまえば、インストールコストを無視できますし、 Bitrise のキャッシュはブランチごとに保存され、ワークフローやスタックを跨いで共有できるため、キャッシュの存在期間も長そうです。

後者で改善を進めてみます。

キャッシュについて - Bitrise Docs

上記ページにキャッシュの利用方法が日本語で記載されているので、参考にしつつ、ワークフローにキャッシュの機能を追加します。

1. Git Clone Repository ステップと Bootstrap ステップの間に Bitrise.io Cache:Pull ステップを追加する

f:id:ynakagawa33:20190724194500p:plain
Bitrise Workflow Editor での設定画面

2. キャッシュするディレクトリを環境変数に追加する

Sansan iOS アプリでは利用する ruby のバージョンを .ruby-version で固定化しています。
なので、以下のコマンドで指定バージョンの ruby のディレクトリを取得しつつ、環境変数へ追加することができます。

envman add --key RBENV_RUBY_VERSION_HOME --value "$(rbenv root)""/versions/""$(< .ruby-version)"

上記のスクリプトは Bootstrap 直後に実行します。

f:id:ynakagawa33:20190730161243p:plain
Bitrise Workflow Editor での設定画面

3. ワークフローの最後のステップに Bitrise.io Cache:Push ステップを追加する

前段階で環境変数に追加したキャッシュしたい ruby のディレクトリを Cache paths に指定します。

f:id:ynakagawa33:20190730161915p:plain
Bitrise Workflow Editor での設定画面

4. キャッシュが存在している限り、 Bootstrap ステップで ruby のインストールを行わない

利用しているスタックに ruby 2.6.3 がインストールされていないという前提を利用して、パスチェックで存在していれば、キャッシュからダウンロードできている or スタックに ruby 2.6.3 がインストールされていると判断し、何もしないヌルコマンドを実行しています。そうでなければ、指定バージョンの ruby のインストールを実行します。

rubyVersionBinaryPath="$(rbenv root)""/versions/""$(< .ruby-version)""/bin/ruby"

if [ -e "$rubyVersionBinaryPath" ]; then
  :
else
  brew update && brew upgrade ruby-build || true # すでにインストール済みだったとしても異常終了にしない
  rbenv install "$(< .ruby-version)"
fi

完成 🎉

キャッシュが存在している場合のビルドログは以下のようになります。

++ rbenv root
+ rubyVersionBinaryPath=/Users/vagrant/.rbenv/versions/2.6.3/bin/ruby
+ '[' -e /Users/vagrant/.rbenv/versions/2.6.3/bin/ruby ']'
+ :

✂️ 都度、インストールしている gem

Fetching CFPropertyList 3.0.0
Installing CFPropertyList 3.0.0
… (中略)
Bundle complete! 2 Gemfile dependencies, 35 gems now installed.
Bundled gems are installed into `./vendor/bundler`

Sansan iOS アプリでは CocoaPods を利用しており、その CocoaPods が依存している gem が大量にインストールされています。
こちらも頻繁に変わるものでも有りませんし、キャッシュに追加しても問題なさそうです 🤗

1. Bitrise.io Cache:Push ステップに Gemfile.lock に変更があったら、 gem インストール先ディレクトリをキャッシュに追加するように設定変更する

一つ前の rbenv でインストールしている ruby 2.6.3 のキャッシュへの追加でワークフローにはキャッシュの仕組みがもたらされているので、 Bitrise.io Cache:Push の設定変更だけで済みます 🆒

Cache paths に ./vendor/bundler -> ./Gemfile.lock を追記します。
./vendor/bundler は Sansan iOS アプリで指定している gem のインストール先ですので、環境に合わせて、変更してください 🙏
設定内容の意図ですが、 Cache paths にディレクトリを指定するとキャッシュの変更があったかどうかを判定するのにすべてのファイルに対してチェックを行ってしまいますが、 -> の後にファイルを指定することでそのファイルのチェックのみに抑制することができます。
gem であれば、 依存性の変更は Gemfile.lock に現れるので、そのファイルを監視すれば OK ということになります。

f:id:ynakagawa33:20190730165840p:plain
Bitrise Workflow Editor での設定画面

完成 🎉

キャッシュが存在している場合のビルドログは以下のようになります。
gem の取得やインストールが発生せず、インストール済みの gem を使用するようになっています。

Using CFPropertyList 3.0.0
… (中略)
Bundle complete! 2 Gemfile dependencies, 35 gems now installed.
Bundled gems are installed into `./vendor/bundler`

✂️ 都度、インストールしている pod

Installing Crashlytics (3.12.0)
Installing DZNSegmentedControl (1.3.3)
Installing Fabric (1.9.0)
Installing HMSegmentedControl (1.5.3)
Installing MXPagerView (0.2.1)
Installing MXParallaxHeader (0.6.1)
Installing MXSegmentedPager (3.3.0)
Installing R.swift (5.0.3)
Installing R.swift.Library (5.0.1)
Installing Sourcery (0.16.1)
Installing SwiftLint (0.31.0)

gem と同じく pod も大量にインストールされていますね。
こちらもキャッシュに追加してしまいましょう 💪

1. Bitrise.io Cache:Push ステップに Podfile.lock に変更があったら、 pod インストール先ディレクトリをキャッシュに追加するように設定変更する

Cache paths に ./Pods -> ./Podfile.lock を追記します。

f:id:ynakagawa33:20190730171105p:plain
Bitrise Workflow Editor での設定画面

完成 🎉

キャッシュが存在している場合のビルドログは以下のようになります。
pod のインストールが発生せず、インストール済みの pod を使用するようになっています。

Using Crashlytics (3.12.0)
Using DZNSegmentedControl (1.3.3)
Using Fabric (1.9.0)
Using HMSegmentedControl (1.5.3)
Using MXPagerView (0.2.1)
Using MXParallaxHeader (0.6.1)
Using MXSegmentedPager (3.3.0)
Using R.swift (5.0.3)
Using R.swift.Library (5.0.1)
Using Sourcery (0.16.1)
Using SwiftLint (0.31.0)

改善した結果

以下が改善後の bitrise summary です。 改善前は 347 sec かかっていた Bootstrap ステップはキャッシュが存在していれば、 50 sec で完了するようになりました。
追加で Bitrise.io Cache:Pull ステップとBitrise.io Cache:Push ステップが増えていますが合わせて、 26 sec の増加なので、Bootstrap ステップの増減率は ((50+26)/347-1)*100 = -78.097982709% となるので、約 78 % 高速化したことになります。
全体で見ると、他の遅い xcode-test ステップと xcode-archive ステップの処理時間のばらつきによって増減率が小さくなってしまっていますが、 25 %以上の高速化を得たことになります。今後、取り組む予定の Xcode での iOS アプリのビルドの高速化に期待したいです 🤗

f:id:ynakagawa33:20190731113744p:plain
改善後の bitrise summary

以下は 20 分を超えるビルドが無くなり、 15 分くらいで収まるようになった iOS アプリのビルド一覧です。
ほっと一息つけそうです ☕️

f:id:ynakagawa33:20190731111649p:plain
改善後の iOS アプリのビルド一覧

© Sansan, Inc.