はじめに
こんにちは!技術本部Sansan Engineering Unit Mobile Applicationグループに所属しているiOSエンジニアのヤズジュ夢佐です。
Sansan iOSアプリは、開発生産性においてビルドの重さが課題でした。全体のfull buildにかかる時間は2分程度。Incremental buildでも、差分がない状態で28秒程度、差分があればさらにそれ以上かかる状態でした。
本記事では、このincremental buildを高速化するために行った取り組みを紹介します。
遅延の原因はbuild phase script
調査の結果、差分のないincremental buildでも毎回28秒以上かかってしまう原因は、差分の有無に関わらずbuild processの中で毎回実行されるbuild phase scriptであることがわかりました。
怪しいbuild phase scriptを一時的に外しつつ、incremental buildを実行することによって、どのbuild phase scriptがどの程度のbuild遅延を引き起こしているのかを簡単に特定できました。
特に遅延が大きかったのは、R.swiftとmockoloでした。
遅延の原因1: mockolo
mockoloは、mockコードの生成を自動化するライブラリです。@mockableのアノテーションが付いたprotocolのmock実装を自動で生成でき、それをUnit Testなどで活用できます。
このmockコードの生成処理がbuild phase scriptとして組み込まれており、差分の内容に関わらず毎回実行されるようになっていました。
遅延の原因2: R.swift
R.swiftは、リソースへのアクセスをtype safeに行うための参照コードを生成してくれるライブラリです。Sansan iOSでは、画像、文字列、storyboard、xibなどの定義にtype safeなアクセスを行うために利用されています。
このR.swiftのコード生成処理も、差分の有無に関わらず毎回実行されるようになっていました。この処理がすべてのbuild phase scriptの中で最も大きな遅延を引き起こしており、その遅延時間はおよそ18〜19秒でした。
解決策: 必要な時だけ実行する
mockoloもR.swiftも、すべてのbuildで毎回実行する必要はありません。mockoloの場合は、@mockableのアノテーションのついた定義が編集された時だけ実行すれば十分です。それ以外の実行は無意味な遅延です。
R.swiftも、画像、文字列、storyboard、xibに変更がある時にのみ実行すればよく、それ以外の実行は無意味なbuild遅延です。
解決策として、Build phase scriptとして実行するのではなく、Xcodeのbuild processの外側で任意のタイミングで実行できるようにしました。
Sansan iOSでは、開発中に使うさまざまなcommandをMakefileにまとめています。その中に、make mockolo, make rswiftというcommandを追加しました。
make mockolo commandの実装
make mockoloは、全ターゲット分のMock.generated.swiftを削除した後、ターゲットごとにmockoloを実行して再生成するcommandです。
ポイントは、mockoloをxcrun経由で呼び出す際に、Xcodeのbuild phase scriptが持っているような環境変数(SRCROOTやTARGET_NAMEなど)を明示的に付与する必要があることです。これによって、Xcodeのbuild processの外側からでもMockoloが正常に動作します。
また、削除フェーズでは-pruneで不要なディレクトリを除外して走査コストを削減し、各ターゲットの生成処理は&+waitで並列実行しています。
このアプローチによって、make mockoloの実行は1秒程度で完了するようになりました。
.PHONY: mockolo
mockolo: ## 全ModuleのMock.generated.swiftを生成
@# -pruneで不要なディレクトリを除外して走査コストを削減
@find $(MAKEFILE_DIR) \
-type d \( -name Pods -o -name build -o -name DerivedData ... \) -prune \
-o -name "Mock.generated.swift" -type f -print0 | xargs -0 rm -f
@# 各targetは互いに独立しているので並列実行
@for target in <ターゲット名...>; do \
(export SRCROOT=$(MAKEFILE_DIR) && export TARGET_NAME=$$target && export ACTION=build && \
xcrun --sdk macosx mint run mockolo \
--sourcedirs "$$SRCROOT/$$TARGET_NAME" \
--destination "$$SRCROOT/$$TARGET_NAME/Mock.generated.swift" \
--mock-final --enable-args-history --macro "DEBUG") & \
done; \
wait
make rswift commandの実装
make rswiftは、全ターゲット分のR.generated.swiftを生成するcommandです。
R.swift CLIはもともとXcodeのbuild phase script内で動作することを想定しているため、SDKROOTやPROJECT_FILE_PATHといったXcodeが自動で設定する環境変数を必要とします。
Build Phaseの外から実行する際には、これらの値を事前に設定する必要があります。
export DEVELOPER_DIR="$(xcode-select -p)" export PLATFORM_DIR="$DEVELOPER_DIR/Platforms/iPhoneOS.platform" export SDKROOT="$(xcrun --sdk iphoneos --show-sdk-path)" export PROJECT_FILE_PATH="$SRCROOT/Sansan.xcodeproj" export SOURCE_ROOT="$SRCROOT"
環境変数を設定した後は、ターゲットごとにR.swift CLIを並列実行します。複数ターゲットを並列で処理することで、実行時間を大幅に短縮しました。実行は1秒未満で完了します。
# ターゲットごとに並列実行
"$RSWIFT" generate --target <ターゲット名> "<出力先パス>" &
# ...(全ターゲット分)
# 全ターゲットの完了を待ち、1つでも失敗したらその結果を保持する
for pid in "${PIDS[@]}"; do
wait "$pid" || FAILED=$?
done
既存のmake commandに組み込む
Sansan iOSには、make generateという開発に必要なものを一通り生成できる便利なcommandが用意されています。
make generateでは、xcodegenによるxcodeprojファイルの生成や、KMPをiOSに埋め込むためのxcframeworkの生成、環境変数が入ったファイルの生成などを行っています。
make mockolo, make rswiftは、make generate commandの中で実行されるようにしました。
これによって、branchを切り替えるなどの大規模なコード変更の後に、開発者は今まで通りmake generateを実行するだけで、rswiftやmockoloの生成物も更新できるようにしました。
まとめ
本記事では、差分なしのincremental buildを28秒から2秒に高速化した取り組みを紹介しました。
- R.swiftとmockoloをbuild phase scriptから外し、必要な時だけ手動で実行できるようにすることで高速化
- 既存のmake generateに組み込み、追加の手間を軽減
Buildが遅い、特に差分がないincremental buildでも時間がかかる場合は、差分に関わらず毎回実行されるようになっている不要なbuild phase scriptがないか、疑ってみてください。
Sansan技術本部ではカジュアル面談を実施しています
Sansan技術本部では中途の方向けにカジュアル面談を実施しています。Sansan技術本部での働き方、仕事の魅力について、現役エンジニアの視点からお話しします。「実際に働く人の話を直接聞きたい」「どんな人が働いているのかを事前に知っておきたい」とお考えの方は、ぜひエントリーをご検討ください。