エンジニアリングノート

xcresultでXCTest実行時間の回帰ゲートを作る

xcresultでXCTest実行時間の回帰ゲートを作る

xcresultでXCTest実行時間の回帰ゲートを作る

本来は4秒で終わっていたXCTestが徐々に11秒まで延びても、パイプラインは正常のままで、チームが気付くのはジョブの待ち行列が目に見えて長くなってから、ということがよくあります。ビルド全体の所要時間は、この種の変化を特定する指標には向きません。依存関係の解決、コンパイル、シミュレータの起動、結果の書き込みがすべて含まれるためです。より確実な方法は、NUMACSのクラウドMacで実行する各テストジョブの.xcresultを保存し、テストケース単位の所要時間を抽出して、管理されたベースラインと比較することです。

まずゲートで何を測るかを定義する

ゲートが答えるべきなのは「同じテストが継続的に遅くなっているか」であり、「今回のジョブが前回より何秒長かったか」ではありません。まず指標を次の3階層に分けます。

階層 指標 用途
テストケース 1回の実行時間 個別の回帰を特定する
テストスイート 中央値と高パーセンタイル テスト群全体の推移を把握する
CIジョブ 起動から終了までの総所要時間 インフラまたはコンパイル工程の異常を検出する

1回の結果だけでマージをブロックしてはいけません。短いテストはスケジューリングの揺らぎに影響されやすく、長いテストはネットワーク、アニメーション、非同期処理の待機によって時間が大きく延びることがあります。実用的なのは「二重しきい値」による判定です。候補値がベースラインに対する相対増加率と絶対増加量の両方を超えた場合にだけ、再テストへ進めます。たとえばベースラインが2秒の場合、20%増えていても実際の増加が0.1秒だけなら失敗にすべきではありません。一方、ベースラインが40秒で8秒増えているなら、確認する価値があります。

パフォーマンスゲートの目的は、毎回同じ数値を得ることではありません。再現性があり、原因を特定できる低速化を早期に見つけることです。

実行環境を固定して結果バンドルを生成する

最初にXcodeのパス、Scheme、実行先、並列実行ポリシーを固定します。シミュレータのモデルとシステムバージョンは明示し、「現在起動しているデバイス」に依存してはいけません。同じジョブの途中で同時実行数を動的に変更することも避けます。

set -euo pipefail

RESULT_DIR="$PWD/artifacts"
RESULT_BUNDLE="$RESULT_DIR/RegressionTests.xcresult"

mkdir -p "$RESULT_DIR"
rm -rf "$RESULT_BUNDLE"

xcodebuild test \
  -workspace Example.xcworkspace \
  -scheme ExampleTests \
  -destination 'platform=iOS Simulator,name=iPhone 16,OS=18.0' \
  -resultBundlePath "$RESULT_BUNDLE" \
  -parallel-testing-enabled NO

例に示したバージョンは、実行環境を構成する要素の一つにすぎません。実際のプロジェクトでは、検証済みのXcodeとシミュレータランタイムを固定してください。まず統計に含めないウォームアップジョブを1回実行し、シミュレータの起動を完了させ、必要なテストリソースをディスクへ配置します。正式なサンプルは少なくとも3回実行します。テスト自体の分散が大きい場合は、意味を失うほどしきい値を緩めるのではなく、実行回数を増やしてください。

xcresultからテストケースの所要時間を抽出する

比較的新しいXcodeでは、xcresulttoolを使ってテストツリーを出力できます。コマンドインターフェースはXcodeのバージョンとともに変化するため、パーサーもCIで使用するXcodeバージョンと合わせて固定し、アップグレード前には保存済みの結果バンドルを使って互換性をテストする必要があります。

xcrun xcresulttool get test-results tests \
  --path artifacts/RegressionTests.xcresult \
  --format json > artifacts/tests.json

jq -r '
  .. | objects
  | select(.nodeType? == "Test Case" and .duration? != null)
  | [.name, .duration] | @tsv
' artifacts/tests.json > artifacts/test-durations.tsv

比較へ進む前に、TSVが空でないことと、テスト数が想定どおりであることを確認します。空のファイルを「回帰なし」と見なして通過させてはいけません。通常はコマンドインターフェースが変更された、テストが実行されなかった、または抽出条件が一致しなくなったことを示します。テスト識別子にはモジュール、クラス、メソッドを含めます。パラメータ化テストでは、異なるサンプルが誤って統合されないように、パラメータ名も保持する必要があります。

正規化しても元の証拠を残す

所要時間をすべて秒単位に変換し、各レコードにcommit、Xcodeバージョン、実行先、ジョブ番号を付加します。集約ファイルは比較に便利ですが、元の.xcresultもジョブの成果物として保存してください。異常が発生した際には、失敗情報、アクティビティログ、添付ファイルのコンテキストも確認できます。

直前の結果ではなく堅牢なベースラインを使う

ベースラインをメインブランチの直近1回の実行結果にしてはいけません。1回の遅い起動が後続の判定を汚染する一方、異常に速かった1回の実行が大量の誤検知を引き起こす可能性もあります。より堅牢な方法は、メインブランチで直近に成功した複数のサンプルを収集し、テストごとに中央値を求め、高パーセンタイルまたは中央絶対偏差も記録することです。

テストごとに次のフィールドを保存することを推奨します。

{
  "ExampleTests.testParsing": {
    "median_seconds": 3.84,
    "absolute_limit_seconds": 1.5,
    "relative_limit": 0.25,
    "sample_count": 9
  }
}

候補ブランチで初めてしきい値を超えたら、超過したテストケースまたはそのテストスイートだけを再実行します。再テストの中央値がmedian_seconds + absolute_limit_secondsmedian_seconds × (1 + relative_limit)の両方を超えた場合にのみ、ジョブを失敗として扱います。新しいテストはまず観察期間に入り、サンプルが不足している間は報告だけを行い、ブロック判定には使用しません。

よくある偽の回帰を除外する

シミュレータのコールドスタートは最大のノイズ要因です。テストデータの初期化、フォントの初回読み込み、データベーステーブルの作成も初回実行を遅くするため、ウォームアップを使うか、明示的なsetUp処理として切り分けます。次に、テストがネットワークへアクセスしていないか、実時間の経過を待っていないか、ユーザーデフォルトを共有していないか、前のテストケースが残したファイルを再利用していないかを確認します。

並列テストはCPU、メモリ、ディスクの競合状態を変える可能性があります。テストケース単位のベースラインを作ることが目的なら、並列実行を無効にします。実際のパイプラインのスループットを評価することが目的なら、worker数を固定し、その数をベースラインの軸として記録してください。Xcode、システムランタイム、ハードウェア構成が異なるデータを直接混在させてはいけません。

突然遅くなった場合は、次の順序で確認します。

  1. テスト数または実行先が変わっていないか
  2. コンパイル、インストール、シミュレータ起動の待ち時間が発生していないか
  3. タイムアウトがポーリングや固定スリープに起因していないか
  4. テストフィクスチャが肥大化していないか、終了後にクリーンアップされているか
  5. 複数のジョブが同じ作業ディレクトリを同時に使用していないか

ベースラインの変更をレビュー可能にする

ベースラインファイルはバージョン管理に含めますが、通常のテストジョブには読み取りだけを許可し、自動的に上書きさせてはいけません。妥当な理由による低速化の場合は、独立したジョブでメインブランチの安定したサンプルから差分を生成します。レビュー担当者が旧値、新値、サンプル数、変更理由を確認できるようにしてください。

最終レポートは「新規回帰」「回復済み」「観察中」に分類し、絶対増加量、相対増加率、再テスト結果を一覧にします。これにより、隠れたパフォーマンス低下をブロックしながら、1回のシミュレータの揺らぎで開発フローを止めずに済みます。最終的にチームが得るのは、壊れやすいストップウォッチではなく、再現でき、説明でき、レビューできるXCTest所要時間のベースラインです。

よくある質問

xcodebuild全体の所要時間だけでは不十分なのはなぜですか?

依存関係の解決、コンパイル、シミュレータ起動、結果の書き込みが混在するためです。テスト単位の診断にはxcresult内の個別所要時間を使います。

しきい値は秒数と割合のどちらで設定すべきですか?

絶対増加量と増加率を組み合わせます。再実行後も両方を超えた場合だけ回帰と判定すると、短いテストの微小な揺らぎを除外できます。

基準値を更新するときに残すべき情報は何ですか?

基準ファイル、対象commit、Xcodeと実行先のバージョン、元のxcresult、更新理由を一緒に保存します。

Numacs クラウドMac

ビルド作業を専用の物理ワークステーションへ

2種類の Apple Silicon 構成を、専用の物理マシンとして提供します。仮想マシンではなく、日単位、週単位、月単位、四半期単位でレンタルできます。シンガポール、東京、ソウル、香港のノードに対応しています。

デバイスを選んで注文