<!--
{
  "documentType" : "article",
  "framework" : "MetricKit",
  "identifier" : "/documentation/MetricKit/analyzing-app-performance-with-metrickit",
  "metadataVersion" : "0.1.0",
  "role" : "article",
  "title" : "Analyzing app performance with MetricKit"
}
-->

# Analyzing app performance with MetricKit

Work with the metric values, diagnostic data, and environments in MetricKit reports.

## Overview

MetricKit reports contain a rich set of typed measurements, diagnostic data, and environmental context. [`MetricResult`](/documentation/MetricKit/MetricResult) carries individual metric values (scalars, histograms, or statistics depending on the metric), and you can use the [`metricGroup`](/documentation/MetricKit/MetricResult/metricGroup) property to route them to category-specific handlers. [`DiagnosticReport`](/documentation/MetricKit/DiagnosticReport) wraps a single [`DiagnosticResult`](/documentation/MetricKit/DiagnosticResult) case with type-specific properties and a [`CallStackTree`](/documentation/MetricKit/CallStackTree) you can navigate to locate the code involved. [`MetricReport`](/documentation/MetricKit/MetricReport) and [`DiagnosticReport`](/documentation/MetricKit/DiagnosticReport) include an environment with device, operating system, and app context. Both also conform to <doc://com.apple.documentation/documentation/Swift/Codable>, so you can serialize them with <doc://com.apple.documentation/documentation/Foundation/JSONEncoder> for storage or upload.

## Filter groups of metrics

Every [`MetricResult`](/documentation/MetricKit/MetricResult) case carries a [`metricGroup`](/documentation/MetricKit/MetricResult/metricGroup) property that returns a [`MetricGroup`](/documentation/MetricKit/MetricGroup) value identifying the category the measurement belongs to, such as CPU, GPU, memory, or disk I/O. You can use [`metricGroup`](/documentation/MetricKit/MetricResult/metricGroup) to filter the [`values`](/documentation/MetricKit/MetricReport/IntervalEntry/values) array without writing an exhaustive switch. This pattern is useful when routing measurements to category-specific handlers, logging only a subset of metrics, or building a summary that groups data by category.

```swift
let memoryValues = entry.values.filter { $0.metricGroup == .memory }
```

## Understand measurements

The majority of values are scalar values expressed as a <doc://com.apple.documentation/documentation/Foundation/Measurement>. A `Measurement` pairs a `Double` value with a unit. MetricKit uses unit types including:

- <doc://com.apple.documentation/documentation/Foundation/UnitDuration>: Time-based measurements such as CPU time, hang time, background activity durations, and launch times.
- <doc://com.apple.documentation/documentation/Foundation/UnitInformationStorage>: Byte-based measurements such as memory usage, network transfer totals, and disk sizes.
- <doc://com.apple.documentation/documentation/Foundation/UnitFrequency>: Rate measurements such as frames per second in [`MetricResult.metalFrameRate(_:)`](/documentation/MetricKit/MetricResult/metalFrameRate(_:)).
- [`SignalBars`](/documentation/MetricKit/SignalBars): The <doc://com.apple.documentation/documentation/Foundation/Dimension> subclass used in [`MetricResult.cellularConditionTime(_:)`](/documentation/MetricKit/MetricResult/cellularConditionTime(_:)) histograms to represent cellular signal strength levels.
- [`HitchTimeRatio`](/documentation/MetricKit/HitchTimeRatio): The <doc://com.apple.documentation/documentation/Foundation/Dimension> subclass used by [`ratio`](/documentation/MetricKit/HitchTimeMetric/ratio) and [`hitchTimeRatio`](/documentation/MetricKit/SignpostIntervalMetric/hitchTimeRatio) to represent the ratio of hitch time to total tracked time, expressed as milliseconds per second.

## Work with histogram distributions

Several [`MetricResult`](/documentation/MetricKit/MetricResult) cases expose a [`Histogram`](/documentation/MetricKit/Histogram) rather than a scalar value. A `Histogram` contains an ordered array of buckets, each with a lower bound, an upper bound, and a count of observations that fell within that range.

Iterating through the buckets gives you the full distribution of measured values:

```swift
case let .hangTime(metric):
    for bucket in metric.histogram.buckets {
        print(
            "\(bucket.lowerBound) – \(bucket.upperBound):"
            + " \(bucket.count) hangs"
        )
    }
```

## Work with average statistics

Some metrics report a single averaged value rather than a distribution. For example, [`SuspendedMemoryMetric`](/documentation/MetricKit/SuspendedMemoryMetric) exposes a [`value`](/documentation/MetricKit/SuspendedMemoryMetric/value) property of type [`AverageStatistics`](/documentation/MetricKit/AverageStatistics). It provides three properties: an average, a count, and an optional standard deviation. A [`count`](/documentation/MetricKit/AverageStatistics/count) of zero means the sample count isn’t available for this reporting period. [`standardDeviation`](/documentation/MetricKit/AverageStatistics/standardDeviation) is `nil` when unavailable.

```swift
case let .suspendedMemory(metric):
    let statistics = metric.value
    print("Average suspended memory: \(statistics.average)")

    if statistics.count > 0 {
        // A count equal to 0 means it's unavailable.
        print("Sample count: \(statistics.count)")
    }

    if let standardDeviation = statistics.standardDeviation {
        print("Std dev: \(standardDeviation)")
    }
```

## Read location and disk space metrics

Not every metric condenses to a single value. [`MetricResult.locationActivityTime(_:)`](/documentation/MetricKit/MetricResult/locationActivityTime(_:)) breaks location accuracy usage into six tiers, each a `Measurement<UnitDuration>`:

```swift
case let .locationActivityTime(metric):
    print("Best accuracy for navigation: \(metric.bestAccuracyForNavigation)")
    print("Best accuracy:               \(metric.bestAccuracy)")
    print("Ten meters:                  \(metric.tenMeters)")
    print("One hundred meters:          \(metric.oneHundredMeter)")
    print("One kilometer:               \(metric.oneKilometer)")
    print("Three kilometers:            \(metric.threeKilometers)")
```

[`MetricResult.totalFileCount(_:)`](/documentation/MetricKit/MetricResult/totalFileCount(_:)) and [`MetricResult.totalFileSize(_:)`](/documentation/MetricKit/MetricResult/totalFileSize(_:)) distinguish binary content from data content. Use them to understand how your app’s storage breaks down between executable code and user data:

```swift
case let .totalFileSize(metric):
    print("Binary size: \(metric.binaryFileSize)")
    print("Data size:   \(metric.dataFileSize)")

case let .totalFileCount(metric):
    print("Binary files: \(metric.binaryFileCount)")
    print("Data files:   \(metric.dataFileCount)")

case let .totalDiskSpaceCapacity(metric):
    print("Device capacity: \(metric.capacity)")
```

[`MetricResult.metalFrameRate(_:)`](/documentation/MetricKit/MetricResult/metalFrameRate(_:)) provides frame statistics for a specific Metal layer, including the layer name, frame count, active drawing duration, and a frames-per-second measurement:

```swift
case let .metalFrameRate(metric):
    print("Layer: \(metric.layerName)")
    print("Frames: \(metric.frameCount)")
    print("Active drawing: \(metric.activeDrawingDuration)")
    print("FPS: \(metric.framesPerSecond)")
```

## Extract diagnostic details

Each [`DiagnosticReport`](/documentation/MetricKit/DiagnosticReport) wraps a single [`DiagnosticResult`](/documentation/MetricKit/DiagnosticResult) case. Switch over the result to access the type-specific properties of each diagnostic. MetricKit generates a [`MemoryExceptionDiagnostic`](/documentation/MetricKit/MemoryExceptionDiagnostic) when your app or extension terminates because it exceeds the memory limit.  [`MemoryExceptionDiagnostic`](/documentation/MetricKit/MemoryExceptionDiagnostic) is only available on iOS.

```swift
switch report.result {
case let .crash(diagnostic):
    if let reason = diagnostic.terminationReason {
        log("Termination reason: \(reason.rawValue)")
    }
    if let exceptionType = diagnostic.exceptionType {
        log("Exception type: \(exceptionType)")
    }
    analyze(diagnostic.callStackTree)
case let .hang(diagnostic):
    log("Hang duration: \(diagnostic.hangDuration)")
    analyze(diagnostic.callStackTree)
case let .cpuException(diagnostic):
    log("CPU time: \(diagnostic.totalCPUTime)")
    log("Sampled time: \(diagnostic.totalSampledTime)")
    analyze(diagnostic.callStackTree)
case let .diskWriteException(diagnostic):
    log("Bytes written: \(diagnostic.totalBytesWritten)")
    analyze(diagnostic.callStackTree)
case let .appLaunch(diagnostic):
    log("Launch duration: \(diagnostic.launchDuration)")
case let .memoryException(diagnostic):
    analyze(diagnostic.callStackTree)
@unknown default:
    break
}
```

## Navigate call stack trees

[`CallStackTree`](/documentation/MetricKit/CallStackTree) is the primary structure for analyzing crashes, hangs, and exceptions. It contains an array of [`CallStackThread`](/documentation/MetricKit/CallStackThread) values and supports binary metadata lookup by UUID.

The [`callStackPerThread`](/documentation/MetricKit/CallStackTree/callStackPerThread) property tells you how the frames are organized. When [`callStackPerThread`](/documentation/MetricKit/CallStackTree/callStackPerThread) is `true`, each thread has its own root frames. When it’s `false`, all frames across all threads are merged into a single thread.

The simplest way to iterate through every frame in the tree is [`forEachFrame(_:)`](/documentation/MetricKit/CallStackTree/forEachFrame(_:)), which handles the recursive [`subFrames`](/documentation/MetricKit/CallStackFrame/subFrames) traversal for you:

```swift
var frames: [(address: UInt64, binaryName: String)] = []

diagnostic.callStackTree.forEachFrame { frame in
    guard let address = frame.address,
          let uuid = frame.binaryUUID,
          let info = diagnostic.callStackTree.binaryInfo[uuid]
    else { return }
    frames.append((address: address, binaryName: info.name))
}
```

[`binaryInfo`](/documentation/MetricKit/CallStackTree/binaryInfo-swift.property) is a dictionary, and the key is the same [`binaryUUID`](/documentation/MetricKit/CallStackFrame/binaryUUID) that each [`CallStackFrame`](/documentation/MetricKit/CallStackFrame) carries. A [`CallStackTree.BinaryInfo`](/documentation/MetricKit/CallStackTree/BinaryInfo-swift.struct) value provides the binary’s [`uuid`](/documentation/MetricKit/CallStackTree/BinaryInfo-swift.struct/uuid) and [`name`](/documentation/MetricKit/CallStackTree/BinaryInfo-swift.struct/name).

When you need to examine threads individually — for example, to identify the crashing thread separately — iterate [`callStackThreads`](/documentation/MetricKit/CallStackTree/callStackThreads) directly. Each [`CallStackThread`](/documentation/MetricKit/CallStackThread) exposes a [`rootFrames`](/documentation/MetricKit/CallStackThread/rootFrames) array, and each [`CallStackFrame`](/documentation/MetricKit/CallStackFrame) has a [`subFrames`](/documentation/MetricKit/CallStackFrame/subFrames) array for manual recursion:

```swift
for frame in frames {
    if let uuid = frame.binaryUUID,
       let info = tree.binaryInfo[uuid] {
        let indent = String(repeating: "  ", count: depth)
        print("\(indent)\(info.name) + "
            + "\(frame.offsetIntoBinaryTextSegment ?? 0)")
    }
    visitFrames(frame.subFrames, depth: depth + 1, tree: tree)
}
```

## Review a report’s environment

[`MetricReport`](/documentation/MetricKit/MetricReport) and [`DiagnosticReport`](/documentation/MetricKit/DiagnosticReport) both carry an environment that provides context about the device and session at the time of collection. The two environments have different optionality: [`environment`](/documentation/MetricKit/MetricReport/environment-swift.property) is optional, while [`environment`](/documentation/MetricKit/DiagnosticReport/environment-swift.property) is required.

[`MetricReport.Environment`](/documentation/MetricKit/MetricReport/Environment-swift.struct) includes [`osVersion`](/documentation/MetricKit/MetricReport/Environment-swift.struct/osVersion), [`deviceType`](/documentation/MetricKit/MetricReport/Environment-swift.struct/deviceType), [`regionFormat`](/documentation/MetricKit/MetricReport/Environment-swift.struct/regionFormat), and [`lowPowerModeEnabled`](/documentation/MetricKit/MetricReport/Environment-swift.struct/lowPowerModeEnabled). It also exposes [`hasExceededStateLimit`](/documentation/MetricKit/MetricReport/Environment-swift.struct/hasExceededStateLimit), which is `true` when the number of unique states during the reporting period exceeded the system limit — some state data is then folded into the full-day interval entry rather than appearing in [`stateEntries`](/documentation/MetricKit/MetricReport/stateEntries).

[`DiagnosticReport.Environment`](/documentation/MetricKit/DiagnosticReport/Environment-swift.struct) provides additional app-specific context. Check it to understand the exact build context of a diagnostic event:

```swift
let environment = report.environment
print("OS: \(environment.osVersion), device: \(environment.deviceType)")
print("App: \(environment.applicationVersion) (\(environment.applicationBuildVersion))")

if environment.isTestFlightApp {
    print("Running under TestFlight")
}

if !environment.signpostData.isEmpty {
    print("Active signposts at event time:")
    for record in environment.signpostData {
        print("  \(record)")
    }
}
```

[`signpostData`](/documentation/MetricKit/DiagnosticReport/Environment-swift.struct/signpostData) is an array of [`SignpostRecord`](/documentation/MetricKit/SignpostRecord) values representing any <doc://com.apple.documentation/documentation/os/OSSignposter> intervals that were active when the diagnostic occurred. [`DiagnosticReport.Environment`](/documentation/MetricKit/DiagnosticReport/Environment-swift.struct) also provides a [`pid`](/documentation/MetricKit/DiagnosticReport/Environment-swift.struct/pid), [`bundleIdentifier`](/documentation/MetricKit/DiagnosticReport/Environment-swift.struct/bundleIdentifier), [`regionFormat`](/documentation/MetricKit/DiagnosticReport/Environment-swift.struct/regionFormat), and [`states`](/documentation/MetricKit/DiagnosticReport/Environment-swift.struct/states).

## Serialize reports

Both [`MetricReport`](/documentation/MetricKit/MetricReport) and [`DiagnosticReport`](/documentation/MetricKit/DiagnosticReport) conform to <doc://com.apple.documentation/documentation/Swift/Codable>. To send a [`MetricReport`](/documentation/MetricKit/MetricReport) to your server, encode it inside your observation loop using <doc://com.apple.documentation/documentation/Foundation/JSONEncoder>. Setting the [`encodingFormatKey`](/documentation/MetricKit/MetricReport/encodingFormatKey) in the encoder’s <doc://com.apple.documentation/documentation/Foundation/JSONEncoder/userInfo> to [`MetricReport.EncodingFormat.byStateReportingDomain`](/documentation/MetricKit/MetricReport/EncodingFormat/byStateReportingDomain) groups the encoded output by domain, so both state entries and interval entries in the resulting JSON contain your app’s performance metrics organized by each reporting domain and the states within it:

```swift
import MetricKit

for await report in manager.metricReports {
    do {
        let encoder = JSONEncoder()
        encoder.outputFormatting = .prettyPrinted

        let formatKey = MetricReport.encodingFormatKey
        encoder.userInfo[formatKey] = MetricReport.EncodingFormat.byStateReportingDomain

        let jsonData = try encoder.encode(report)
        // Send to server
    } catch {
        // Handle encoding error
    }
}
```

---

Copyright &copy; 2026 Apple Inc. All rights reserved. | [Terms of Use](https://www.apple.com/legal/internet-services/terms/site.html) | [Privacy Policy](https://www.apple.com/privacy/privacy-policy)