metrickit-diagnostics について
Appleプラットフォームのアプリ開発者向けに、MetricKitによる診断方法を解説するスキルです。性能指標や診断データの収集、保存、分析、アップロードを扱います。
主な機能
- 01指標と診断ペイロードの購読
- 02起動・CPU・メモリ・ネットワーク指標の確認
- 03シンボル化に使うコールスタックの抽出
- 04カスタムSignpost指標の収集
- 05JSONペイロードの保存とアップロード
- 06拡張起動タスクの計測
活用シーン
- 01本番アプリのクラッシュやハングの調査
- 02アプリのバージョン間での性能変化の分析
- 03端末指標の分析バックエンドへの送信
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ dpearson2699/ swift-ios-skills --skill metrickit-diagnosticsインストール後、Claude Code で/metrickit-diagnosticsを入力して読み込まれたか確認します。
詳細
MetricKit の診断
MetricKit を使用して、本番環境のデバイスから集計済みのパフォーマンス指標とクラッシュ診断を収集します。このフレームワークは、日次の指標ペイロード(CPU、メモリ、起動時間、ハング率、アニメーションのカクつき、ネットワーク使用量)と、トリアージに役立つ完全なコールスタックツリーを含む即時の診断ペイロード(クラッシュ、ハング、ディスク書き込み例外)を配信します。
目次
- サブスクライバーの設定
- 指標ペイロードの受信
- 診断ペイロードの受信
- 主な指標
- コールスタックツリー
- カスタムサインポスト指標
- ペイロードのエクスポートとアップロード
- 拡張起動計測
- Xcode Organizer との連携
- よくある間違い
- レビュー用チェックリスト
- 参考資料
サブスクライバーの設定
サブスクライバーはできるだけ早く登録してください。理想的には、
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でリグレッションの傾向を確認している