MetricKit API reference for field diagnostics - MXMetricPayload, MXDiagnosticPayload, MXCallStackTree parsing, crash and hang collection
Parses MetricKit payloads for crashes, hangs, and performance metrics.
npx claudepluginhub charleswiltgen/axiomThis skill inherits all available tools. When active, it can use any tool Claude has access to.
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 | 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 |
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
Activates when the user asks about AI prompts, needs prompt templates, wants to search for prompts, or mentions prompts.chat. Use for discovering, retrieving, and improving prompts.
Search, retrieve, and install Agent Skills from the prompts.chat registry using MCP tools. Use when the user asks to find skills, browse skill catalogs, install a skill for Claude, or extend Claude's capabilities with reusable AI agent components.
Creating algorithmic art using p5.js with seeded randomness and interactive parameter exploration. Use this when users request creating art using code, generative art, algorithmic art, flow fields, or particle systems. Create original algorithmic art rather than copying existing artists' work to avoid copyright violations.