-
Meet the new MetricKit
Find and fix performance problems faster than ever. Join us to explore how MetricKit equips you with vital performance metrics and actionable diagnostics to help you understand exactly where your app has opportunities for improvements. We'll also cover how to intersect your app's metrics and diagnostics by app state by using the StateReporting framework, providing you with the full picture to investigate optimizations in your app's experience.
Chapters
- 0:01 - Introduction
- 4:07 - Metrics
- 7:13 - Diagnostics
- 10:03 - Context
Resources
- Getting started with StateReporting
- Analyzing app performance with MetricKit
- Monitoring app performance with MetricKit
- Track performance by app state using MetricKit
- MetricKit
Related Videos
WWDC26
-
Search this video…
Hi, I'm Yonni, and I'm an engineer on the MetricKit team. Great apps and games monitor and optimize their performance out in the real world, on real devices. MetricKit is the framework that can provide you real insights into the quality of your app's experience. In this session, I will start off with an introduction to MetricKit, including what's new in iOS 27.
Then, I'll show you how to start receiving your first metric report.
Your first diagnostic report. And finally, I'll explore how to get even more rich data by connecting performance problems to specific areas in your app.
I'll start with an overview of the framework.
Optimizing your app's performance is a process. You start by collecting data, and analyze it to identify problems. For each problem, you triage it to find the root cause, fix it, and go back to step one to monitor the results. MetricKit is the collection piece in that workflow.
The framework provides two types of data: metrics and diagnostics. Metrics give you a sense of whether an area of performance is improving or worsening overall, while diagnostics tell you which code path is causing a performance problem.
In iOS 27, the framework has been rebuilt from the ground up with a contextually rich and expressive modern Swift-first API.
The new MetricKit APIs are the future of the framework. All of the advances I'll be discussing today are exclusive to this new set of APIs. Metrics are your app's ongoing health signal.
Launch time, hangs, and animation metrics tell you how responsive and smooth your app feels. A slow launch can frustrate people and lead them to leave your app, while a fast launch gets people right into your app's core experiences.
Resource consumption metrics like CPU, GPU, Disk writes, and network transfers tell you how hard your app is working and how it's affecting device health.
For example, MetricKit launch times like the time to first draw metric, provide a histogram of launch counts that fall within certain time range buckets. This graph shows the time it took for the app to launch, every time someone opened it over the course of a day.
Most of the launches took between 510 and 540 milliseconds, with a few outliers.
You can also track ongoing performance by deriving your own insights from the data MetricKit provides.
For example, MetricKit reported that for this app, it had a total hang time on average of 3 seconds while the app was used for 30 minutes. That information can be used to derive an average hang rate of 6 seconds per hour.
If you aggregate this data across all devices, this can give you a measurable signal of how your app's performance is trending.
In iOS 27, MetricKit can provide each metric as a function of the app's state.
For example, when measuring hang time in an app that has multiple tabs, MetricKit can provide this metric intersected with when the active tab is tab 1, tab 2 or tab 3. I'll go into this more later. In iOS 27, MetricKit now also provides a new metric - Metal frame rate. Frame rate is a key metric for game developers to understand render performance. To learn more about optimizing your game for the platform, check out the session "Find and fix performance issues in your Metal game".
In addition to metrics, MetricKit also provides diagnostics. Diagnostics contain useful information that helps you identify which code path caused a performance problem so you can investigate and fix it.
In iOS 27, MetricKit provides memory exception diagnostics. So when your app or extension is terminated for exceeding its memory limit, you get more insight on what happened.
I'll dive in to explore how your app can get performance metrics.
As people use your app throughout the day, MetricKit continuously collects metrics like app launches, hangs, memory, and CPU and delivers it to your app in a daily report.
In this report, MetricKit provides an entry that spans the full day usage of the app. It also provides separate entries for smaller breakdowns, typically a few hours each. These smaller breakdowns are only present when there are metrics associated with them. Here's how the data is structured. Inside each interval, metrics are organized into metric groups. Each group represents an aspect of the system, things like .cpu, .memory, .display, and .gpu. Inside a group, you'll find individual performance metrics.
I'll work through this in code.
Your entry point is the MetricManager class. To receive reports, you await them through the metricReports property.
This setup should be done at app start up to avoid any data loss from delayed subscription. MetricManager should be kept alive so that the streams can continue to deliver reports as subsequent data becomes ready.
With just these few lines, your app is now receiving structured metric data.
Typically, you may want to send these metrics to a server so you can examine your app's health across many devices. MetricReports are Codable, which makes it easy to encode into a format like JSON to send to the server.
Just create a JSONEncoder and encode the entire report.
If you just want to access a particular group of metrics, or a particular value, you can also inspect the metric report further. To do this, iterate through your intervalEntries. This includes a full-day aggregated entry and smaller breakdown windows when available. Then, filter the metrics down to the group you are interested in. In this case, memoryMetrics contains only metrics in the memory group.
Finally, switch over the metric cases to access the type of metric you're interested in, as well as the value of that metric in this report. In this case, the code only handles the peakMemory value. Perform this work in a detached task or a dedicated service class as soon as your app launches.
Going back to the workflow. You have now completed the collection phase. And ready to move on to analysis.
Analyzing metrics across all devices is a data science problem. To enable this analysis, you'll want to set up a server that can ingest each of these reports and aggregate them according to the dimensions that you care about.
You'll need to decide what the best statistical analysis is for the data that you want to generate, and the insights that you want to find.
Using your custom aggregation, this can give you a baseline, an idea of how your app is performing already. Then, monitor your aggregated metrics to detect when things are getting better or worse.
I've shown how you could identify issues in your app using metrics. Now, I will cover how you can fix these issues using diagnostics.
Metrics are a great way to monitor your app. As you collect metrics and monitor performance over time, you can enter the triage phase of your workflow. Diagnostics are particularly helpful in this phase.
When something goes wrong, like a crash or a hang, the system captures a diagnostic on device. A diagnostic report packages up the details and delivers it immediately to your app through MetricKit.
Inside, you get useful information to triage the issue. For example, many diagnostics include backtraces that show you the exact call stack at the time of the event.
One of the most important diagnostics is for crashes. Crash diagnostics not only provide a backtrace, but they'll also indicate why your app was terminated and an exception type that tells you what kind of failure it is.
In iOS 27, a termination category now indicates how each crash was accounted for in metrics.
That way, if abnormal terminations are trending up, you can correlate those directly with individual diagnostics.
In this example, the symbolicated backtrace begins in the system at thread start. As execution flows downward, it crosses into the app's code. To find the crash site, you can follow the calls all the way down. Here, execution reaches the app's submitReport() function and stops. This indicates that this is the point of failure in the execution path.
Now, you can use this information to target fixes in this function.
To get diagnostic reports, you await on the diagnosticReports of your MetricManager instance. Like MetricReports, start listening to this stream as soon as a your app launches on a detached task or a dedicated service class.
Just like MetricReports, DiagnosticReports are Codable. This code awaits incoming diagnosticReports, then encodes them into JSON using a JSONEncoder.
Now, you can send all of this diagnostic information to your analytics server.
Diagnostics reports are also structured, so you can pick and choose what you want to receive. For example, this code awaits diagnosticReports, and switches on the cases of different types of diagnostics. In the case of a crash diagnostic, it extracts the backtrace, the reason, and the category. Now, this information can be processed by the app, like sending it up to a server. In the case of hang diagnostics, it can use the hang case to process that report differently.
I've covered how to get metric and diagnostic reports for your app. Next, I'm going to show you how to contextualize this data.
So far, the metrics and diagnostics provided by MetricKit, represent the overall picture of the app's performance. But, to investigate individual issues, you might need richer, more granular data that tells you more about the state of the app, like what the user flow is or how the app is configured. MetricKit can give you data contextualized to meaningful information you defined about your app.
Here's an example. I have an expense reporting app that allows employees to scan receipts, submit expenses and track their spending by categories and budgets. These functions are organized in a Reports tab and a Spending tab.
I'm interested in the scroll hitch metric. It indicates that during the course of the day, the app had a total hitch time of 4.5 seconds while scrolling for 5 minutes. That is a hitch rate of 15 milliseconds per second. But that's an average scroll hitch rate over all app usage, even if someone is going back and forth between the Reports tab and the Spending tab. To know what conditions of the app this metric was collected under, you can report app states through the StateReporting framework. I'll explore that now.
States are information you define that describes your app's configuration or behavior, so that MetricKit can aggregate metrics as a function of those characteristics. As people use your app, parameters might change depending on how they're using it.
In the expense app, people might move between tabs during their usage. They could be on the Reports tab to add a new expense, leave the app, and come back to the Spending tab when they want to check on their daily meal budget. As the app transitions between these states, it reports these transitions. Then, MetricKit can intersect them with metric and diagnostic data.
You can also add more details for each state by adding a custom structured type. For the expense app, this could be details about the items in the view, like whether the list of transactions is considered a small, medium or large list and whether the transactions on that list are sorted.
Now, instead of getting a single blended metric across all of these states, like a total scroll hitch rate of 15 ms/s, metrics are reported for each individual state. And in the expense app, there are individual metrics for each tab. In this example, scrolling on the Spending tab was incredibly smooth, with a hitch rate of just 1 ms/s. But, when scrolling through the Reports tab, the hitch rate spiked to 71 ms/s. With this granularity, you can make a much more focused conclusion: the Spending tab is performing great! But the Reports tab is experiencing critical interruptions, and that's exactly where your optimization effort should focus.
Each state you provide is scoped to a domain. A domain describes a function or area of an app. A domain can only have one active state at a given time.
Separate domains allow multiple states to be in flight at the same time. In the expense app, I'm testing out an experimental change, and I want to know if it helps with performance. With the experiment turned on, expenses are fetched in small batches from the database. Turned off, it uses larger batches instead. By placing the tab state and the batch size state in separate domains, MetricKit will deliver separate metrics for each tab and each batch size.
States follow a transition model. Your app reports the state it's moving to, and MetricKit tracks how long it remains in that state. There's no start or end pairs - the app reports the condition it is in, at any given time.
To report states in your app, start by importing the StateReporting framework. Then, create a domain - typically a reverse DNS string - and register it when you set up your MetricManager instance.
Finally, report the transitions as your app enters the states that you define. In this case, the app transitions to a state identified by the string "Reports".
If you want to further granularize your data, you can provide additional structured information for these states by defining your own struct with the ReportableMetadata macro. Then, create a new StateReporter with this metadata type.
Finally, report transitions by including the label and your custom type. This example transitions to the "Reports" state, and also provides a ViewConfiguration struct that includes values for the listSize and whether items on this list are sorted.
Before adding states to your app, your metric report provides broad metrics across all usages of your app. The stateEntries property contains state-aware metrics. This is empty when there are no states reported. After adding states, your MetricReport will now have StateEntry values. State entries provide another way to perceive your app's metrics. Each state has its own StateEntry with metric values aggregated across the time spent in that individual state. When you're ready to send this data to your analytics server, you can choose to group your MetricReport data by state reporting domain. Configure your JSONEncoder to group state entries by each domain. Set the key encodingFormatKey on the encoder's userInfo property to the value byStateReportingDomain. Now, when the report is encoded, both state entries and interval entries, will contain your app's performance metrics, grouped by each domain and state that exists in the report.
Here are some best practices to keep in mind when defining states.
Domains should be narrowly scoped so that each app area can have its state and ability to understand data for those states.
State transitions should represent stable, meaningful phases, not transient UI events.
Carefully consider what each state means, so that if a regression appears, the state will give enough information to target your fix. Carefully plan out the number of state transitions in your app and how you plan to interpret the data that each state and domain generates. Too many states can result in data that's too granular and can actually make it harder to interpret the overall picture. There are also upper limits to the number of states to minimize overhead.
Finally, use the Points of Interest instrument to validate that the states you report match your expectations before you ship.
MetricKit lets you find and fix performance problems faster than ever. Continue to monitor the data on an ongoing basis to target and plan your performance work.
Use MetricManager to start collecting performance metrics to monitor your app's health.
Analyze diagnostics to identify specific opportunities for improvements.
Contextualize the data you get by reporting important states of your app.
Explore the new types of data provided by MetricKit, like memory diagnostics and Metal frame rate metric.
Finally, if you're using the MXMetricManager API, migrate over to the new MetricManager API to take advantage of all these new capabilities. Thank you and have a great WWDC!
-
-
4:59 - Receive metrics from MetricKit
// Receive metrics from MetricKit import MetricKit let manager = MetricManager() for await report in manager.metricReports { processReport(report) } -
5:25 - Send your metrics to the server
// Send your metrics to the server import MetricKit for await report in manager.metricReports { let jsonData = try JSONEncoder().encode(report) sendToServer(jsonData) } -
5:44 - Access your performance metrics
// Access your performance metrics import MetricKit for await report in manager.metricReports { let intervalEntries = report.intervalEntries let fullDayEntry = intervalEntries.fullDayEntry for entry in intervalEntries { let memoryMetrics = entry.values.filter { $0.metricGroup == .memory } for metric in memoryMetrics { switch metric { case .peakMemory(let peak): processPeakMemory(peak) default: break } } } } -
8:59 - Receive diagnostics
// Receive diagnostics import MetricKit let manager = MetricManager() for await report in manager.diagnosticReports { processReport(report) } -
9:14 - Send your diagnostic data to the server
// Send your diagnostic data to the server import MetricKit for await report in manager.diagnosticReports { let jsonData = try JSONEncoder().encode(report) sendToServer(jsonData) } -
9:39 - Access your diagnostic data
// Access your diagnostic data import MetricKit for await report in manager.diagnosticReports { switch report.result { case .crash(let crash): let backtrace = crash.callStackTree let reason = crash.terminationReason let category = crash.terminationCategory processCrash(backtrace: backtrace, reason: reason, category: category) case .hang(let hang): processHangDiagnostic(hang) default: break } } -
13:57 - Receive MetricKit data with states
// Receive MetricKit data with states import MetricKit import StateReporting let domain = StateReportingDomain("com.metrickitsample.tabs") let manager = MetricManager(enabledStateReportingDomains: [domain]) // Report transitions throughout the app let reporter = StateReporter.reporter(for: domain.rawValue) reporter.reportTransition(to: "Reports") -
14:21 - Define custom structured types
// Define custom structured types import StateReporting @ReportableMetadata struct ViewConfiguration { let listSize: String let isSorted: Bool } let reporter = StateReporter.reporter( for: domain.rawValue, stableMetadata: ViewConfiguration.self ) reporter.reportTransition( to: "Reports", stableMetadata: ViewConfiguration(listSize: "large", isSorted: false) ) -
15:29 - Send encoded metric reports to the server
// Send encoded metric reports to the server import MetricKit for await report in manager.metricReports { let encoder = JSONEncoder() let formatKey = MetricReport.encodingFormatKey encoder.userInfo[formatKey] = MetricReport.EncodingFormat.byStateReportingDomain let jsonData = try encoder.encode(report) sendToServer(jsonData) }
-
-
- 0:01 - Introduction
MetricKit is a framework that provides metrics and diagnostics for monitoring real-world app performance. In iOS 27, the framework has been rebuilt from the ground up with a new Swift-first API and new features including Metal frame rate metrics, memory exception diagnostics, and granular data with state reporting.
- 4:07 - Metrics
MetricKit delivers daily reports containing performance metrics — including launch time, hangs, CPU, memory, organized into interval entries and metric groups. Metrics can be retrieved as Codable reports for server-side aggregation or inspected directly by filtering for specific groups and values.
- 7:13 - Diagnostics
When an app encounters a crash, hang, or other failure, MetricKit captures and immediately delivers a diagnostic report containing a symbolicated backtrace and metadata such as exception type and termination reason. iOS 27 adds memory exception diagnostics and a new termination category field on crash diagnostics.
- 10:03 - Context
The State Reporting framework lets apps report their active configuration or user flow as named domains and states, which then allows MetricKit to aggregate data separately for each state rather than blending them. Custom structured metadata can be attached to states using the @ReportableMetadata macro, and per-state metrics are surfaced as StateEntry values in the metric report.