クラウドMacでiOS起動性能の回帰を検出する
ホーム画面のリファクタリングをマージし、機能テストもすべて通過したにもかかわらず、ユーザーから初回起動が遅くなったという報告が寄せられ始めました。起動性能は日々の開発で見落とされやすい領域です。データベースマイグレーションの追加、設定の同期読み込み、分析モジュールの初期化、大きな画像の読み込みなどによって、メインスレッドに処理時間が少しずつ積み上がることがあります。人が操作して問題に気付くのを待つのではなく、固定したクラウドMacでコールド起動を計測し、マージゲートとして運用します。
比較可能な起動シナリオを先に定義する
「アプリを開く」だけでは、完全なテスト定義にはなりません。少なくともビルド構成、Simulatorの機種、OSランタイム、アプリの初期データ、起動引数を固定する必要があります。テスト対象も単一に保ちます。たとえば、プロセスの生成から最初の操作可能な画面が表示されるまでだけを計測し、ログインリクエストやテストデータのダウンロードを結果に含めないようにします。
起動性能テストだけを有効にした Performance.xctestplan を別途作成し、ランダム実行を無効にすることを推奨します。テスト環境では、起動引数を使って安定した画面へ遷移させます。
final class LaunchPerformanceTests: XCTestCase {
override func setUp() {
continueAfterFailure = false
}
func testColdLaunch() {
let app = XCUIApplication()
app.launchArguments = ["-uiTesting", "-resetLocalState"]
let options = XCTMeasureOptions()
options.iterationCount = 8
measure(
metrics: [XCTApplicationLaunchMetric(waitUntilResponsive: true)],
options: options
) {
app.launch()
}
}
}
-resetLocalState はアプリのテスト用エントリポイントで処理し、再構築可能なローカル状態だけを消去します。この引数を理由に、本来計測すべき初期化処理を本番コードの経路でスキップしてはいけません。それでは「テスト専用の起動速度」しか得られません。
クラウドMacとSimulatorの状態を固定する
性能ゲートには、未知のジョブとCPUやディスクを奪い合う共有環境ではなく、物理マシンを占有できる環境が必要です。実行前には毎回、現在のXcodeパス、利用可能なSimulator、ディスクの空き容量を確認します。
set -euo pipefail
xcode-select -p
xcodebuild -version
xcrun simctl list devices available
df -h "$HOME"
テストプランでは、インストール済みのSimulatorを1台指定します。スクリプトで「最初に見つかった利用可能なデバイス」を自動選択してはいけません。イメージが変わると、過去のデータとの比較が成立しなくなるためです。ゲートの実行中は、アーカイブの並列実行、依存関係の更新、大規模なインデックス処理も避けます。
性能データは、第一に環境のデータであり、コードのデータであるのはその次です。実行ノード、ビルド構成、Simulatorの状態を説明できない数値を、そのままマージのブロック判定に使うべきではありません。
最初にベースラインを作成するときは、同じコミットで繰り返し実行します。ばらつきが明らかに大きい場合は、すぐに許容範囲を広げるのではなく、バックグラウンドジョブ、ディスク負荷、テストデータのリセット処理を先に確認します。
xcodebuildで単一のゲートを実行する
コマンドラインジョブでは対象のテストケースだけを実行し、結果バンドルをパイプラインの成果物として保存します。デバイス名は、テストプランで実際に固定した機種に置き換える必要があります。
set -o pipefail
mkdir -p Artifacts
xcodebuild test \
-workspace App.xcworkspace \
-scheme App \
-testPlan Performance \
-destination 'platform=iOS Simulator,name=iPhone 16' \
-only-testing:AppUITests/LaunchPerformanceTests/testColdLaunch \
-resultBundlePath Artifacts/Launch.xcresult \
| tee Artifacts/launch-test.log
set -o pipefail は非常に重要です。これがないと、xcodebuild が失敗しても、パイプ末尾の tee によってジョブが成功として終了する可能性があります。Launch.xcresult とテキストログはどちらも保存します。前者は各計測結果やテスト添付ファイルの確認に使用し、後者はコンパイル時または起動時のエラーを素早く特定するために役立ちます。
| 確認項目 | 固定方法 | 変更後の対応 |
|---|---|---|
| Xcode | xcodebuild -version を記録 |
先にベースライン候補を作り直す |
| Simulator | 機種とランタイムを固定 | 古いベースラインとは直接比較しない |
| ビルド構成 | 同じテストプランを使用 | 一時的な引数による上書きを認めない |
| テストデータ | 起動引数で決定的にリセット | リセットが完全か確認 |
| 反復回数 | ゲートでは一律8回実行 | 1回だけの結果で判定しない |
単発の数値を追わず、ベースラインを構築する
Xcodeのテストレポートで対象テストの性能ベースラインを作成し、生成された共有ベースラインファイルをバージョン管理に含めます。ベースラインの変更は、依存関係のロックファイルと同様にレビュー対象とします。コミットメッセージには変更理由、検証に使用したコミット、想定される影響を明記し、単に「性能データを更新」とだけ書いて済ませてはいけません。
しきい値は、警告とブロックを分けて設計する必要があります。小さな変動は傾向観察にとどめ、連続して再実行しても許容範囲を超える変化だけを失敗と判定できます。判断材料になるのは1回だけの最速値ではなく、同じ環境で複数回反復した結果が、レビュー済みのベースラインから安定して外れているかどうかです。
次のような場合は、ベースラインをそのまま更新してはいけません。
- テストノードで別の高負荷ジョブが同時に実行されていた。
- Simulatorの機種またはランタイムが変更された。
- 依存関係の初回ダウンロードやインデックス処理が完了していなかった。
- テストを通すために初期化処理をスキップしていた。
- 異常が発生したのは1回だけで、ほかの結果は正常だった。
失敗時の証拠を残し、原因となる経路を特定する
ゲートが失敗したら、まず同じクラウドMac、同じコミットでもう一度実行します。結果を再現できた場合は、プロセスのエントリポイント、依存性注入、データベースのオープン、初期画面モデルの構築、リソースのデコード、最初のフレームの描画という起動段階ごとに分解します。これらの境界に統一した os_signpost を追加すれば、コミットの規模から推測するのではなく、後続の分析でどの区間に時間がかかっているかを確認できます。
調査は直近の変更から始めますが、ビジネスロジックだけに注目してはいけません。ビルド構成の変更、デバッグ診断オプション、リソースサイズの増加、テストフィクスチャも起動経路を変える可能性があります。修正後は、失敗時と成功時の2つの xcresult を保存し、マージリクエストに比較対象のコミットを記録します。
信頼できるゲートの価値は、起動時間に絶対的な評価を付けることではなく、同じシナリオを継続して比較可能にすることにあります。環境を固定し、ベースラインをバージョン管理し、証拠を保存したうえで、ベースラインの更新もレビュー対象にします。こうして初めて、起動性能を主観的な感覚ではなく、追跡可能なエンジニアリング上の制約として扱えるようになります。
よくある質問
起動性能テストは何回実行すべきですか?
同じコミットで最低5回、ゲート用ジョブでは8回以上を目安にし、Simulator機種、OSランタイム、ビルド設定、バックグラウンド処理を固定します。
性能ゲートが失敗したら閾値を緩めてもよいですか?
すぐには変更しません。同じノードで再実行し、Simulatorの状態と実行経路を確認したうえで、意図した仕様変更の場合だけ新しい基準をレビューします。
プロジェクト単位で起動・停止できるクラウドMacでワークフローを再現
3種類のApple Silicon構成から必要なリソースを選び、日単位、週単位、月単位、または四半期単位でレンタルできます。専用物理マシンを使用しており、仮想マシンではありません。実際の利用可能状況は、コンソールからリアルタイムで確認できます。