axiom-metrickit-ref by charleswiltgen/axiom
npx skills add https://github.com/charleswiltgen/axiom --skill axiom-metrickit-ref使用 MetricKit 收集现场性能指标和诊断的完整 API 参考。
MetricKit 提供来自选择共享分析数据的用户的聚合、设备端性能和诊断数据。数据每日交付(或在开发中按需交付)。
在以下情况下使用此参考:
对于卡顿诊断工作流,请参阅 axiom-hang-diagnostics。对于使用 Instruments 进行常规性能分析,请参阅 axiom-performance-profiling。对于包括 jetsam 在内的内存调试,请参阅 axiom-memory-debugging。
iOS 版本支持 :
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
| 功能 | iOS 版本 |
|---|---|
| 基础指标(电池、CPU、内存) | iOS 13+ |
| 诊断负载 | iOS 14+ |
| 卡顿诊断 | iOS 14+ |
| 启动诊断 | iOS 16+ |
| 开发环境即时交付 | iOS 15+ |
import MetricKit
class AppMetricsSubscriber: NSObject, MXMetricManagerSubscriber {
override init() {
super.init()
MXMetricManager.shared.add(self)
}
deinit {
MXMetricManager.shared.remove(self)
}
// MARK: - MXMetricManagerSubscriber
func didReceive(_ payloads: [MXMetricPayload]) {
for payload in payloads {
processMetrics(payload)
}
}
func didReceive(_ payloads: [MXDiagnosticPayload]) {
for payload in payloads {
processDiagnostics(payload)
}
}
}
在应用生命周期的早期注册订阅者:
@main
struct MyApp: App {
@StateObject private var metricsSubscriber = AppMetricsSubscriber()
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
或在 AppDelegate 中:
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
metricsSubscriber = AppMetricsSubscriber()
return true
}
在 iOS 15+ 中,通过调试菜单触发即时交付:
Xcode > Debug > Simulate MetricKit Payloads
或以编程方式(仅限调试版本):
#if DEBUG
// Payloads delivered immediately in development
// No special code needed - just run and wait
#endif
MXMetricPayload 包含过去 24 小时的聚合性能指标。
func processMetrics(_ payload: MXMetricPayload) {
// Time range for this payload
let start = payload.timeStampBegin
let end = payload.timeStampEnd
// App version that generated this data
let version = payload.metaData?.applicationBuildVersion
// Access specific metric categories
if let cpuMetrics = payload.cpuMetrics {
processCPU(cpuMetrics)
}
if let memoryMetrics = payload.memoryMetrics {
processMemory(memoryMetrics)
}
if let launchMetrics = payload.applicationLaunchMetrics {
processLaunches(launchMetrics)
}
// ... other categories
}
func processCPU(_ metrics: MXCPUMetric) {
// Cumulative CPU time
let cpuTime = metrics.cumulativeCPUTime // Measurement<UnitDuration>
// iOS 14+: CPU instruction count
if #available(iOS 14.0, *) {
let instructions = metrics.cumulativeCPUInstructions // Measurement<Unit>
}
}
func processMemory(_ metrics: MXMemoryMetric) {
// Peak memory usage
let peakMemory = metrics.peakMemoryUsage // Measurement<UnitInformationStorage>
// Average suspended memory
let avgSuspended = metrics.averageSuspendedMemory // MXAverage<UnitInformationStorage>
}
func processLaunches(_ metrics: MXAppLaunchMetric) {
// First draw (cold launch) histogram
let firstDrawHistogram = metrics.histogrammedTimeToFirstDraw
// Resume time histogram
let resumeHistogram = metrics.histogrammedApplicationResumeTime
// Optimized time to first draw (iOS 15.2+)
if #available(iOS 15.2, *) {
let optimizedLaunch = metrics.histogrammedOptimizedTimeToFirstDraw
}
// Parse histogram buckets
for bucket in firstDrawHistogram.bucketEnumerator {
if let bucket = bucket as? MXHistogramBucket<UnitDuration> {
let start = bucket.bucketStart // e.g., 0ms
let end = bucket.bucketEnd // e.g., 100ms
let count = bucket.bucketCount // Number of launches in this range
}
}
}
@available(iOS 14.0, *)
func processExits(_ metrics: MXAppExitMetric) {
let fg = metrics.foregroundExitData
let bg = metrics.backgroundExitData
// Foreground (onscreen) exits
let fgNormal = fg.cumulativeNormalAppExitCount
let fgWatchdog = fg.cumulativeAppWatchdogExitCount
let fgMemoryLimit = fg.cumulativeMemoryResourceLimitExitCount
let fgMemoryPressure = fg.cumulativeMemoryPressureExitCount
let fgBadAccess = fg.cumulativeBadAccessExitCount
let fgIllegalInstruction = fg.cumulativeIllegalInstructionExitCount
let fgAbnormal = fg.cumulativeAbnormalExitCount
// Background exits
let bgSuspended = bg.cumulativeSuspendedWithLockedFileExitCount
let bgTaskTimeout = bg.cumulativeBackgroundTaskAssertionTimeoutExitCount
let bgCPULimit = bg.cumulativeCPUResourceLimitExitCount
}
@available(iOS 14.0, *)
func processHitches(_ metrics: MXAnimationMetric) {
// Scroll hitch rate (hitches per scroll)
let scrollHitchRate = metrics.scrollHitchTimeRatio // Double (0.0 - 1.0)
}
func processDiskIO(_ metrics: MXDiskIOMetric) {
let logicalWrites = metrics.cumulativeLogicalWrites // Measurement<UnitInformationStorage>
}
func processNetwork(_ metrics: MXNetworkTransferMetric) {
let cellUpload = metrics.cumulativeCellularUpload
let cellDownload = metrics.cumulativeCellularDownload
let wifiUpload = metrics.cumulativeWifiUpload
let wifiDownload = metrics.cumulativeWifiDownload
}
使用标记点跟踪自定义操作:
// In your code: emit signposts
import os.signpost
let log = MXMetricManager.makeLogHandle(category: "ImageProcessing")
func processImage(_ image: UIImage) {
mxSignpost(.begin, log: log, name: "ProcessImage")
// ... do work ...
mxSignpost(.end, log: log, name: "ProcessImage")
}
// In metrics subscriber: read signpost data
func processSignposts(_ metrics: MXSignpostMetric) {
let name = metrics.signpostName
let category = metrics.signpostCategory
// Histogram of durations
let histogram = metrics.signpostIntervalData.histogrammedSignpostDurations
// Total count
let count = metrics.totalCount
}
func exportPayload(_ payload: MXMetricPayload) {
// JSON representation for upload to analytics
let jsonData = payload.jsonRepresentation()
// Or as Dictionary
if let json = try? JSONSerialization.jsonObject(with: jsonData) as? [String: Any] {
uploadToAnalytics(json)
}
}
MXDiagnosticPayload 包含崩溃、卡顿、磁盘写入异常和 CPU 异常的诊断报告。
@available(iOS 14.0, *)
func processDiagnostics(_ payload: MXDiagnosticPayload) {
// Crash diagnostics
if let crashes = payload.crashDiagnostics {
for crash in crashes {
processCrash(crash)
}
}
// Hang diagnostics
if let hangs = payload.hangDiagnostics {
for hang in hangs {
processHang(hang)
}
}
// Disk write exceptions
if let diskWrites = payload.diskWriteExceptionDiagnostics {
for diskWrite in diskWrites {
processDiskWriteException(diskWrite)
}
}
// CPU exceptions
if let cpuExceptions = payload.cpuExceptionDiagnostics {
for cpuException in cpuExceptions {
processCPUException(cpuException)
}
}
}
@available(iOS 14.0, *)
func processCrash(_ diagnostic: MXCrashDiagnostic) {
// Call stack tree (needs symbolication)
let callStackTree = diagnostic.callStackTree
// Crash metadata
let signal = diagnostic.signal // e.g., SIGSEGV
let exceptionType = diagnostic.exceptionType // e.g., EXC_BAD_ACCESS
let exceptionCode = diagnostic.exceptionCode
let terminationReason = diagnostic.terminationReason
// Virtual memory info
let virtualMemoryRegionInfo = diagnostic.virtualMemoryRegionInfo
// Unique identifier for grouping similar crashes
// (not available - use call stack signature)
}
@available(iOS 14.0, *)
func processHang(_ diagnostic: MXHangDiagnostic) {
// How long the hang lasted
let duration = diagnostic.hangDuration // Measurement<UnitDuration>
// Call stack when hang occurred
let callStackTree = diagnostic.callStackTree
}
@available(iOS 14.0, *)
func processDiskWriteException(_ diagnostic: MXDiskWriteExceptionDiagnostic) {
// Total bytes written that triggered exception
let totalWrites = diagnostic.totalWritesCaused // Measurement<UnitInformationStorage>
// Call stack of writes
let callStackTree = diagnostic.callStackTree
}
@available(iOS 14.0, *)
func processCPUException(_ diagnostic: MXCPUExceptionDiagnostic) {
// Total CPU time that triggered exception
let totalCPUTime = diagnostic.totalCPUTime // Measurement<UnitDuration>
// Total sampled time
let totalSampledTime = diagnostic.totalSampledTime
// Call stack of CPU-intensive code
let callStackTree = diagnostic.callStackTree
}
MXCallStackTree 包含来自诊断的堆栈帧。帧未符号化——您必须使用您的 dSYM 进行符号化。
@available(iOS 14.0, *)
func parseCallStackTree(_ tree: MXCallStackTree) {
// JSON representation
let jsonData = tree.jsonRepresentation()
// Parse the JSON
guard let json = try? JSONSerialization.jsonObject(with: jsonData) as? [String: Any],
let callStacks = json["callStacks"] as? [[String: Any]] else {
return
}
for callStack in callStacks {
guard let threadAttributed = callStack["threadAttributed"] as? Bool,
let frames = callStack["callStackRootFrames"] as? [[String: Any]] else {
continue
}
// threadAttributed = true means this thread caused the issue
if threadAttributed {
parseFrames(frames)
}
}
}
func parseFrames(_ frames: [[String: Any]]) {
for frame in frames {
// Binary image UUID (match to dSYM)
let binaryUUID = frame["binaryUUID"] as? String
// Address offset within binary
let offsetIntoBinaryTextSegment = frame["offsetIntoBinaryTextSegment"] as? Int
// Binary name (e.g., "MyApp", "UIKitCore")
let binaryName = frame["binaryName"] as? String
// Address (for symbolication)
let address = frame["address"] as? Int
// Sample count (how many times this frame appeared)
let sampleCount = frame["sampleCount"] as? Int
// Sub-frames (tree structure)
let subFrames = frame["subFrames"] as? [[String: Any]]
}
}
{
"callStacks": [
{
"threadAttributed": true,
"callStackRootFrames": [
{
"binaryUUID": "A1B2C3D4-E5F6-7890-ABCD-EF1234567890",
"offsetIntoBinaryTextSegment": 123456,
"binaryName": "MyApp",
"address": 4384712345,
"sampleCount": 10,
"subFrames": [
{
"binaryUUID": "F1E2D3C4-B5A6-7890-1234-567890ABCDEF",
"offsetIntoBinaryTextSegment": 78901,
"binaryName": "UIKitCore",
"address": 7234567890,
"sampleCount": 10
}
]
}
]
}
]
}
MetricKit 调用堆栈未符号化。要进行符号化:
binaryUUID 到您的 dSYM# Find dSYM for binary UUID
mdfind "com_apple_xcode_dsym_uuids == A1B2C3D4-E5F6-7890-ABCD-EF1234567890"
# Symbolicate address
atos -arch arm64 -o MyApp.app.dSYM/Contents/Resources/DWARF/MyApp -l 0x100000000 0x105234567
或使用处理符号化的崩溃报告服务(Crashlytics、Sentry 等)。
跟踪您的应用在后台被终止的原因:
@available(iOS 14.0, *)
func analyzeBackgroundExits(_ data: MXBackgroundExitData) {
// Normal exits (user closed, system reclaimed)
let normal = data.cumulativeNormalAppExitCount
// Memory issues
let memoryLimit = data.cumulativeMemoryResourceLimitExitCount // Exceeded memory limit
let memoryPressure = data.cumulativeMemoryPressureExitCount // Jetsam
// Crashes
let badAccess = data.cumulativeBadAccessExitCount // SIGSEGV
let illegalInstruction = data.cumulativeIllegalInstructionExitCount // SIGILL
let abnormal = data.cumulativeAbnormalExitCount // Other crashes
// System terminations
let watchdog = data.cumulativeAppWatchdogExitCount // Timeout during transition
let taskTimeout = data.cumulativeBackgroundTaskAssertionTimeoutExitCount // Background task timeout
let cpuLimit = data.cumulativeCPUResourceLimitExitCount // Exceeded CPU quota
let lockedFile = data.cumulativeSuspendedWithLockedFileExitCount // File lock held
}
| 退出类型 | 含义 | 应对措施 |
|---|---|---|
normalAppExitCount | 正常退出 | 无(预期情况) |
memoryResourceLimitExitCount | 使用内存过多 | 减少内存占用 |
memoryPressureExitCount | Jetsam(系统回收) | 将后台内存减少到 <50MB |
badAccessExitCount | SIGSEGV 崩溃 | 检查空指针、无效内存 |
illegalInstructionExitCount | SIGILL 崩溃 | 检查无效函数指针 |
abnormalExitCount | 其他崩溃 | 检查崩溃诊断 |
appWatchdogExitCount | 状态转换期间挂起 | 减少启动/后台工作 |
backgroundTaskAssertionTimeoutExitCount | 未结束后台任务 | 正确调用 endBackgroundTask |
cpuResourceLimitExitCount | 后台 CPU 使用过多 | 移至 BGProcessingTask |
suspendedWithLockedFileExitCount | 挂起时持有文件锁 | 在挂起前释放锁 |
class MetricsUploader {
func upload(_ payload: MXMetricPayload) {
let jsonData = payload.jsonRepresentation()
var request = URLRequest(url: analyticsEndpoint)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = jsonData
URLSession.shared.dataTask(with: request) { _, response, error in
if let error = error {
// Queue for retry
self.queueForRetry(jsonData)
}
}.resume()
}
}
class HybridCrashReporter: MXMetricManagerSubscriber {
let crashlytics: Crashlytics // or Sentry, etc.
func didReceive(_ payloads: [MXDiagnosticPayload]) {
for payload in payloads {
// MetricKit captures crashes that traditional reporters might miss
// (e.g., watchdog kills, memory pressure exits)
if let crashes = payload.crashDiagnostics {
for crash in crashes {
crashlytics.recordException(
name: crash.exceptionType?.description ?? "Unknown",
reason: crash.terminationReason ?? "MetricKit crash",
callStack: parseCallStack(crash.callStackTree)
)
}
}
}
}
}
class MetricsMonitor: MXMetricManagerSubscriber {
let thresholds = MetricThresholds(
launchTime: 2.0, // seconds
hangRate: 0.01, // 1% of sessions
memoryPeak: 200 // MB
)
func didReceive(_ payloads: [MXMetricPayload]) {
for payload in payloads {
checkThresholds(payload)
}
}
private func checkThresholds(_ payload: MXMetricPayload) {
// Check launch time
if let launches = payload.applicationLaunchMetrics {
let p50 = calculateP50(launches.histogrammedTimeToFirstDraw)
if p50 > thresholds.launchTime {
sendAlert("Launch time regression: \(p50)s > \(thresholds.launchTime)s")
}
}
// Check memory
if let memory = payload.memoryMetrics {
let peakMB = memory.peakMemoryUsage.converted(to: .megabytes).value
if peakMB > Double(thresholds.memoryPeak) {
sendAlert("Memory peak regression: \(peakMB)MB > \(thresholds.memoryPeak)MB")
}
}
}
}
application(_:didFinishLaunchingWithOptions:) 或 App 初始化中| 功能 | MetricKit | Xcode Organizer |
|---|---|---|
| 数据源 | 运行您应用的设备 | App Store Connect 聚合 |
| 交付方式 | 每日发送给您的订阅者 | Xcode 中按需查看 |
| 自定义程度 | 完全访问原始数据 | 预定义视图 |
| 符号化 | 您必须进行符号化 | 预符号化 |
| 历史数据 | 仅当订阅者活跃时 | 最近 16 个版本 |
| 需要代码 | 是 | 否 |
两者都用:使用 Organizer 进行快速概览,使用 MetricKit 进行自定义分析和警报。
WWDC : 2019-417, 2020-10081, 2021-10087
文档 : /metrickit, /metrickit/mxmetricmanager, /metrickit/mxdiagnosticpayload
技能 : axiom-hang-diagnostics, axiom-performance-profiling, axiom-testflight-triage
每周安装量
88
代码仓库
GitHub Stars
606
首次出现
Jan 21, 2026
安全审计
安装于
opencode73
claude-code69
codex68
gemini-cli66
cursor66
github-copilot63
Complete API reference for collecting field performance metrics and diagnostics using MetricKit.
MetricKit provides aggregated, on-device performance and diagnostic data from users who opt into sharing analytics. Data is delivered daily (or on-demand in development).
Use this reference when:
For hang diagnosis workflows, see axiom-hang-diagnostics. For general profiling with Instruments, see axiom-performance-profiling. For memory debugging including jetsam, see axiom-memory-debugging.
iOS Version Support :
| Feature | iOS Version |
|---|---|
| Basic metrics (battery, CPU, memory) | iOS 13+ |
| Diagnostic payloads | iOS 14+ |
| Hang diagnostics | iOS 14+ |
| Launch diagnostics | iOS 16+ |
| Immediate delivery in dev | iOS 15+ |
import MetricKit
class AppMetricsSubscriber: NSObject, MXMetricManagerSubscriber {
override init() {
super.init()
MXMetricManager.shared.add(self)
}
deinit {
MXMetricManager.shared.remove(self)
}
// MARK: - MXMetricManagerSubscriber
func didReceive(_ payloads: [MXMetricPayload]) {
for payload in payloads {
processMetrics(payload)
}
}
func didReceive(_ payloads: [MXDiagnosticPayload]) {
for payload in payloads {
processDiagnostics(payload)
}
}
}
Register subscriber early in app lifecycle:
@main
struct MyApp: App {
@StateObject private var metricsSubscriber = AppMetricsSubscriber()
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
Or in AppDelegate:
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
metricsSubscriber = AppMetricsSubscriber()
return true
}
In iOS 15+, trigger immediate delivery via Debug menu:
Xcode > Debug > Simulate MetricKit Payloads
Or programmatically (debug builds only):
#if DEBUG
// Payloads delivered immediately in development
// No special code needed - just run and wait
#endif
MXMetricPayload contains aggregated performance metrics from the past 24 hours.
func processMetrics(_ payload: MXMetricPayload) {
// Time range for this payload
let start = payload.timeStampBegin
let end = payload.timeStampEnd
// App version that generated this data
let version = payload.metaData?.applicationBuildVersion
// Access specific metric categories
if let cpuMetrics = payload.cpuMetrics {
processCPU(cpuMetrics)
}
if let memoryMetrics = payload.memoryMetrics {
processMemory(memoryMetrics)
}
if let launchMetrics = payload.applicationLaunchMetrics {
processLaunches(launchMetrics)
}
// ... other categories
}
func processCPU(_ metrics: MXCPUMetric) {
// Cumulative CPU time
let cpuTime = metrics.cumulativeCPUTime // Measurement<UnitDuration>
// iOS 14+: CPU instruction count
if #available(iOS 14.0, *) {
let instructions = metrics.cumulativeCPUInstructions // Measurement<Unit>
}
}
func processMemory(_ metrics: MXMemoryMetric) {
// Peak memory usage
let peakMemory = metrics.peakMemoryUsage // Measurement<UnitInformationStorage>
// Average suspended memory
let avgSuspended = metrics.averageSuspendedMemory // MXAverage<UnitInformationStorage>
}
func processLaunches(_ metrics: MXAppLaunchMetric) {
// First draw (cold launch) histogram
let firstDrawHistogram = metrics.histogrammedTimeToFirstDraw
// Resume time histogram
let resumeHistogram = metrics.histogrammedApplicationResumeTime
// Optimized time to first draw (iOS 15.2+)
if #available(iOS 15.2, *) {
let optimizedLaunch = metrics.histogrammedOptimizedTimeToFirstDraw
}
// Parse histogram buckets
for bucket in firstDrawHistogram.bucketEnumerator {
if let bucket = bucket as? MXHistogramBucket<UnitDuration> {
let start = bucket.bucketStart // e.g., 0ms
let end = bucket.bucketEnd // e.g., 100ms
let count = bucket.bucketCount // Number of launches in this range
}
}
}
@available(iOS 14.0, *)
func processExits(_ metrics: MXAppExitMetric) {
let fg = metrics.foregroundExitData
let bg = metrics.backgroundExitData
// Foreground (onscreen) exits
let fgNormal = fg.cumulativeNormalAppExitCount
let fgWatchdog = fg.cumulativeAppWatchdogExitCount
let fgMemoryLimit = fg.cumulativeMemoryResourceLimitExitCount
let fgMemoryPressure = fg.cumulativeMemoryPressureExitCount
let fgBadAccess = fg.cumulativeBadAccessExitCount
let fgIllegalInstruction = fg.cumulativeIllegalInstructionExitCount
let fgAbnormal = fg.cumulativeAbnormalExitCount
// Background exits
let bgSuspended = bg.cumulativeSuspendedWithLockedFileExitCount
let bgTaskTimeout = bg.cumulativeBackgroundTaskAssertionTimeoutExitCount
let bgCPULimit = bg.cumulativeCPUResourceLimitExitCount
}
@available(iOS 14.0, *)
func processHitches(_ metrics: MXAnimationMetric) {
// Scroll hitch rate (hitches per scroll)
let scrollHitchRate = metrics.scrollHitchTimeRatio // Double (0.0 - 1.0)
}
func processDiskIO(_ metrics: MXDiskIOMetric) {
let logicalWrites = metrics.cumulativeLogicalWrites // Measurement<UnitInformationStorage>
}
func processNetwork(_ metrics: MXNetworkTransferMetric) {
let cellUpload = metrics.cumulativeCellularUpload
let cellDownload = metrics.cumulativeCellularDownload
let wifiUpload = metrics.cumulativeWifiUpload
let wifiDownload = metrics.cumulativeWifiDownload
}
Track custom operations with signposts:
// In your code: emit signposts
import os.signpost
let log = MXMetricManager.makeLogHandle(category: "ImageProcessing")
func processImage(_ image: UIImage) {
mxSignpost(.begin, log: log, name: "ProcessImage")
// ... do work ...
mxSignpost(.end, log: log, name: "ProcessImage")
}
// In metrics subscriber: read signpost data
func processSignposts(_ metrics: MXSignpostMetric) {
let name = metrics.signpostName
let category = metrics.signpostCategory
// Histogram of durations
let histogram = metrics.signpostIntervalData.histogrammedSignpostDurations
// Total count
let count = metrics.totalCount
}
func exportPayload(_ payload: MXMetricPayload) {
// JSON representation for upload to analytics
let jsonData = payload.jsonRepresentation()
// Or as Dictionary
if let json = try? JSONSerialization.jsonObject(with: jsonData) as? [String: Any] {
uploadToAnalytics(json)
}
}
MXDiagnosticPayload contains diagnostic reports for crashes, hangs, disk write exceptions, and CPU exceptions.
@available(iOS 14.0, *)
func processDiagnostics(_ payload: MXDiagnosticPayload) {
// Crash diagnostics
if let crashes = payload.crashDiagnostics {
for crash in crashes {
processCrash(crash)
}
}
// Hang diagnostics
if let hangs = payload.hangDiagnostics {
for hang in hangs {
processHang(hang)
}
}
// Disk write exceptions
if let diskWrites = payload.diskWriteExceptionDiagnostics {
for diskWrite in diskWrites {
processDiskWriteException(diskWrite)
}
}
// CPU exceptions
if let cpuExceptions = payload.cpuExceptionDiagnostics {
for cpuException in cpuExceptions {
processCPUException(cpuException)
}
}
}
@available(iOS 14.0, *)
func processCrash(_ diagnostic: MXCrashDiagnostic) {
// Call stack tree (needs symbolication)
let callStackTree = diagnostic.callStackTree
// Crash metadata
let signal = diagnostic.signal // e.g., SIGSEGV
let exceptionType = diagnostic.exceptionType // e.g., EXC_BAD_ACCESS
let exceptionCode = diagnostic.exceptionCode
let terminationReason = diagnostic.terminationReason
// Virtual memory info
let virtualMemoryRegionInfo = diagnostic.virtualMemoryRegionInfo
// Unique identifier for grouping similar crashes
// (not available - use call stack signature)
}
@available(iOS 14.0, *)
func processHang(_ diagnostic: MXHangDiagnostic) {
// How long the hang lasted
let duration = diagnostic.hangDuration // Measurement<UnitDuration>
// Call stack when hang occurred
let callStackTree = diagnostic.callStackTree
}
@available(iOS 14.0, *)
func processDiskWriteException(_ diagnostic: MXDiskWriteExceptionDiagnostic) {
// Total bytes written that triggered exception
let totalWrites = diagnostic.totalWritesCaused // Measurement<UnitInformationStorage>
// Call stack of writes
let callStackTree = diagnostic.callStackTree
}
@available(iOS 14.0, *)
func processCPUException(_ diagnostic: MXCPUExceptionDiagnostic) {
// Total CPU time that triggered exception
let totalCPUTime = diagnostic.totalCPUTime // Measurement<UnitDuration>
// Total sampled time
let totalSampledTime = diagnostic.totalSampledTime
// Call stack of CPU-intensive code
let callStackTree = diagnostic.callStackTree
}
MXCallStackTree contains stack frames from diagnostics. Frames are NOT symbolicated—you must symbolicate using your dSYM.
@available(iOS 14.0, *)
func parseCallStackTree(_ tree: MXCallStackTree) {
// JSON representation
let jsonData = tree.jsonRepresentation()
// Parse the JSON
guard let json = try? JSONSerialization.jsonObject(with: jsonData) as? [String: Any],
let callStacks = json["callStacks"] as? [[String: Any]] else {
return
}
for callStack in callStacks {
guard let threadAttributed = callStack["threadAttributed"] as? Bool,
let frames = callStack["callStackRootFrames"] as? [[String: Any]] else {
continue
}
// threadAttributed = true means this thread caused the issue
if threadAttributed {
parseFrames(frames)
}
}
}
func parseFrames(_ frames: [[String: Any]]) {
for frame in frames {
// Binary image UUID (match to dSYM)
let binaryUUID = frame["binaryUUID"] as? String
// Address offset within binary
let offsetIntoBinaryTextSegment = frame["offsetIntoBinaryTextSegment"] as? Int
// Binary name (e.g., "MyApp", "UIKitCore")
let binaryName = frame["binaryName"] as? String
// Address (for symbolication)
let address = frame["address"] as? Int
// Sample count (how many times this frame appeared)
let sampleCount = frame["sampleCount"] as? Int
// Sub-frames (tree structure)
let subFrames = frame["subFrames"] as? [[String: Any]]
}
}
{
"callStacks": [
{
"threadAttributed": true,
"callStackRootFrames": [
{
"binaryUUID": "A1B2C3D4-E5F6-7890-ABCD-EF1234567890",
"offsetIntoBinaryTextSegment": 123456,
"binaryName": "MyApp",
"address": 4384712345,
"sampleCount": 10,
"subFrames": [
{
"binaryUUID": "F1E2D3C4-B5A6-7890-1234-567890ABCDEF",
"offsetIntoBinaryTextSegment": 78901,
"binaryName": "UIKitCore",
"address": 7234567890,
"sampleCount": 10
}
]
}
]
}
]
}
MetricKit call stacks are unsymbolicated. To symbolicate:
binaryUUID to your dSYM# Find dSYM for binary UUID
mdfind "com_apple_xcode_dsym_uuids == A1B2C3D4-E5F6-7890-ABCD-EF1234567890"
# Symbolicate address
atos -arch arm64 -o MyApp.app.dSYM/Contents/Resources/DWARF/MyApp -l 0x100000000 0x105234567
Or use a crash reporting service that handles symbolication (Crashlytics, Sentry, etc.).
Track why your app was terminated in the background:
@available(iOS 14.0, *)
func analyzeBackgroundExits(_ data: MXBackgroundExitData) {
// Normal exits (user closed, system reclaimed)
let normal = data.cumulativeNormalAppExitCount
// Memory issues
let memoryLimit = data.cumulativeMemoryResourceLimitExitCount // Exceeded memory limit
let memoryPressure = data.cumulativeMemoryPressureExitCount // Jetsam
// Crashes
let badAccess = data.cumulativeBadAccessExitCount // SIGSEGV
let illegalInstruction = data.cumulativeIllegalInstructionExitCount // SIGILL
let abnormal = data.cumulativeAbnormalExitCount // Other crashes
// System terminations
let watchdog = data.cumulativeAppWatchdogExitCount // Timeout during transition
let taskTimeout = data.cumulativeBackgroundTaskAssertionTimeoutExitCount // Background task timeout
let cpuLimit = data.cumulativeCPUResourceLimitExitCount // Exceeded CPU quota
let lockedFile = data.cumulativeSuspendedWithLockedFileExitCount // File lock held
}
| Exit Type | Meaning | Action |
|---|---|---|
normalAppExitCount | Clean exit | None (expected) |
memoryResourceLimitExitCount | Used too much memory | Reduce footprint |
memoryPressureExitCount | Jetsam (system reclaimed) | Reduce background memory to <50MB |
badAccessExitCount | SIGSEGV crash | Check null pointers, invalid memory |
illegalInstructionExitCount |
class MetricsUploader {
func upload(_ payload: MXMetricPayload) {
let jsonData = payload.jsonRepresentation()
var request = URLRequest(url: analyticsEndpoint)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = jsonData
URLSession.shared.dataTask(with: request) { _, response, error in
if let error = error {
// Queue for retry
self.queueForRetry(jsonData)
}
}.resume()
}
}
class HybridCrashReporter: MXMetricManagerSubscriber {
let crashlytics: Crashlytics // or Sentry, etc.
func didReceive(_ payloads: [MXDiagnosticPayload]) {
for payload in payloads {
// MetricKit captures crashes that traditional reporters might miss
// (e.g., watchdog kills, memory pressure exits)
if let crashes = payload.crashDiagnostics {
for crash in crashes {
crashlytics.recordException(
name: crash.exceptionType?.description ?? "Unknown",
reason: crash.terminationReason ?? "MetricKit crash",
callStack: parseCallStack(crash.callStackTree)
)
}
}
}
}
}
class MetricsMonitor: MXMetricManagerSubscriber {
let thresholds = MetricThresholds(
launchTime: 2.0, // seconds
hangRate: 0.01, // 1% of sessions
memoryPeak: 200 // MB
)
func didReceive(_ payloads: [MXMetricPayload]) {
for payload in payloads {
checkThresholds(payload)
}
}
private func checkThresholds(_ payload: MXMetricPayload) {
// Check launch time
if let launches = payload.applicationLaunchMetrics {
let p50 = calculateP50(launches.histogrammedTimeToFirstDraw)
if p50 > thresholds.launchTime {
sendAlert("Launch time regression: \(p50)s > \(thresholds.launchTime)s")
}
}
// Check memory
if let memory = payload.memoryMetrics {
let peakMB = memory.peakMemoryUsage.converted(to: .megabytes).value
if peakMB > Double(thresholds.memoryPeak) {
sendAlert("Memory peak regression: \(peakMB)MB > \(thresholds.memoryPeak)MB")
}
}
}
}
application(_:didFinishLaunchingWithOptions:) or App init| Feature | MetricKit | Xcode Organizer |
|---|---|---|
| Data source | Devices running your app | App Store Connect aggregation |
| Delivery | Daily to your subscriber | On-demand in Xcode |
| Customization | Full access to raw data | Predefined views |
| Symbolication | You must symbolicate | Pre-symbolicated |
| Historical data | Only when subscriber active | Last 16 versions |
| Requires code | Yes | No |
Use both : Organizer for quick overview, MetricKit for custom analytics and alerting.
WWDC : 2019-417, 2020-10081, 2021-10087
Docs : /metrickit, /metrickit/mxmetricmanager, /metrickit/mxdiagnosticpayload
Skills : axiom-hang-diagnostics, axiom-performance-profiling, axiom-testflight-triage
Weekly Installs
88
Repository
GitHub Stars
606
First Seen
Jan 21, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
opencode73
claude-code69
codex68
gemini-cli66
cursor66
github-copilot63
Convex性能审计指南 - 诊断修复Convex应用性能问题与优化方案
16,300 周安装
| SIGILL crash |
| Check invalid function pointers |
abnormalExitCount | Other crash | Check crash diagnostics |
appWatchdogExitCount | Hung during transition | Reduce launch/background work |
backgroundTaskAssertionTimeoutExitCount | Didn't end background task | Call endBackgroundTask properly |
cpuResourceLimitExitCount | Too much background CPU | Move to BGProcessingTask |
suspendedWithLockedFileExitCount | Held file lock while suspended | Release locks before suspend |