| name | axiom-metrickit-ref |
| description | MetricKit API reference for field diagnostics - MXMetricPayload, MXDiagnosticPayload, MXCallStackTree parsing, crash and hang collection |
| license | MIT |
MetricKit API Reference
Complete API reference for collecting field performance metrics and diagnostics using MetricKit.
Overview
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).
When to Use This Reference
Use this reference when:
- Setting up MetricKit subscriber in your app
- Parsing MXMetricPayload or MXDiagnosticPayload
- Symbolicating MXCallStackTree crash data
- Understanding background exit reasons (jetsam, watchdog)
- Integrating MetricKit with existing crash reporters
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.
Common Gotchas
- 24-hour delay — MetricKit data arrives once daily; it's not real-time debugging
- Call stacks require symbolication — MXCallStackTree frames are unsymbolicated; keep dSYMs
- Opt-in only — Only users who enable "Share with App Developers" contribute data
- Aggregated, not individual — You get counts and averages, not per-user traces
- Simulator doesn't work — MetricKit only collects on physical devices
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+ |
Part 1: Setup
Basic Integration
import MetricKit
class AppMetricsSubscriber: NSObject, MXMetricManagerSubscriber {
override init() {
super.init()
MXMetricManager.shared.add(self)
}
deinit {
MXMetricManager.shared.remove(self)
}
func didReceive(_ payloads: [MXMetricPayload]) {
for payload in payloads {
processMetrics(payload)
}
}
func didReceive(_ payloads: [MXDiagnosticPayload]) {
for payload in payloads {
processDiagnostics(payload)
}
}
}
Registration Timing
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
}
Development Testing
In iOS 15+, trigger immediate delivery via Debug menu:
Xcode > Debug > Simulate MetricKit Payloads
Or programmatically (debug builds only):
#if DEBUG
#endif
Part 2: MXMetricPayload
MXMetricPayload contains aggregated performance metrics from the past 24 hours.
Payload Structure
func processMetrics(_ payload: MXMetricPayload) {
let start = payload.timeStampBegin
let end = payload.timeStampEnd
let version = payload.metaData?.applicationBuildVersion
if let cpuMetrics = payload.cpuMetrics {
processCPU(cpuMetrics)
}
if let memoryMetrics = payload.memoryMetrics {
processMemory(memoryMetrics)
}
if let launchMetrics = payload.applicationLaunchMetrics {
processLaunches(launchMetrics)
}
}
CPU Metrics (MXCPUMetric)
func processCPU(_ metrics: MXCPUMetric) {
let cpuTime = metrics.cumulativeCPUTime
if #available(iOS 14.0, *) {
let instructions = metrics.cumulativeCPUInstructions
}
}
Memory Metrics (MXMemoryMetric)
func processMemory(_ metrics: MXMemoryMetric) {
let peakMemory = metrics.peakMemoryUsage
let avgSuspended = metrics.averageSuspendedMemory
}
Launch Metrics (MXAppLaunchMetric)
func processLaunches(_ metrics: MXAppLaunchMetric) {
let firstDrawHistogram = metrics.histogrammedTimeToFirstDraw
let resumeHistogram = metrics.histogrammedApplicationResumeTime
if #available(iOS 15.2, *) {
let optimizedLaunch = metrics.histogrammedOptimizedTimeToFirstDraw
}
for bucket in firstDrawHistogram.bucketEnumerator {
if let bucket = bucket as? MXHistogramBucket<UnitDuration> {
let start = bucket.bucketStart
let end = bucket.bucketEnd
let count = bucket.bucketCount
}
}
}
Application Exit Metrics (MXAppExitMetric) — iOS 14+
@available(iOS 14.0, *)
func processExits(_ metrics: MXAppExitMetric) {
let fg = metrics.foregroundExitData
let bg = metrics.backgroundExitData
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
let bgSuspended = bg.cumulativeSuspendedWithLockedFileExitCount
let bgTaskTimeout = bg.cumulativeBackgroundTaskAssertionTimeoutExitCount
let bgCPULimit = bg.cumulativeCPUResourceLimitExitCount
}
Scroll Hitch Metrics (MXAnimationMetric) — iOS 14+
@available(iOS 14.0, *)
func processHitches(_ metrics: MXAnimationMetric) {
let scrollHitchRate = metrics.scrollHitchTimeRatio
}
Disk I/O Metrics (MXDiskIOMetric)
func processDiskIO(_ metrics: MXDiskIOMetric) {
let logicalWrites = metrics.cumulativeLogicalWrites
}
Network Metrics (MXNetworkTransferMetric)
func processNetwork(_ metrics: MXNetworkTransferMetric) {
let cellUpload = metrics.cumulativeCellularUpload
let cellDownload = metrics.cumulativeCellularDownload
let wifiUpload = metrics.cumulativeWifiUpload
let wifiDownload = metrics.cumulativeWifiDownload
}
Signpost Metrics (MXSignpostMetric)
Track custom operations with signposts:
import os.signpost
let log = MXMetricManager.makeLogHandle(category: "ImageProcessing")
func processImage(_ image: UIImage) {
mxSignpost(.begin, log: log, name: "ProcessImage")
mxSignpost(.end, log: log, name: "ProcessImage")
}
func processSignposts(_ metrics: MXSignpostMetric) {
let name = metrics.signpostName
let category = metrics.signpostCategory
let histogram = metrics.signpostIntervalData.histogrammedSignpostDurations
let count = metrics.totalCount
}
Exporting Payload as JSON
func exportPayload(_ payload: MXMetricPayload) {
let jsonData = payload.jsonRepresentation()
if let json = try? JSONSerialization.jsonObject(with: jsonData) as? [String: Any] {
uploadToAnalytics(json)
}
}
Part 3: MXDiagnosticPayload — iOS 14+
MXDiagnosticPayload contains diagnostic reports for crashes, hangs, disk write exceptions, and CPU exceptions.
Payload Structure
@available(iOS 14.0, *)
func processDiagnostics(_ payload: MXDiagnosticPayload) {
if let crashes = payload.crashDiagnostics {
for crash in crashes {
processCrash(crash)
}
}
if let hangs = payload.hangDiagnostics {
for hang in hangs {
processHang(hang)
}
}
if let diskWrites = payload.diskWriteExceptionDiagnostics {
for diskWrite in diskWrites {
processDiskWriteException(diskWrite)
}
}
if let cpuExceptions = payload.cpuExceptionDiagnostics {
for cpuException in cpuExceptions {
processCPUException(cpuException)
}
}
}
MXCrashDiagnostic
@available(iOS 14.0, *)
func processCrash(_ diagnostic: MXCrashDiagnostic) {
let callStackTree = diagnostic.callStackTree
let signal = diagnostic.signal
let exceptionType = diagnostic.exceptionType
let exceptionCode = diagnostic.exceptionCode
let terminationReason = diagnostic.terminationReason
let virtualMemoryRegionInfo = diagnostic.virtualMemoryRegionInfo
}
MXHangDiagnostic
@available(iOS 14.0, *)
func processHang(_ diagnostic: MXHangDiagnostic) {
let duration = diagnostic.hangDuration
let callStackTree = diagnostic.callStackTree
}
MXDiskWriteExceptionDiagnostic
@available(iOS 14.0, *)
func processDiskWriteException(_ diagnostic: MXDiskWriteExceptionDiagnostic) {
let totalWrites = diagnostic.totalWritesCaused
let callStackTree = diagnostic.callStackTree
}
MXCPUExceptionDiagnostic
@available(iOS 14.0, *)
func processCPUException(_ diagnostic: MXCPUExceptionDiagnostic) {
let totalCPUTime = diagnostic.totalCPUTime
let totalSampledTime = diagnostic.totalSampledTime
let callStackTree = diagnostic.callStackTree
}
Part 4: MXCallStackTree
MXCallStackTree contains stack frames from diagnostics. Frames are NOT symbolicated—you must symbolicate using your dSYM.
Structure
@available(iOS 14.0, *)
func parseCallStackTree(_ tree: MXCallStackTree) {
let jsonData = tree.jsonRepresentation()
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
}
if threadAttributed {
parseFrames(frames)
}
}
}
func parseFrames(_ : [[: ]]) {
frame frames {
binaryUUID frame[]
offsetIntoBinaryTextSegment frame[]
binaryName frame[]
address frame[]
sampleCount frame[]
subFrames frame[] [[: ]]
}
}
JSON Structure Example
{
"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,
Symbolication
MetricKit call stacks are unsymbolicated. To symbolicate:
- Keep your dSYM files for every App Store build
- Match UUID from
binaryUUID to your dSYM
- Use atos to symbolicate:
mdfind "com_apple_xcode_dsym_uuids == A1B2C3D4-E5F6-7890-ABCD-EF1234567890"
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.).
Part 5: MXBackgroundExitData
Track why your app was terminated in the background:
@available(iOS 14.0, *)
func analyzeBackgroundExits(_ data: MXBackgroundExitData) {
let normal = data.cumulativeNormalAppExitCount
let memoryLimit = data.cumulativeMemoryResourceLimitExitCount
let memoryPressure = data.cumulativeMemoryPressureExitCount
let badAccess = data.cumulativeBadAccessExitCount
let illegalInstruction = data.cumulativeIllegalInstructionExitCount
let abnormal = data.cumulativeAbnormalExitCount
let watchdog = data.cumulativeAppWatchdogExitCount
let taskTimeout = data.cumulativeBackgroundTaskAssertionTimeoutExitCount
let cpuLimit = data.cumulativeCPUResourceLimitExitCount
let lockedFile = data.cumulativeSuspendedWithLockedFileExitCount
}
Exit Type Interpretation
| 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 | 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 |
Part 6: Integration Patterns
Upload to Analytics Service
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 {
self.queueForRetry(jsonData)
}
}.resume()
}
}
Combine with Crash Reporter
class HybridCrashReporter: MXMetricManagerSubscriber {
let crashlytics: Crashlytics
func didReceive(_ payloads: [MXDiagnosticPayload]) {
for payload in payloads {
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)
)
}
}
}
}
}
Alert on Regressions
class MetricsMonitor: MXMetricManagerSubscriber {
let thresholds = MetricThresholds(
launchTime: 2.0,
hangRate: 0.01,
memoryPeak: 200
)
func didReceive(_ payloads: [MXMetricPayload]) {
for payload in payloads {
checkThresholds(payload)
}
}
private func checkThresholds(_ payload: MXMetricPayload) {
if let launches = payload.applicationLaunchMetrics {
let p50 = calculateP50(launches.histogrammedTimeToFirstDraw)
if p50 > thresholds.launchTime {
sendAlert("Launch time regression: \(p50)s > \(thresholds.launchTime)s")
}
}
if let memory = payload.memoryMetrics {
let peakMB = memory.peakMemoryUsage.converted(to: .megabytes).value
if peakMB > Double(thresholds.memoryPeak) {
sendAlert("Memory peak regression: MB > MB")
}
}
}
}
Part 7: Best Practices
Do
- Register subscriber early — In
application(_:didFinishLaunchingWithOptions:) or App init
- Keep dSYM files — Required for symbolicating call stacks
- Upload payloads to server — Local processing loses data on uninstall
- Set up alerting — Detect regressions before users report them
- Test with simulated payloads — Xcode Debug menu in iOS 15+
Don't
- Don't rely solely on MetricKit — 24-hour delay, requires user opt-in
- Don't ignore background exits — Jetsam and task timeouts affect UX
- Don't skip symbolication — Raw addresses are unusable
- Don't process on main thread — Payload processing can be expensive
Privacy Considerations
- MetricKit data is aggregated and anonymized
- Data only from users who opted into sharing analytics
- No personally identifiable information
- Safe to upload to your servers
Part 8: MetricKit vs Xcode Organizer
| 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.
Resources
WWDC: 2019-417, 2020-10081, 2021-10087
Docs: /metrickit, /metrickit/mxmetricmanager, /metrickit/mxdiagnosticpayload
Skills: axiom-hang-diagnostics, axiom-performance-profiling, axiom-testflight-triage