WWDC Quick Look 💓 By SwiftGGTeam
新しいMetricKitを知る

新しいMetricKitを知る

元の動画を見る

ハイライト

iOS 27ではMetricKitがSwiftで全面的に書き直され、新しいMetricManagerStateReportingフレームワークが導入されます。開発者は、現在のタブやユーザーフローといったアプリの状態ごとにパフォーマンス指標をクロス分析でき、さらにメモリ例外診断とMetalフレームレート指標も利用できます。

重要ポイント

アプリを公開したあと、ユーザーから「少し重い」と言われたとします。Xcode Instrumentsを開くと、すべて正常に見えます。では問題はどこにあるのでしょうか。実機上のパフォーマンス問題は、シミュレータ上の挙動とは大きく違います。ユーザーは古い端末を使っているかもしれませんし、バッテリー残量が少ない状態や、通信環境が悪い場所で使っているかもしれません。必要なのは、現実の利用環境から得られるデータです。

MetricKitはそのための仕組みです。アプリが実際のデバイス上で記録したパフォーマンスデータを、毎日レポートとしてまとめてアプリに届けます。そのデータを自分のサーバーにアップロードし、集計して問題を見つけられます。

ただし、従来のMetricKitには大きな弱点がありました。すべての指標がグローバル平均だったことです。アプリに3つのタブがあり、MetricKitが「1日の平均スクロールひっかかり率は15ms/s」と教えてくれたとします。この数値自体は正しくても、どのタブで起きたのかは分かりません。Spendingタブは非常に滑らかで、Reportsタブだけが使いものにならない可能性があります。グローバル平均は問題を埋もれさせます。

iOS 27の解決策は2つです。Swiftファーストの新しいAPIと、StateReportingというフレームワークです。

新APIでは古いdelegateコールバックを捨て、async forループを使います。MetricManagermetricReportsdiagnosticReportsという2つのAsyncSequenceを提供し、コードはかなりすっきりします。レポート自体もCodableになったので、JSONEncoderでそのままエンコードしてサーバーへ送れます。手書きの解析ロジックは不要です。

StateReportingが解決するのは、「指標をどの軸で分けるべきか」という問題です。現在どのタブにいるか、どのユーザーフローにいるかなど、アプリの重要な状態を定義すると、MetricKitはその状態ごとに指標を集計します。先ほどの例なら、StateReportingを追加したあとに、Spendingタブのひっかかり率は1ms/sしかない一方で、Reportsタブは71ms/sに達していると分かります。最適化すべき場所がすぐ明確になります。

詳細

新しいMetricManager API

00:04:58

iOS 27のMetricKitは純粋なSwiftで書き直されています。入口はMXMetricManagerからMetricManagerに変わります。

import MetricKit

let manager = MetricManager()

for await report in manager.metricReports {
    processReport(report)
}

ポイント:

  • MetricManager()は新しい入口クラスで、従来のMXMetricManagerを置き換えます
  • metricReportsAsyncSequenceで、for awaitを使って日次レポートを受け取ります
  • このコードはアプリ起動時にできるだけ早く実行する必要があります。購読が遅れると、その日のデータを取り逃がす可能性があります
  • 以後のレポートを継続して受け取るため、MetricManagerは生存させておく必要があります

00:05:25

レポートはCodableなので、サーバーへ送るコードは数行で済みます。

import MetricKit

for await report in manager.metricReports {
    let jsonData = try JSONEncoder().encode(report)
    sendToServer(jsonData)
}

ポイント:

  • MetricReportCodableに準拠しています
  • レポート全体をJSONEncoderで直接エンコードでき、手書きの解析モデルは不要です
  • sendToServerは自分のネットワーク層のメソッドです

00:05:44

特定の指標をローカルで分析したい場合は、レポート内の区間エントリを走査します。

import MetricKit

for await report in manager.metricReports {
    let intervalEntries = report.intervalEntries
    let fullDayEntry = intervalEntries.fullDayEntry

    for entry in intervalEntries {
        let memoryMetrics = entry.values.filter { $0.metricGroup == .memory }

        for metric in memoryMetrics {
            switch metric {
            case .peakMemory(let peak):
                processPeakMemory(peak)
            default: break
            }
        }
    }
}

ポイント:

  • intervalEntriesには、1日全体の集計エントリと、複数の時間単位の細分化エントリが含まれます
  • fullDayEntryは1日全体の集計データです
  • 各エントリのvaluesは、.memory.cpu.display.gpuなどのmetricGroupで分類されます
  • .peakMemoryのような具体的な指標型はswitchでマッチします
  • この処理はdetached Taskや専用のサービスクラスに置くべきです

診断レポート: 指標から根本原因へ

00:08:59

指標は「問題がある」と教えてくれます。診断は「どこで起きたか」を教えてくれます。

import MetricKit

let manager = MetricManager()

for await report in manager.diagnosticReports {
    processReport(report)
}

ポイント:

  • diagnosticReportsAsyncSequenceです
  • 診断レポートは日次集計を待たず、イベント発生時にすぐ配信されます
  • こちらもアプリ起動時にできるだけ早く監視を始める必要があります

00:09:14

診断レポートもCodableに対応しています。

import MetricKit

for await report in manager.diagnosticReports {
    let jsonData = try JSONEncoder().encode(report)
    sendToServer(jsonData)
}

00:09:39

診断内容には構造化してアクセスできます。

import MetricKit

for await report in manager.diagnosticReports {
    switch report.result {
    case .crash(let crash):
        let backtrace = crash.callStackTree
        let reason = crash.terminationReason
        let category = crash.terminationCategory
        processCrash(backtrace: backtrace, reason: reason, category: category)
    case .hang(let hang):
        processHangDiagnostic(hang)
    default: break
    }
}

ポイント:

  • report.result.crash.hang.memoryExceptionなどの診断種別をenumで表します
  • crash.callStackTreeはシンボル化されたスタックで、問題のコードを直接示します
  • terminationReasonはクラッシュ理由を説明します
  • terminationCategoryはiOS 27で追加されたフィールドで、クラッシュが指標上どのカテゴリに分類されるかを示し、診断と指標を関連付けやすくします

StateReporting: アプリ状態ごとに指標を分ける

00:13:57

これはiOS 27のMetricKitで最も重要な新機能です。状態を定義すると、MetricKitがその状態ごとに指標を集計します。

import MetricKit
import StateReporting

let domain = StateReportingDomain("com.metrickitsample.tabs")
let manager = MetricManager(enabledStateReportingDomains: [domain])

let reporter = StateReporter.reporter(for: domain.rawValue)
reporter.reportTransition(to: "Reports")

ポイント:

  • StateReportingDomainは状態ドメインを定義します。通常は逆DNS形式を使います
  • MetricManagerを作るときに、enabledStateReportingDomainsへドメインを渡します
  • StateReporter.reporter(for:)で、そのドメインのレポーターを取得します
  • reportTransition(to:)は現在の状態へ入ったことを報告します。「終了」はなく、「どの状態へ切り替わったか」だけを扱います

00:14:21

@ReportableMetadataマクロで構造化メタデータを付けられます。

import StateReporting

@ReportableMetadata
struct ViewConfiguration {
    let listSize: String
    let isSorted: Bool
}

let reporter = StateReporter.reporter(
    for: domain.rawValue,
    stableMetadata: ViewConfiguration.self
)

reporter.reportTransition(
    to: "Reports",
    stableMetadata: ViewConfiguration(listSize: "large", isSorted: false)
)

ポイント:

  • @ReportableMetadataマクロはカスタム構造体をマークし、コンパイラがシリアライズコードを生成します
  • stableMetadataパラメータでメタデータ型を指定します
  • 状態を報告するとき、具体的なメタデータインスタンスも渡します
  • これによりMetricKitは、「Reportsタブの大きな未ソートリスト」と「Reportsタブの小さなソート済みリスト」のパフォーマンス差を区別できます

00:15:29

状態ごとにグループ化してエンコードします。

import MetricKit

for await report in manager.metricReports {
    let encoder = JSONEncoder()

    let formatKey = MetricReport.encodingFormatKey
    encoder.userInfo[formatKey] = MetricReport.EncodingFormat.byStateReportingDomain

    let jsonData = try encoder.encode(report)
    sendToServer(jsonData)
}

ポイント:

  • MetricReport.encodingFormatKeyはエンコード形式の設定キーです
  • MetricReport.EncodingFormat.byStateReportingDomainは、状態ドメインと状態ごとにグループ化したJSONを出力します
  • サーバー側では、状態という軸で直接集計・分析できます

iOS 27で追加された指標と診断

00:03:23

Metalフレームレート指標。ゲーム開発者は、レンダリングフレームレートの分布データをMetricKitから直接取得できます。自前で統計ロジックを書く必要はありません。

00:03:55

メモリ例外診断。アプリや拡張機能がメモリ制限を超えてシステムに終了されたとき、MetricKitはスタックと終了理由を含む診断レポートを配信します。これは以前にはなかったものです。

実装アイデア

1. 主要ユーザーフローごとのパフォーマンス像を作る

StateReportingを使い、ホーム画面の読み込み、商品詳細、支払い確認、動画再生など、アプリの重要な流れを独立した状態として定義します。公開から1週間後にデータを見ると、どの流れでHang率やメモリピークが異常なのか一目で分かります。入口はStateReportingDomainStateReporter.reportTransition(to:)です。

2. A/B実験でパフォーマンスデータを自動収集する

実験グループもStateReportingドメインとして扱います。実験群と対照群のパフォーマンス指標が自動で分かれ、実験コードに手動でパフォーマンス計測点を埋め込む必要がありません。実験結果はビジネス指標だけでなく、パフォーマンス指標も合わせて評価できます。

3. リアルタイム診断アラートを作る

診断レポートは日次集計ではなく即時配信されます。サーバーがCrashやMemory Exceptionの診断を受け取ったら、すぐアラートを出せます。terminationCategoryフィールドを組み合わせると、「緊急修正が必要なクラッシュ」と「既知のシステムレベル終了」を区別できます。

4. 複雑なリスト画面を設定単位で監視する

@ReportableMetadataを使い、データ量、ソート有無、画像の遅延読み込み有無など、リスト画面の設定をメタデータとして報告します。「大きなリスト+未ソート」の組み合わせでスクロールひっかかり率が急増していると分かれば、最適化方針はすぐ明確になります。

5. 旧MXMetricManagerから移行する

すでにMXMetricManagerを使っている場合、移行は分かりやすいです。delegateコールバックをfor awaitループへ置き換え、手書きのJSON解析をJSONEncoder().encode(report)へ置き換えます。新APIがフレームワークの今後の中心であり、新機能はすべて新APIだけで提供されます。

関連セッション

コメント

GitHub Issues · utterances