mcpField
M
metrickit-diagnostics

metrickit-diagnostics

github.com/dpearson2699開発1.2k409 回インストール13 週間前に更新
リポジトリ

metrickit-diagnostics について

Appleプラットフォームのアプリ開発者向けに、MetricKitによる診断方法を解説するスキルです。性能指標や診断データの収集、保存、分析、アップロードを扱います。

主な機能

  1. 01指標と診断ペイロードの購読
  2. 02起動・CPU・メモリ・ネットワーク指標の確認
  3. 03シンボル化に使うコールスタックの抽出
  4. 04カスタムSignpost指標の収集
  5. 05JSONペイロードの保存とアップロード
  6. 06拡張起動タスクの計測

活用シーン

  1. 01本番アプリのクラッシュやハングの調査
  2. 02アプリのバージョン間での性能変化の分析
  3. 03端末指標の分析バックエンドへの送信

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/dpearson2699/swift-ios-skills --skill metrickit-diagnostics

インストール後、Claude Code で/metrickit-diagnosticsを入力して読み込まれたか確認します。

詳細

MetricKit の診断

MetricKit を使用して、本番環境のデバイスから集計済みのパフォーマンス指標とクラッシュ診断を収集します。このフレームワークは、日次の指標ペイロード(CPU、メモリ、起動時間、ハング率、アニメーションのカクつき、ネットワーク使用量)と、トリアージに役立つ完全なコールスタックツリーを含む即時の診断ペイロード(クラッシュ、ハング、ディスク書き込み例外)を配信します。

目次

サブスクライバーの設定

サブスクライバーはできるだけ早く登録してください。理想的には、 application(_:didFinishLaunchingWithOptions:) または App.init で登録します。MetricKit は、MXMetricManager.shared に初めてアクセスした後、レポートの蓄積を開始します。

import MetricKit

final class MetricsSubscriber: NSObject, MXMetricManagerSubscriber {
    static let shared = MetricsSubscriber()

    func subscribe() {
        MXMetricManager.shared.add(self)
    }

    func unsubscribe() {
        MXMetricManager.shared.remove(self)
    }

    func didReceive(_ payloads: [MXMetricPayload]) {
        // Handle daily metrics
    }

    func didReceive(_ payloads: [MXDiagnosticPayload]) {
        // Handle diagnostics (crashes, hangs, disk writes)
    }
}
UIKit での登録
func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
    MetricsSubscriber.shared.subscribe()
    return true
}
SwiftUI での登録
@main
struct MyApp: App {
    init() {
        MetricsSubscriber.shared.subscribe()
    }
    var body: some Scene {
        WindowGroup { ContentView() }
    }
}

指標ペイロードの受信

MXMetricPayload は、集計済みの指標を含み、約 24 時間ごとに届きます。以前の配信を受信できなかった場合、配列には複数のペイロードが含まれることがあります。

func didReceive(_ payloads: [MXMetricPayload]) {
    for payload in payloads {
        let begin = payload.timeStampBegin
        let end = payload.timeStampEnd
        let version = payload.latestApplicationVersion

        // Persist raw JSON before processing
        let jsonData = payload.jsonRepresentation()
        persistPayload(jsonData, from: begin, to: end)

        processMetrics(payload)
    }
}

利用可能環境:MXMetricPayload — iOS 13.0 以降、macOS 10.15 以降、visionOS 1.0 以降

診断ペイロードの受信

MXDiagnosticPayload は、クラッシュ、ハング、CPU 例外、ディスク書き込み、アプリ起動に関する診断を配信します。iOS 15 以降および macOS 12 以降では、診断は日次レポートにまとめられず、即時に届きます。

func didReceive(_ payloads: [MXDiagnosticPayload]) {
    for payload in payloads {
        let jsonData = payload.jsonRepresentation()
        persistPayload(jsonData)

        if let crashes = payload.crashDiagnostics {
            for crash in crashes {
                handleCrash(crash)
            }
        }
        if let hangs = payload.hangDiagnostics {
            for hang in hangs {
                handleHang(hang)
            }
        }
        if let diskWrites = payload.diskWriteExceptionDiagnostics {
            for diskWrite in diskWrites {
                handleDiskWrite(diskWrite)
            }
        }
        if let cpuExceptions = payload.cpuExceptionDiagnostics {
            for cpuException in cpuExceptions {
                handleCPUException(cpuException)
            }
        }
        if let launchDiags = payload.appLaunchDiagnostics {
            for launchDiag in launchDiags {
                handleSlowLaunch(launchDiag)
            }
        }
    }
}

利用可能環境:MXDiagnosticPayload — iOS 14.0 以降、macOS 12.0 以降、visionOS 1.0 以降

主な指標

起動時間 — MXAppLaunchMetric
if let launch = payload.applicationLaunchMetrics {
    let firstDraw = launch.histogrammedTimeToFirstDraw
    let optimized = launch.histogrammedOptimizedTimeToFirstDraw
    let resume = launch.histogrammedApplicationResumeTime
    let extended = launch.histogrammedExtendedLaunch
}
実行時間 — MXAppRunTimeMetric
if let runTime = payload.applicationTimeMetrics {
    let fg = runTime.cumulativeForegroundTime    // Measurement<UnitDuration>
    let bg = runTime.cumulativeBackgroundTime
    let bgAudio = runTime.cumulativeBackgroundAudioTime
    let bgLocation = runTime.cumulativeBackgroundLocationTime
}
CPU、メモリ、応答性
if let cpu = payload.cpuMetrics {
    let cpuTime = cpu.cumulativeCPUTime              // Measurement<UnitDuration>
}
if let memory = payload.memoryMetrics {
    let peakMemory = memory.peakMemoryUsage           // Measurement<UnitInformationStorage>
}
if let responsiveness = payload.applicationResponsivenessMetrics {
    let hangTime = responsiveness.histogrammedApplicationHangTime
}
if let animation = payload.animationMetrics {
    let scrollHitchRate = animation.scrollHitchTimeRatio  // Measurement<Unit>
}
ネットワークとセルラー
if let network = payload.networkTransferMetrics {
    let wifiUp = network.cumulativeWifiUpload          // Measurement<UnitInformationStorage>
    let wifiDown = network.cumulativeWifiDownload
    let cellUp = network.cumulativeCellularUpload
    let cellDown = network.cumulativeCellularDownload
}
アプリ終了の指標
if let exits = payload.applicationExitMetrics {
    let fg = exits.foregroundExitData
    let bg = exits.backgroundExitData
    // Inspect normal, abnormal, watchdog, memory, etc.
}

コールスタックツリー

MXCallStackTree は各診断(クラッシュ、ハング、CPU 例外、ディスク書き込み、アプリ起動)に付属します。jsonRepresentation() を使用して抽出し、シンボリケートします。

func handleCrash(_ crash: MXCrashDiagnostic) {
    let tree = crash.callStackTree
    let treeJSON = tree.jsonRepresentation()

    let exceptionType = crash.exceptionType
    let signal = crash.signal
    let reason = crash.terminationReason

    uploadDiagnostic(
        type: "crash",
        exceptionType: exceptionType,
        signal: signal,
        reason: reason,
        callStack: treeJSON
    )
}

func handleHang(_ hang: MXHangDiagnostic) {
    let tree = hang.callStackTree
    let duration = hang.hangDuration  // Measurement<UnitDuration>
    uploadDiagnostic(type: "hang", duration: duration, callStack: tree.jsonRepresentation())
}

JSON 構造には、バイナリ名、オフセット、アドレスを含むコールスタックフレームの配列があります。atos を使用してシンボリケートするか、dSYM を分析サービスにアップロードしてください。

利用可能環境:MXCallStackTree — iOS 14.0 以降、macOS 12.0 以降、visionOS 1.0 以降

カスタムサインポスト指標

MetricKit のログハンドルで mxSignpost を使用すると、カスタムのパフォーマンス区間を記録できます。これらは日次の MXMetricPayload 内の signpostMetrics に表示されます。

ログハンドルの作成
let metricLog = MXMetricManager.makeLogHandle(category: "Networking")
サインポストの記録
import os

func fetchData() async throws -> Data {
    let signpostID = MXSignpostIntervalData.makeSignpostID(log: metricLog)

    mxSignpost(.begin, log: metricLog, name: "DataFetch", signpostID: signpostID)
    let data = try await URLSession.shared.data(from: url).0
    mxSignpost(.end, log: metricLog, name: "DataFetch", signpostID: signpostID)

    return data
}
ペイロードからのカスタム指標の読み取り
if let signposts = payload.signpostMetrics {
    for metric in signposts {
        let name = metric.signpostName       // "DataFetch"
        let category = metric.signpostCategory // "Networking"
        let count = metric.totalCount
        if let intervalData = metric.signpostIntervalData {
            let avgMemory = intervalData.averageMemory
            let cumulativeCPUTime = intervalData.cumulativeCPUTime
        }
    }
}

システムは、デバイス上のオーバーヘッドを軽減するため、ログごとのカスタムサインポスト指標の数を制限します。カスタム指標は重要なコードパスに限って使用してください。

ペイロードのエクスポートとアップロード

どちらのペイロード型も NSSecureCoding に準拠しており、シリアル化に便利な jsonRepresentation() を提供します。

func persistPayload(_ jsonData: Data, from: Date? = nil, to: Date? = nil) {
    let fileName = "metrics_\(ISO8601DateFormatter().string(from: Date())).json"
    let url = FileManager.default.temporaryDirectory.appending(path: fileName)
    try? jsonData.write(to: url)
}

func uploadPayloads(_ jsonData: Data) {
    Task.detached(priority: .utility) {
        var request = URLRequest(url: URL(string: "https://api.example.com/metrics")!)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.httpBody = jsonData
        _ = try? await URLSession.shared.data(for: request)
    }
}
過去のペイロードの取得

ペイロードが届いたときにサブスクライバーが登録されていなかった場合は、pastPayloads と pastDiagnosticPayloads を使用して取得できます。これらは共有マネージャーが最後に割り当てられて以降に生成されたレポートを返します。

let pastMetrics = MXMetricManager.shared.pastPayloads
let pastDiags = MXMetricManager.shared.pastDiagnosticPayloads

拡張起動計測

拡張起動計測を使用すると、初回描画後のセットアップ作業(データベースの読み込み、状態の復元)を起動指標の一部として追跡できます。

let taskID = MXLaunchTaskID("com.example.app.loadDatabase")

MXMetricManager.shared.extendLaunchMeasurement(forTaskID: taskID)
// Perform extended launch work...
await database.load()
MXMetricManager.shared.finishExtendedLaunchMeasurement(forTaskID: taskID)

拡張起動時間は、MXAppLaunchMetric 内の histogrammedExtendedLaunch に表示されます。

Xcode Organizerとの統合

Xcode Organizerには、診断情報の共有に同意したすべてのユーザーから集約された、同じMetricKitデータが表示されます。トレンド分析にはOrganizerを使用します。

  • Metricsタブ:バッテリー、パフォーマンス、ディスク書き込みの各メトリックの経時的な推移
  • Regressionsタブ:バージョンごとのメトリックのリグレッションを自動検出
  • Crashesタブ:シンボリケーション済みスタックトレースを含むクラッシュログ

デバイス上のMetricKitによる収集をOrganizerと併用すると、生データを独自のバックエンドに送信し、カスタムダッシュボード、アラート、ユーザーコホートごとのフィルタリングに利用できます。

よくある間違い

やってはいけないこと:MXMetricManagerへの登録が遅すぎる

システムは起動直後に保留中のペイロードを配信する場合があります。登録が遅れると(たとえば、ビューコントローラー内で登録すると)、ペイロードを完全に取り逃すおそれがあります。

// WRONG — subscribing in a view controller
override func viewDidLoad() {
    super.viewDidLoad()
    MXMetricManager.shared.add(self)
}

// CORRECT — subscribe in application(_:didFinishLaunchingWithOptions:)
func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions opts: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
    MXMetricManager.shared.add(metricsSubscriber)
    return true
}
やってはいけないこと:MXDiagnosticPayloadを無視する

MXMetricPayloadだけを処理すると、MetricKitが提供する最も実用的なデータであるクラッシュ、ハング、ディスク書き込みの診断情報を見逃します。

// WRONG — only implementing metric callback
func didReceive(_ payloads: [MXMetricPayload]) { /* ... */ }

// CORRECT — implement both callbacks
func didReceive(_ payloads: [MXMetricPayload]) { /* ... */ }
func didReceive(_ payloads: [MXDiagnosticPayload]) { /* ... */ }
やってはいけないこと:永続化せずにペイロードを処理する

システムは各ペイロードを一度だけ配信します。処理中にサブスクライバーがクラッシュすると、データは永久に失われます。

// WRONG — process inline, crash loses data
func didReceive(_ payloads: [MXDiagnosticPayload]) {
    for p in payloads {
        riskyProcessing(p)  // If this crashes, payload is gone
    }
}

// CORRECT — persist raw JSON first, then process
func didReceive(_ payloads: [MXDiagnosticPayload]) {
    for p in payloads {
        let json = p.jsonRepresentation()
        try? json.write(to: localCacheURL())   // Safe on disk
        Task.detached { self.processAsync(json) }
    }
}
やってはいけないこと:didReceive内で重い処理を同期的に行う

コールバックは任意のスレッドで実行されます。重い処理や同期的なネットワーク呼び出しでブロックすると、後続のペイロードの配信が遅れます。

// WRONG — synchronous upload in callback
func didReceive(_ payloads: [MXMetricPayload]) {
    for p in payloads {
        let data = p.jsonRepresentation()
        URLSession.shared.uploadTask(with: request, from: data).resume()  // sync wait
    }
}

// CORRECT — persist and dispatch async
func didReceive(_ payloads: [MXMetricPayload]) {
    for p in payloads {
        let json = p.jsonRepresentation()
        persistLocally(json)
        Task.detached(priority: .utility) {
            await self.uploadToBackend(json)
        }
    }
}
やってはいけないこと:開発中にデータがすぐ届くと期待する

MetricKitは24時間単位でデータを集約します。計測を開始しても、ペイロードはすぐには届きません。開発中に素早く反復するには、Xcode Organizerまたはシミュレートしたペイロードを使用してください。

レビューチェックリスト

  • MXMetricManager.shared.add(subscriber)がapplication(_:didFinishLaunchingWithOptions:)またはApp.initで呼び出されている
  • サブスクライバーがMXMetricManagerSubscriberに準拠し、NSObjectを継承している
  • didReceive(_: [MXMetricPayload])とdidReceive(_: [MXDiagnosticPayload])の両方が実装されている
  • 生のjsonRepresentation()が処理前にディスクへ永続化されている
  • 重い処理がコールバックのスレッドから非同期で実行される
  • シンボリケーションのため、MXCallStackTreeのJSONがdSYMとともにアップロードされている
  • カスタムsignpostメトリックが重要なコードパスに限定されている
  • 配信漏れがないか、起動時にpastPayloadsとpastDiagnosticPayloadsを確認している
  • 起動延長タスクでextendLaunchMeasurementとfinishExtendedLaunchMeasurementの両方を呼び出している
  • 分析バックエンドがMetricKitのJSON形式を受け入れ、保存している
  • デバイス上のデータと併せて、Xcode Organizerでリグレッションの傾向を確認している

参考資料

コメント