エンジニアリング記事

クラウドMacでXCTMetricによるiOSスクロール性能回帰ゲートを作る

クラウドMacでXCTMetricによるiOSスクロール性能回帰ゲートを作る

商品リストにグラデーションレイヤー、画像デコード、複雑なシャドウなどを追加すると、機能テストはすべて成功していても、スクロール時にフレーム落ちが発生することがあります。人が数回操作しただけでは、一時的なカクつきなのか、コード変更による回帰なのかを判断するのは困難です。より確実なのは、クラウドMac上でシミュレータ、データセット、ジェスチャーを固定し、XCTMetricで実行時間、CPU、メモリを継続的に測定したうえで、ベースラインを超えた変化だけを継続的インテグレーションで検出する方法です。

結果に影響する変数を固定する

性能ゲートを構築する最初の作業は、アサーションを書くことではなく、実行環境のノイズを減らすことです。XcodeとmacOSのバージョン、シミュレータの機種、システム言語、画面の向き、テストデータを固定します。テスト用アカウントは対象画面を直接開けるようにし、ログインリクエスト、画像のダウンロード、サーバーの応答時間がスクロール指標に混入しないようにします。

-UITestSeed fixed のように、UIテスト時だけ有効になる起動引数をアプリに追加します。この引数を指定した場合は、ローカルのフィクスチャを読み込み、決まった数のリスト項目を固定順で生成し、ランダムなアニメーションを無効にします。また、リストには feed.listfeed.reset のような安定したアクセシビリティ識別子が必要です。テストコードからローカライズ済みテキストを使って要素を検索しないでください。

初回実行時には通常、アプリのインストール、動的ライブラリのロード、フォントの初期化、キャッシュのウォームアップが含まれます。本番用のサンプルを取得する前に、計測対象外のテストを一度実行してください。ただし、キャッシュを削除し、日常的な利用状況とは異なる「完全なコールドスタート」環境を意図的に作るべきではありません。

性能ベースラインが表すのは環境全体であり、あらゆるMacやシミュレータに適用できる普遍的なミリ秒値ではありません。

再現可能なスクロール経路を作る

swipeUp() を一度実行するだけでは、開始位置やリストの長さに結果が左右されやすくなります。信頼性の高いテストにするには、まずリストの先頭に戻り、最初の基準要素が表示されていることを確認してから、決まった回数のジェスチャーを実行します。各計測の終了後には毎回リセットし、次の計測がリスト末尾から始まらないようにします。

import XCTest

final class FeedScrollPerformanceTests: XCTestCase {
    func testFeedScrollPerformance() {
        let app = XCUIApplication()
        app.launchArguments += ["-UITestSeed", "fixed"]
        app.launch()

        let list = app.collectionViews["feed.list"]
        XCTAssertTrue(list.waitForExistence(timeout: 10))

        app.buttons["feed.reset"].tap()
        XCTAssertTrue(app.cells["feed.item.0"].waitForExistence(timeout: 5))

        let options = XCTMeasureOptions()
        options.iterationCount = 5

        measure(
            metrics: [XCTClockMetric(), XCTCPUMetric(), XCTMemoryMetric()],
            options: options
        ) {
            app.buttons["feed.reset"].tap()
            for _ in 0..<6 {
                list.swipeUp(velocity: .fast)
            }
        }
    }
}

リセットボタンはテストモードでのみ表示してもかまいませんが、データを再取得するのではなく、常に同じ位置へスクロールする必要があります。画面でページネーションを使用している場合は、ネットワーク層をローカルレスポンスに置き換えるか、ページネーションの所要時間を別のテスト項目として明確に分離してください。

固定した実行先でテストし、結果を保存する

まず利用可能なシミュレータを一覧表示し、継続的インテグレーション環境でデバイスが暗黙に切り替わっていないことを確認します。続いて、テスト結果バンドルに一意のパスを指定します。古い結果バンドルが存在する場合、xcodebuild は上書きを拒否するため、事前に削除しなければなりません。

set -euo pipefail

RESULT="$PWD/artifacts/FeedScroll.xcresult"
rm -rf "$RESULT"
mkdir -p "$PWD/artifacts"

xcodebuild test \
  -workspace App.xcworkspace \
  -scheme AppUITests \
  -destination 'platform=iOS Simulator,name=iPhone 16,OS=latest' \
  -only-testing:AppUITests/FeedScrollPerformanceTests \
  -resultBundlePath "$RESULT"

OS=latest は、常に最新のツールチェーンへ追従するパイプラインに適しています。長期的に比較する場合は、特定のランタイムを固定し、XcodeやOSをアップグレードした後にベースラインを再構築してください。OVPSノード上で実行するジョブでも、ホスト構成、コミット番号、Xcodeのバージョン、シミュレータのランタイムを記録し、環境の更新をコードの性能低下と誤認しないようにします。

現在のXcodeに付属する xcresulttool を使用する前に、ヘルプを確認してください。ツールチェーンによってサブコマンドが変更される可能性があります。スクリプトとテストレポートパーサーのどちらを使用する場合でも、テスト失敗時には元の xcresult を添付ファイルとして保存します。

中央値と相対しきい値で判定する

一度だけ遅いサンプルが出たことを理由に、マージをブロックしてはいけません。コミットごとに5〜7回実行し、ウォームアップ回を除外してから中央値を計算します。ベースラインには、最初の計測値を永久に保存するのではなく、メインブランチで直近に実行された安定した複数回の結果から中央値を採用できます。

変化は3種類に分けて確認することを推奨します。総実行時間はユーザーの待ち時間を示し、CPU指標からはレイアウトの繰り返し処理や過剰な描画を見つけやすくなります。メモリ指標は、画像キャッシュやビューの再利用に関する問題の検出に役立ちます。あるコミットで実行時間だけが増加し、CPUとメモリが安定している場合は、まずシミュレータの負荷を確認してください。3つの指標が同時に悪化している場合は、アプリケーションコードの回帰である可能性が高くなります。

しきい値には比率を使用し、最小絶対差も設定します。たとえば、実行時間の中央値がベースラインより12%を超えて増加し、なおかつ絶対増加量がチームで定義したノイズ下限を超えた場合にのみ失敗とします。しきい値は、同じ環境における過去の変動を基に決定する必要があり、他のプロジェクトの値をそのまま流用してはいけません。

失敗時の証拠を保全し、原因範囲を絞り込む

性能ゲートが失敗しても、すぐにベースラインを上書きしてはいけません。まず同じコミットをもう一度実行します。それでも失敗する場合は、xcresult、完全なログ、テストデータのバージョン、OSとツールチェーンのバージョン、CPU、メモリ、実行時間のサマリーを保存します。そのうえで、対象となるコミット範囲を絞り込み、画像デコード、シャドウとぼかし、Auto Layout制約、メインスレッドでの同期I/O、リスト項目の再利用、ログ出力を重点的に確認します。

「指標の悪化」と「テスト経路の変化」も区別する必要があります。アクセシビリティ識別子が機能しなくなった場合や、リセット操作で先頭に戻らなくなった場合、テスト自体は実行できていても、以前とは異なる画面領域を計測している可能性があります。単に再試行回数を増やすよりも、重要な開始地点と終了地点に表示状態のアサーションを追加するほうが効果的です。

ベースラインの更新は独立した変更としてコミットし、環境のアップグレードや許容可能な操作変更について説明するとともに、更新前後のサンプルを添付してください。これにより、偶発的なノイズで性能ゲートが頻繁に作動するのを防ぎながら、安易なベースライン更新によってゲートが機能しなくなる事態も避けられます。

よくある質問

固定のミリ秒値をしきい値にするべきですか?

異なる環境で共通の固定値を使うべきではありません。同じMac、OS、シミュレータ、データで基準を作り、安定した実行結果の中央値に対する相対値で判定します。

最初の実行を基準から除外するのはなぜですか?

アプリのインストール、動的ライブラリの読み込み、フォント初期化、キャッシュの準備が含まれるためです。計測前に採点しないウォームアップを一度実行します。

失敗時に保存すべき情報は何ですか?

xcresult、テストログ、コミットID、XcodeとmacOSのバージョン、シミュレータ機種、テストデータの版、CPUとメモリの計測値を保存します。

OVPS クラウドMac

次回のビルドを専有物理ノードで実行する

3種類の Apple Silicon 構成と、販売中の6つのノードから選択できます。実際の利用可能状況は、コンソールにリアルタイムで表示される情報をご確認ください。

構成を選んで注文する