{"$schema": "https://raw.githubusercontent.com/guitaripod/wwdc-sessions/master/schema/transcript.schema.json", "id": "wwdc2026-222", "language": "eng", "source": "https://developer.apple.com/videos/play/wwdc2026/222", "wordCount": 2668, "segments": [{"start": 7.0, "text": "Hi, I'm Yonni, and I'm an engineer on the MetricKit team."}, {"start": 11.0, "text": "Great apps and games monitor and optimize their performance"}, {"start": 14.0, "text": "out in the real world, on real devices."}, {"start": 17.0, "text": "MetricKit is the framework that can provide you real insights"}, {"start": 20.0, "text": "into the quality of your app's experience."}, {"start": 23.0, "text": "In this session, I will start off with an introduction to MetricKit,"}, {"start": 27.0, "text": "including what's new in iOS 27."}, {"start": 30.0, "text": "Then, I'll show you how to start receiving your first metric report."}, {"start": 34.0, "text": "Your first diagnostic report."}, {"start": 37.0, "text": "And finally, I'll explore how to get even more rich data"}, {"start": 40.0, "text": "by connecting performance problems to specific areas in your app."}, {"start": 45.0, "text": "I'll start with an overview of the framework."}, {"start": 48.0, "text": "Optimizing your app's performance is a process."}, {"start": 51.0, "text": "You start by collecting data, and analyze it to identify problems."}, {"start": 55.0, "text": "For each problem, you triage it to find the root cause,"}, {"start": 59.0, "text": "fix it, and go back to step one to monitor the results."}, {"start": 63.0, "text": "MetricKit is the collection piece in that workflow."}, {"start": 67.0, "text": "The framework provides two types of data: metrics and diagnostics."}, {"start": 72.0, "text": "Metrics give you a sense of whether an area of performance"}, {"start": 75.0, "text": "is improving or worsening overall,"}, {"start": 78.0, "text": "while diagnostics tell you which code path is causing a performance problem."}, {"start": 83.0, "text": "In iOS 27, the framework has been rebuilt from the ground up"}, {"start": 88.0, "text": "with a contextually rich and expressive modern Swift-first API."}, {"start": 93.0, "text": "The new MetricKit APIs are the future of the framework."}, {"start": 97.0, "text": "All of the advances I'll be discussing today"}, {"start": 99.0, "text": "are exclusive to this new set of APIs."}, {"start": 102.0, "text": "Metrics are your app's ongoing health signal."}, {"start": 106.0, "text": "Launch time, hangs, and animation metrics"}, {"start": 108.0, "text": "tell you how responsive and smooth your app feels."}, {"start": 112.0, "text": "A slow launch can frustrate people and lead them to leave your app,"}, {"start": 115.0, "text": "while a fast launch gets people right into your app's core experiences."}, {"start": 120.0, "text": "Resource consumption metrics like CPU, GPU, Disk writes, and network transfers"}, {"start": 126.0, "text": "tell you how hard your app is working and how it's affecting device health."}, {"start": 131.0, "text": "For example, MetricKit launch times like the time to first draw metric,"}, {"start": 135.0, "text": "provide a histogram of launch counts that fall within certain time range buckets."}, {"start": 140.0, "text": "This graph shows the time it took for the app to launch,"}, {"start": 143.0, "text": "every time someone opened it over the course of a day."}, {"start": 147.0, "text": "Most of the launches took between 510 and 540 milliseconds, with a few outliers."}, {"start": 154.0, "text": "You can also track ongoing performance"}, {"start": 156.0, "text": "by deriving your own insights from the data MetricKit provides."}, {"start": 160.0, "text": "For example, MetricKit reported that for this app,"}, {"start": 163.0, "text": "it had a total hang time on average of 3 seconds"}, {"start": 167.0, "text": "while the app was used for 30 minutes."}, {"start": 169.0, "text": "That information can be used to derive an average hang rate of 6 seconds per hour."}, {"start": 175.0, "text": "If you aggregate this data across all devices,"}, {"start": 178.0, "text": "this can give you a measurable signal of how your app's performance is trending."}, {"start": 183.0, "text": "In iOS 27, MetricKit can provide each metric"}, {"start": 187.0, "text": "as a function of the app's state."}, {"start": 190.0, "text": "For example, when measuring hang time in an app that has multiple tabs,"}, {"start": 194.0, "text": "MetricKit can provide this metric intersected with when the active tab"}, {"start": 198.0, "text": "is tab 1, tab 2 or tab 3."}, {"start": 200.0, "text": "I'll go into this more later."}, {"start": 203.0, "text": "In iOS 27, MetricKit now also provides a new metric - Metal frame rate."}, {"start": 208.0, "text": "Frame rate is a key metric"}, {"start": 210.0, "text": "for game developers to understand render performance."}, {"start": 213.0, "text": "To learn more about optimizing your game for the platform, check out the session"}, {"start": 217.0, "text": "\"Find and fix performance issues in your Metal game\"."}, {"start": 221.0, "text": "In addition to metrics, MetricKit also provides diagnostics."}, {"start": 225.0, "text": "Diagnostics contain useful information that helps you identify"}, {"start": 229.0, "text": "which code path caused a performance problem"}, {"start": 232.0, "text": "so you can investigate and fix it."}, {"start": 235.0, "text": "In iOS 27, MetricKit provides memory exception diagnostics."}, {"start": 239.0, "text": "So when your app or extension is terminated for exceeding its memory limit,"}, {"start": 243.0, "text": "you get more insight on what happened."}, {"start": 246.0, "text": "I'll dive in to explore how your app can get performance metrics."}, {"start": 251.0, "text": "As people use your app throughout the day, MetricKit continuously collects metrics"}, {"start": 255.0, "text": "like app launches, hangs, memory, and CPU"}, {"start": 259.0, "text": "and delivers it to your app in a daily report."}, {"start": 263.0, "text": "In this report, MetricKit provides an entry that spans the full day usage of the app."}, {"start": 268.0, "text": "It also provides separate entries for smaller breakdowns,"}, {"start": 272.0, "text": "typically a few hours each."}, {"start": 274.0, "text": "These smaller breakdowns are only present"}, {"start": 276.0, "text": "when there are metrics associated with them."}, {"start": 279.0, "text": "Here's how the data is structured."}, {"start": 281.0, "text": "Inside each interval, metrics are organized into metric groups."}, {"start": 285.0, "text": "Each group represents an aspect of the system,"}, {"start": 288.0, "text": "things like .cpu, .memory, .display, and .gpu."}, {"start": 293.0, "text": "Inside a group, you'll find individual performance metrics."}, {"start": 298.0, "text": "I'll work through this in code."}, {"start": 301.0, "text": "Your entry point is the MetricManager class."}, {"start": 303.0, "text": "To receive reports, you await them through the metricReports property."}, {"start": 308.0, "text": "This setup should be done at app start up"}, {"start": 310.0, "text": "to avoid any data loss from delayed subscription."}, {"start": 313.0, "text": "MetricManager should be kept alive"}, {"start": 315.0, "text": "so that the streams can continue to deliver reports as subsequent data becomes ready."}, {"start": 320.0, "text": "With just these few lines, your app is now receiving structured metric data."}, {"start": 326.0, "text": "Typically, you may want to send these metrics to a server"}, {"start": 328.0, "text": "so you can examine your app's health across many devices."}, {"start": 332.0, "text": "MetricReports are Codable, which makes it easy to encode"}, {"start": 336.0, "text": "into a format like JSON to send to the server."}, {"start": 339.0, "text": "Just create a JSONEncoder and encode the entire report."}, {"start": 344.0, "text": "If you just want to access a particular group of metrics, or a particular value,"}, {"start": 349.0, "text": "you can also inspect the metric report further."}, {"start": 352.0, "text": "To do this, iterate through your intervalEntries."}, {"start": 355.0, "text": "This includes a full-day aggregated entry"}, {"start": 357.0, "text": "and smaller breakdown windows when available."}, {"start": 360.0, "text": "Then, filter the metrics down to the group you are interested in."}, {"start": 364.0, "text": "In this case, memoryMetrics contains only metrics in the memory group."}, {"start": 369.0, "text": "Finally, switch over the metric cases"}, {"start": 372.0, "text": "to access the type of metric you're interested in,"}, {"start": 374.0, "text": "as well as the value of that metric in this report."}, {"start": 377.0, "text": "In this case, the code only handles the peakMemory value."}, {"start": 381.0, "text": "Perform this work in a detached task or a dedicated service class"}, {"start": 385.0, "text": "as soon as your app launches."}, {"start": 388.0, "text": "Going back to the workflow."}, {"start": 390.0, "text": "You have now completed the collection phase."}, {"start": 393.0, "text": "And ready to move on to analysis."}, {"start": 397.0, "text": "Analyzing metrics across all devices is a data science problem."}, {"start": 402.0, "text": "To enable this analysis, you'll want to set up a server"}, {"start": 405.0, "text": "that can ingest each of these reports"}, {"start": 407.0, "text": "and aggregate them according to the dimensions that you care about."}, {"start": 411.0, "text": "You'll need to decide what the best statistical analysis is"}, {"start": 415.0, "text": "for the data that you want to generate, and the insights that you want to find."}, {"start": 420.0, "text": "Using your custom aggregation, this can give you a baseline,"}, {"start": 423.0, "text": "an idea of how your app is performing already."}, {"start": 427.0, "text": "Then, monitor your aggregated metrics"}, {"start": 429.0, "text": "to detect when things are getting better or worse."}, {"start": 433.0, "text": "I've shown how you could identify issues in your app using metrics."}, {"start": 437.0, "text": "Now, I will cover how you can fix these issues using diagnostics."}, {"start": 442.0, "text": "Metrics are a great way to monitor your app."}, {"start": 445.0, "text": "As you collect metrics and monitor performance over time,"}, {"start": 449.0, "text": "you can enter the triage phase of your workflow."}, {"start": 452.0, "text": "Diagnostics are particularly helpful in this phase."}, {"start": 457.0, "text": "When something goes wrong, like a crash or a hang,"}, {"start": 460.0, "text": "the system captures a diagnostic on device."}, {"start": 463.0, "text": "A diagnostic report packages up the details"}, {"start": 466.0, "text": "and delivers it immediately to your app through MetricKit."}, {"start": 470.0, "text": "Inside, you get useful information to triage the issue."}, {"start": 474.0, "text": "For example, many diagnostics include backtraces"}, {"start": 477.0, "text": "that show you the exact call stack at the time of the event."}, {"start": 482.0, "text": "One of the most important diagnostics is for crashes."}, {"start": 486.0, "text": "Crash diagnostics not only provide a backtrace,"}, {"start": 489.0, "text": "but they'll also indicate why your app was terminated"}, {"start": 492.0, "text": "and an exception type that tells you what kind of failure it is."}, {"start": 496.0, "text": "In iOS 27, a termination category"}, {"start": 499.0, "text": "now indicates how each crash was accounted for in metrics."}, {"start": 504.0, "text": "That way, if abnormal terminations are trending up,"}, {"start": 507.0, "text": "you can correlate those directly with individual diagnostics."}, {"start": 511.0, "text": "In this example, the symbolicated backtrace begins in the system at thread start."}, {"start": 517.0, "text": "As execution flows downward, it crosses into the app's code."}, {"start": 521.0, "text": "To find the crash site, you can follow the calls all the way down."}, {"start": 525.0, "text": "Here, execution reaches the app's submitReport() function and stops."}, {"start": 530.0, "text": "This indicates that this is the point of failure in the execution path."}, {"start": 535.0, "text": "Now, you can use this information to target fixes in this function."}, {"start": 539.0, "text": "To get diagnostic reports,"}, {"start": 541.0, "text": "you await on the diagnosticReports of your MetricManager instance."}, {"start": 546.0, "text": "Like MetricReports, start listening to this stream as soon as a your app launches"}, {"start": 550.0, "text": "on a detached task or a dedicated service class."}, {"start": 554.0, "text": "Just like MetricReports, DiagnosticReports are Codable."}, {"start": 558.0, "text": "This code awaits incoming diagnosticReports,"}, {"start": 561.0, "text": "then encodes them into JSON using a JSONEncoder."}, {"start": 566.0, "text": "Now, you can send all of this diagnostic information to your analytics server."}, {"start": 571.0, "text": "Diagnostics reports are also structured,"}, {"start": 574.0, "text": "so you can pick and choose what you want to receive."}, {"start": 577.0, "text": "For example, this code awaits diagnosticReports,"}, {"start": 581.0, "text": "and switches on the cases of different types of diagnostics."}, {"start": 585.0, "text": "In the case of a crash diagnostic, it extracts the backtrace,"}, {"start": 589.0, "text": "the reason, and the category."}, {"start": 591.0, "text": "Now, this information can be processed by the app,"}, {"start": 594.0, "text": "like sending it up to a server."}, {"start": 596.0, "text": "In the case of hang diagnostics,"}, {"start": 598.0, "text": "it can use the hang case to process that report differently."}, {"start": 603.0, "text": "I've covered how to get metric and diagnostic reports for your app."}, {"start": 607.0, "text": "Next, I'm going to show you how to contextualize this data."}, {"start": 610.0, "text": "So far, the metrics and diagnostics provided by MetricKit,"}, {"start": 614.0, "text": "represent the overall picture of the app's performance."}, {"start": 617.0, "text": "But, to investigate individual issues, you might need richer, more granular data"}, {"start": 623.0, "text": "that tells you more about the state of the app,"}, {"start": 625.0, "text": "like what the user flow is or how the app is configured."}, {"start": 629.0, "text": "MetricKit can give you data contextualized"}, {"start": 631.0, "text": "to meaningful information you defined about your app."}, {"start": 636.0, "text": "Here's an example."}, {"start": 637.0, "text": "I have an expense reporting app that allows employees to scan receipts,"}, {"start": 641.0, "text": "submit expenses and track their spending by categories and budgets."}, {"start": 645.0, "text": "These functions are organized in a Reports tab and a Spending tab."}, {"start": 650.0, "text": "I'm interested in the scroll hitch metric."}, {"start": 653.0, "text": "It indicates that during the course of the day,"}, {"start": 655.0, "text": "the app had a total hitch time of 4.5 seconds"}, {"start": 659.0, "text": "while scrolling for 5 minutes."}, {"start": 661.0, "text": "That is a hitch rate of 15 milliseconds per second."}, {"start": 664.0, "text": "But that's an average scroll hitch rate over all app usage,"}, {"start": 668.0, "text": "even if someone is going back and forth between the Reports tab and the Spending tab."}, {"start": 673.0, "text": "To know what conditions of the app this metric was collected under,"}, {"start": 677.0, "text": "you can report app states through the StateReporting framework."}, {"start": 680.0, "text": "I'll explore that now."}, {"start": 683.0, "text": "States are information you define that describes your app's configuration"}, {"start": 687.0, "text": "or behavior, so that MetricKit can aggregate metrics"}, {"start": 690.0, "text": "as a function of those characteristics."}, {"start": 693.0, "text": "As people use your app,"}, {"start": 694.0, "text": "parameters might change depending on how they're using it."}, {"start": 698.0, "text": "In the expense app, people might move between tabs during their usage."}, {"start": 702.0, "text": "They could be on the Reports tab to add a new expense, leave the app,"}, {"start": 706.0, "text": "and come back to the Spending tab"}, {"start": 708.0, "text": "when they want to check on their daily meal budget."}, {"start": 711.0, "text": "As the app transitions between these states,"}, {"start": 713.0, "text": "it reports these transitions."}, {"start": 715.0, "text": "Then, MetricKit can intersect them with metric and diagnostic data."}, {"start": 721.0, "text": "You can also add more details for each state"}, {"start": 723.0, "text": "by adding a custom structured type."}, {"start": 725.0, "text": "For the expense app, this could be details about the items in the view,"}, {"start": 729.0, "text": "like whether the list of transactions is considered a small, medium or large list"}, {"start": 734.0, "text": "and whether the transactions on that list are sorted."}, {"start": 738.0, "text": "Now, instead of getting a single blended metric across all of these states,"}, {"start": 742.0, "text": "like a total scroll hitch rate of 15 ms/s,"}, {"start": 746.0, "text": "metrics are reported for each individual state."}, {"start": 749.0, "text": "And in the expense app, there are individual metrics for each tab."}, {"start": 753.0, "text": "In this example, scrolling on the Spending tab was incredibly smooth,"}, {"start": 757.0, "text": "with a hitch rate of just 1 ms/s."}, {"start": 761.0, "text": "But, when scrolling through the Reports tab,"}, {"start": 763.0, "text": "the hitch rate spiked to 71 ms/s."}, {"start": 767.0, "text": "With this granularity, you can make a much more focused conclusion:"}, {"start": 771.0, "text": "the Spending tab is performing great!"}, {"start": 773.0, "text": "But the Reports tab is experiencing critical interruptions,"}, {"start": 777.0, "text": "and that's exactly where your optimization effort should focus."}, {"start": 781.0, "text": "Each state you provide is scoped to a domain."}, {"start": 785.0, "text": "A domain describes a function or area of an app."}, {"start": 788.0, "text": "A domain can only have one active state at a given time."}, {"start": 793.0, "text": "Separate domains allow multiple states to be in flight at the same time."}, {"start": 798.0, "text": "In the expense app, I'm testing out an experimental change,"}, {"start": 801.0, "text": "and I want to know if it helps with performance."}, {"start": 804.0, "text": "With the experiment turned on,"}, {"start": 806.0, "text": "expenses are fetched in small batches from the database."}, {"start": 809.0, "text": "Turned off, it uses larger batches instead."}, {"start": 813.0, "text": "By placing the tab state"}, {"start": 814.0, "text": "and the batch size state in separate domains,"}, {"start": 817.0, "text": "MetricKit will deliver separate metrics for each tab and each batch size."}, {"start": 823.0, "text": "States follow a transition model."}, {"start": 825.0, "text": "Your app reports the state it's moving to,"}, {"start": 828.0, "text": "and MetricKit tracks how long it remains in that state."}, {"start": 831.0, "text": "There's no start or end pairs - the app reports the condition it is in,"}, {"start": 835.0, "text": "at any given time."}, {"start": 837.0, "text": "To report states in your app, start by importing the StateReporting framework."}, {"start": 842.0, "text": "Then, create a domain - typically a reverse DNS string -"}, {"start": 846.0, "text": "and register it when you set up your MetricManager instance."}, {"start": 850.0, "text": "Finally, report the transitions as your app enters the states that you define."}, {"start": 855.0, "text": "In this case, the app transitions to a state"}, {"start": 857.0, "text": "identified by the string \"Reports\"."}, {"start": 861.0, "text": "If you want to further granularize your data,"}, {"start": 863.0, "text": "you can provide additional structured information for these states"}, {"start": 866.0, "text": "by defining your own struct with the ReportableMetadata macro."}, {"start": 871.0, "text": "Then, create a new StateReporter with this metadata type."}, {"start": 875.0, "text": "Finally, report transitions by including the label and your custom type."}, {"start": 880.0, "text": "This example transitions to the \"Reports\" state,"}, {"start": 883.0, "text": "and also provides a ViewConfiguration struct"}, {"start": 886.0, "text": "that includes values for the listSize and whether items on this list are sorted."}, {"start": 891.0, "text": "Before adding states to your app,"}, {"start": 893.0, "text": "your metric report provides broad metrics across all usages of your app."}, {"start": 898.0, "text": "The stateEntries property contains state-aware metrics."}, {"start": 902.0, "text": "This is empty when there are no states reported."}, {"start": 905.0, "text": "After adding states, your MetricReport will now have StateEntry values."}, {"start": 910.0, "text": "State entries provide another way to perceive your app's metrics."}, {"start": 914.0, "text": "Each state has its own StateEntry"}, {"start": 917.0, "text": "with metric values aggregated across the time spent in that individual state."}, {"start": 922.0, "text": "When you're ready to send this data to your analytics server,"}, {"start": 925.0, "text": "you can choose to group your MetricReport data by state reporting domain."}, {"start": 929.0, "text": "Configure your JSONEncoder to group state entries by each domain."}, {"start": 934.0, "text": "Set the key encodingFormatKey on the encoder's userInfo property"}, {"start": 938.0, "text": "to the value byStateReportingDomain."}, {"start": 941.0, "text": "Now, when the report is encoded, both state entries and interval entries,"}, {"start": 946.0, "text": "will contain your app's performance metrics,"}, {"start": 949.0, "text": "grouped by each domain and state that exists in the report."}, {"start": 953.0, "text": "Here are some best practices to keep in mind when defining states."}, {"start": 958.0, "text": "Domains should be narrowly scoped so that each app area can have its state"}, {"start": 962.0, "text": "and ability to understand data for those states."}, {"start": 966.0, "text": "State transitions should represent stable, meaningful phases,"}, {"start": 970.0, "text": "not transient UI events."}, {"start": 973.0, "text": "Carefully consider what each state means, so that if a regression appears,"}, {"start": 978.0, "text": "the state will give enough information to target your fix."}, {"start": 982.0, "text": "Carefully plan out the number of state transitions in your app"}, {"start": 985.0, "text": "and how you plan to interpret the data that each state and domain generates."}, {"start": 989.0, "text": "Too many states can result in data that's too granular"}, {"start": 992.0, "text": "and can actually make it harder to interpret the overall picture."}, {"start": 996.0, "text": "There are also upper limits to the number of states to minimize overhead."}, {"start": 1001.0, "text": "Finally, use the Points of Interest instrument"}, {"start": 1004.0, "text": "to validate that the states you report match your expectations before you ship."}, {"start": 1009.0, "text": "MetricKit lets you find and fix performance problems faster than ever."}, {"start": 1013.0, "text": "Continue to monitor the data on an ongoing basis"}, {"start": 1016.0, "text": "to target and plan your performance work."}, {"start": 1020.0, "text": "Use MetricManager to start collecting performance metrics"}, {"start": 1023.0, "text": "to monitor your app's health."}, {"start": 1025.0, "text": "Analyze diagnostics to identify specific opportunities for improvements."}, {"start": 1030.0, "text": "Contextualize the data you get by reporting important states of your app."}, {"start": 1035.0, "text": "Explore the new types of data provided by MetricKit,"}, {"start": 1038.0, "text": "like memory diagnostics and Metal frame rate metric."}, {"start": 1042.0, "text": "Finally, if you're using the MXMetricManager API,"}, {"start": 1046.0, "text": "migrate over to the new MetricManager API"}, {"start": 1049.0, "text": "to take advantage of all these new capabilities."}, {"start": 1052.0, "text": "Thank you and have a great WWDC!"}]}