-
새로운 MetricKit 만나 보기
그 어느 때보다 더 빠르게 성능 문제를 찾고 수정하세요. MetricKit이 어떻게 필수적인 성능 지표와 실행 가능한 진단 정보를 제공하여 앱의 개선 기회를 정확히 파악할 수 있도록 지원하는지 살펴보세요. StateReporting 프레임워크를 사용하여 앱 상태별로 앱의 지표와 진단 정보를 결합해 분석하는 방법도 다루며, 앱 경험 최적화를 알아볼 수 있도록 종합적인 인사이트를 제공합니다.
챕터
- 0:01 - Introduction
- 4:07 - Metrics
- 7:13 - Diagnostics
- 10:03 - Context
리소스
- Getting started with StateReporting
- Analyzing app performance with MetricKit
- Monitoring app performance with MetricKit
- Track performance by app state using MetricKit
- MetricKit
관련 비디오
WWDC26
-
비디오 검색…
안녕하세요, 저는 Yonni입니다. MetricKit 팀의 엔지니어입니다.
훌륭한 앱과 게임은 성능을 모니터링하고 최적화합니다 실제 세계, 실제 기기에서 말이죠. MetricKit은 앱 경험의 품질에 대한 실질적인 인사이트를 제공하는 프레임워크입니다 앱 경험의 품질에 대한 인사이트를 제공합니다. 이 세션에서는 MetricKit 소개로 시작하겠습니다. iOS 27의 새로운 기능을 포함해서요. 그런 다음, 첫 번째 메트릭 리포트를 받는 방법을 보여드리겠습니다. 첫 번째 진단 리포트도요. 마지막으로, 더욱 풍부한 데이터를 얻는 방법을 살펴보겠습니다 앱의 특정 영역과 성능 문제를 연결함으로써요.
프레임워크 개요부터 시작하겠습니다. 앱 성능 최적화는 하나의 과정입니다. 데이터 수집부터 시작해서 분석하여 문제를 파악합니다. 각 문제에 대해 근본 원인을 찾기 위해 분류하고, 수정한 후, 1단계로 돌아가 결과를 모니터링합니다. MetricKit은 그 워크플로우에서 수집 단계를 담당합니다. 프레임워크는 두 가지 유형의 데이터를 제공합니다: 메트릭과 진단입니다. 메트릭은 성능 영역이 전반적으로 개선되고 있는지 악화되고 있는지 파악하게 해주고, 진단은 어떤 코드 경로가 성능 문제를 일으키는지 알려줍니다.
iOS 27에서 프레임워크는 완전히 새로 재구축되었습니다 맥락이 풍부하고 표현력 있는 현대적인 Swift 우선 API로요. 새로운 MetricKit API들은 프레임워크의 미래입니다. 오늘 제가 다룰 모든 발전 사항들은 이 새로운 API 세트에만 적용됩니다. 메트릭은 앱의 지속적인 건강 신호입니다. 실행 시간, 행(hang), 애니메이션 메트릭은 앱이 얼마나 반응적이고 부드럽게 느껴지는지 알려줍니다. 느린 실행은 사용자를 불편하게 만들어 앱을 떠나게 할 수 있고, 빠른 실행은 사용자를 앱의 핵심 경험으로 바로 안내합니다.
CPU, GPU, 디스크 쓰기, 네트워크 전송 같은 리소스 소비 메트릭은 앱이 얼마나 열심히 작동하고 있으며 기기 건강에 어떤 영향을 주는지 알려줍니다.
예를 들어, 첫 번째 드로우까지의 시간 메트릭 같은 MetricKit 실행 시간은, 특정 시간 범위 버킷 내에 속하는 실행 횟수의 히스토그램을 제공합니다. 이 그래프는 앱이 실행되는 데 걸린 시간을 보여줍니다, 하루 동안 누군가가 앱을 열 때마다의 시간입니다. 대부분의 실행은 510에서 540밀리초 사이였으며, 몇 가지 예외가 있었습니다. 지속적인 성능도 추적할 수 있습니다 MetricKit이 제공하는 데이터에서 자체 인사이트를 도출하여요. 예를 들어, MetricKit이 이 앱에 대해 보고한 것은, 평균 총 행 시간이 3초였다는 것입니다 앱이 30분 동안 사용되는 동안요. 그 정보를 사용하여 시간당 평균 행 비율 6초를 도출할 수 있습니다. 모든 기기에 걸쳐 이 데이터를 집계하면, 앱 성능이 어떻게 추세를 보이는지 측정 가능한 신호를 얻을 수 있습니다.
iOS 27에서 MetricKit은 각 메트릭을 제공할 수 있습니다 앱 상태의 함수로서요. 예를 들어, 여러 탭이 있는 앱에서 행 시간을 측정할 때, MetricKit은 활성 탭이 언제인지와 교차된 이 메트릭을 제공할 수 있습니다 탭 1, 탭 2, 탭 3일 때요. 이에 대해서는 나중에 더 자세히 다루겠습니다. iOS 27에서 MetricKit은 이제 새로운 메트릭인 Metal 프레임 레이트도 제공합니다. 프레임 레이트는 핵심 메트릭입니다 게임 개발자들이 렌더링 성능을 이해하는 데 있어서요. 플랫폼을 위한 게임 최적화에 대해 더 알아보려면, 다음 세션을 확인하세요 "Metal 게임에서 성능 문제를 찾고 수정하기". 메트릭 외에도, MetricKit은 진단도 제공합니다. 진단에는 파악하는 데 도움이 되는 유용한 정보가 포함되어 있습니다 어떤 코드 경로가 성능 문제를 일으켰는지 조사하고 수정할 수 있도록요.
iOS 27에서 MetricKit은 메모리 예외 진단을 제공합니다. 앱이나 익스텐션이 메모리 한도를 초과하여 종료될 때, 무슨 일이 일어났는지 더 많은 인사이트를 얻을 수 있습니다.
앱이 성능 메트릭을 얻는 방법을 살펴보겠습니다. 사람들이 하루 동안 앱을 사용하면서, MetricKit은 메트릭을 지속적으로 수집합니다 앱 실행, 행, 메모리, CPU 같은 것들을요 그리고 일일 리포트로 앱에 전달합니다. 이 리포트에서 MetricKit은 하루 전체 앱 사용을 포괄하는 항목을 제공합니다. 더 작은 분류를 위한 별도 항목도 제공합니다, 일반적으로 각각 몇 시간씩이요. 이 더 작은 분류들은 연관된 메트릭이 있을 때만 존재합니다. 데이터 구조는 이렇습니다. 각 인터벌 내에서 메트릭은 메트릭 그룹으로 구성됩니다. 각 그룹은 시스템의 한 측면을 나타냅니다, .cpu, .memory, .display, .gpu 같은 것들이요. 그룹 내에서 개별 성능 메트릭을 찾을 수 있습니다.
이것을 코드로 살펴보겠습니다. 진입점은 MetricManager 클래스입니다. 리포트를 받으려면 metricReports 프로퍼티를 통해 await하면 됩니다. 이 설정은 앱 시작 시 완료되어야 합니다 지연된 구독으로 인한 데이터 손실을 방지하기 위해서요. MetricManager는 살아있는 상태를 유지해야 합니다 스트림이 이후 데이터가 준비될 때마다 리포트를 계속 전달할 수 있도록요. 이 몇 줄만으로 앱은 이제 구조화된 메트릭 데이터를 받고 있습니다. 일반적으로 이 메트릭들을 서버에 보내고 싶을 것입니다 많은 기기에 걸쳐 앱의 상태를 확인하기 위해서요. MetricReports는 Codable이므로 인코딩이 쉽습니다 서버에 보내기 위해 JSON 같은 형식으로요. JSONEncoder를 만들고 전체 리포트를 인코딩하면 됩니다.
특정 메트릭 그룹이나 특정 값에만 접근하고 싶다면, 메트릭 리포트를 더 자세히 검사할 수도 있습니다. 이를 위해 intervalEntries를 반복합니다. 여기에는 전체 하루 집계 항목이 포함됩니다 그리고 사용 가능한 경우 더 작은 분류 창도요. 그런 다음, 관심 있는 그룹으로 메트릭을 필터링합니다. 이 경우, memoryMetrics는 메모리 그룹의 메트릭만 포함합니다. 마지막으로, 메트릭 케이스를 스위치하여 관심 있는 메트릭 유형에 접근하고, 이 리포트에서 해당 메트릭의 값도 확인합니다. 이 경우, 코드는 peakMemory 값만 처리합니다. 이 작업은 분리된 태스크나 전용 서비스 클래스에서 수행하세요 앱이 실행되자마자요.
워크플로우로 돌아가서. 이제 수집 단계를 완료했습니다. 분석 단계로 넘어갈 준비가 되었습니다.
모든 기기에 걸쳐 메트릭을 분석하는 것은 데이터 사이언스 문제입니다. 이 분석을 가능하게 하려면, 서버를 설정해야 합니다 각 리포트를 수집하고 관심 있는 차원에 따라 집계할 수 있는 서버를요. 생성하려는 데이터와 찾으려는 인사이트에 대해 최선의 통계 분석이 무엇인지 결정해야 합니다. 커스텀 집계를 사용하면 기준선을 얻을 수 있습니다, 앱이 이미 어떻게 성능을 발휘하고 있는지에 대한 아이디어를요. 그런 다음, 집계된 메트릭을 모니터링하여 상황이 개선되거나 악화될 때를 감지합니다.
메트릭을 사용하여 앱의 문제를 파악하는 방법을 보여드렸습니다. 이제, 진단을 사용하여 이러한 문제를 수정하는 방법을 다루겠습니다.
메트릭은 앱을 모니터링하는 훌륭한 방법입니다. 메트릭을 수집하고 시간에 따른 성능을 모니터링하면서, 워크플로우의 분류 단계로 진입할 수 있습니다. 진단은 이 단계에서 특히 유용합니다.
크래시나 행 같은 문제가 발생하면, 시스템이 기기에서 진단을 캡처합니다. 진단 리포트는 세부 정보를 패키지화하여 MetricKit을 통해 즉시 앱에 전달합니다. 내부에는 문제를 분류하는 데 도움이 되는 유용한 정보가 있습니다. 예를 들어, 많은 진단에는 백트레이스가 포함되어 있어 이벤트 당시의 정확한 호출 스택을 보여줍니다.
가장 중요한 진단 중 하나는 크래시에 관한 것입니다. 크래시 진단은 백트레이스를 제공할 뿐만 아니라, 앱이 왜 종료되었는지도 나타내줍니다 그리고 어떤 종류의 실패인지 알려주는 예외 유형도요.
iOS 27에서 종료 카테고리가 각 크래시가 메트릭에서 어떻게 반영되었는지 이제 나타냅니다. 그렇게 하면, 비정상 종료가 증가 추세라면, 개별 진단과 직접 연관지을 수 있습니다. 이 예시에서, 심볼화된 백트레이스는 스레드 시작 시 시스템에서 시작됩니다. 실행이 아래로 흘러가면서 앱의 코드로 진입합니다. 크래시 지점을 찾으려면 호출을 끝까지 따라가면 됩니다. 여기서 실행은 앱의 submitReport() 함수에 도달하고 멈춥니다. 이것은 실행 경로에서 이 부분이 실패 지점임을 나타냅니다.
이제 이 정보를 사용하여 이 함수의 수정에 집중할 수 있습니다. 진단 리포트를 받으려면, MetricManager 인스턴스의 diagnosticReports를 await하면 됩니다. MetricReports처럼, 앱이 실행되자마자 이 스트림을 수신 시작하세요 분리된 태스크나 전용 서비스 클래스에서요.
MetricReports와 마찬가지로, DiagnosticReports도 Codable입니다. 이 코드는 들어오는 diagnosticReports를 await하고, JSONEncoder를 사용하여 JSON으로 인코딩합니다. 이제 이 모든 진단 정보를 분석 서버로 보낼 수 있습니다.
진단 리포트도 구조화되어 있으므로, 받고 싶은 것을 선택할 수 있습니다. 예를 들어, 이 코드는 diagnosticReports를 await하고, 다른 유형의 진단 케이스를 스위치합니다. 크래시 진단의 경우, 백트레이스를 추출하고, 이유와 카테고리를 추출합니다. 이제 이 정보는 앱에서 처리될 수 있습니다, 서버에 보내는 것처럼요. 행 진단의 경우, 행 케이스를 사용하여 그 리포트를 다르게 처리할 수 있습니다.
앱의 메트릭 및 진단 리포트를 얻는 방법을 다루었습니다. 다음으로, 이 데이터를 맥락화하는 방법을 보여드리겠습니다. 지금까지 MetricKit이 제공하는 메트릭과 진단은, 앱 성능의 전체적인 그림을 나타냅니다. 하지만, 개별 문제를 조사하려면, 더 풍부하고 세분화된 데이터가 필요할 수 있습니다 앱의 상태에 대해 더 많은 것을 알려주는, 사용자 흐름이 무엇인지 또는 앱이 어떻게 구성되어 있는지 같은 것들요. MetricKit은 맥락화된 데이터를 제공할 수 있습니다 앱에 대해 정의한 의미 있는 정보로요. 예시를 보겠습니다. 직원들이 영수증을 스캔하고 경비를 제출할 수 있는 경비 보고 앱이 있습니다, 카테고리와 예산별로 지출을 추적하는 앱이요. 이 기능들은 Reports 탭과 Spending 탭으로 구성되어 있습니다.
저는 스크롤 히치 메트릭에 관심이 있습니다. 하루 동안 앱이 총 4.5초의 히치 시간을 가졌다는 것을 나타냅니다 5분 동안 스크롤하는 동안요. 그것은 초당 15밀리초의 히치 비율입니다. 하지만 그것은 모든 앱 사용에 걸친 평균 스크롤 히치 비율이고, 누군가가 Reports 탭과 Spending 탭 사이를 왔다 갔다 하는 경우도 포함합니다. 이 메트릭이 어떤 앱 조건에서 수집되었는지 알려면, StateReporting 프레임워크를 통해 앱 상태를 보고할 수 있습니다. 지금 살펴보겠습니다.
상태는 앱의 구성을 설명하기 위해 정의하는 정보입니다 또는 동작으로, MetricKit이 메트릭을 집계할 수 있도록 합니다 그러한 특성의 함수로서요. 사람들이 앱을 사용하면서, 사용 방식에 따라 매개변수가 변경될 수 있습니다.
경비 앱에서, 사람들은 사용 중 탭 간을 이동할 수 있습니다. 새 경비를 추가하기 위해 Reports 탭에 있다가 앱을 나가고, Spending 탭으로 돌아올 수 있습니다 일일 식사 예산을 확인하고 싶을 때요. 앱이 이러한 상태 간을 전환할 때, 이 전환을 보고합니다. 그러면 MetricKit이 메트릭 및 진단 데이터와 교차할 수 있습니다.
각 상태에 대해 더 많은 세부 정보를 추가할 수도 있습니다 커스텀 구조체 유형을 추가하여요. 경비 앱의 경우, 이것은 뷰의 항목들에 대한 세부 정보일 수 있습니다, 거래 목록이 소형, 중형, 또는 대형 목록으로 간주되는지 그리고 해당 목록의 거래가 정렬되어 있는지 여부 같은 것들이요.
이제, 이 모든 상태에 걸쳐 혼합된 단일 메트릭을 얻는 대신, 총 스크롤 히치 비율 15 ms/s 같은, 메트릭은 각 개별 상태에 대해 보고됩니다. 그리고 경비 앱에서, 각 탭에 대한 개별 메트릭이 있습니다. 이 예시에서, Spending 탭에서의 스크롤은 매우 부드러웠습니다, 히치 비율이 단 1 ms/s로요. 하지만, Reports 탭을 스크롤할 때는, 히치 비율이 71 ms/s로 급증했습니다. 이 세분성으로, 훨씬 더 집중된 결론을 내릴 수 있습니다: WEBVTT Spending 탭은 훌륭하게 성능을 발휘하고 있습니다! 하지만 Reports 탭은 심각한 중단을 겪고 있으며, 그것이 바로 최적화 노력이 집중되어야 할 곳입니다.
제공하는 각 상태는 도메인에 범위가 지정됩니다. 도메인은 앱의 기능이나 영역을 설명합니다. 도메인은 주어진 시간에 하나의 활성 상태만 가질 수 있습니다. 별도 도메인을 사용하면 여러 상태가 동시에 진행될 수 있습니다. 경비 앱에서, 저는 실험적 변경사항을 테스트하고 있습니다, 그것이 성능에 도움이 되는지 알고 싶습니다. 실험이 켜져 있을 때, 경비는 데이터베이스에서 작은 배치로 가져옵니다. 꺼져 있을 때는 대신 더 큰 배치를 사용합니다. 탭 상태를 배치하고 배치 크기 상태를 별도 도메인에 배치함으로써, MetricKit은 각 탭과 배치 크기에 대해 별도 메트릭을 전달할 것입니다.
상태는 전환 모델을 따릅니다. 앱은 전환하려는 상태를 보고하고, MetricKit은 그 상태에 얼마나 오래 머무는지 추적합니다. 시작 또는 종료 쌍이 없습니다 - 앱은 현재 있는 조건을 보고합니다, 주어진 시간에요.
앱에서 상태를 보고하려면, StateReporting 프레임워크를 import하는 것부터 시작하세요. 그런 다음, 도메인을 만드세요 - 일반적으로 역방향 DNS 문자열 - MetricManager 인스턴스를 설정할 때 등록하세요. 마지막으로, 앱이 정의한 상태로 진입할 때 전환을 보고합니다. 이 경우, 앱은 상태로 전환합니다 "Reports" 문자열로 식별되는요. 데이터를 더 세분화하고 싶다면, 이 상태들에 대해 추가적인 구조화된 정보를 제공할 수 있습니다 ReportableMetadata 매크로로 자체 구조체를 정의하여요. 그런 다음, 이 메타데이터 유형으로 새로운 StateReporter를 만드세요. 마지막으로, 레이블과 커스텀 유형을 포함하여 전환을 보고합니다. 이 예시는 "Reports" 상태로 전환하고, ViewConfiguration 구조체도 제공합니다 listSize 값과 이 목록의 항목이 정렬되어 있는지 여부를 포함하여요.
앱에 상태를 추가하기 전에, 메트릭 리포트는 앱의 모든 사용에 걸쳐 광범위한 메트릭을 제공합니다. stateEntries 프로퍼티는 상태 인식 메트릭을 포함합니다. 보고된 상태가 없을 때는 비어 있습니다. 상태를 추가한 후, MetricReport는 이제 StateEntry 값을 가집니다. 상태 항목은 앱의 메트릭을 인식하는 또 다른 방법을 제공합니다. 각 상태는 자체 StateEntry를 가집니다 해당 개별 상태에서 보낸 시간에 걸쳐 집계된 메트릭 값과 함께요. 이 데이터를 분석 서버로 보낼 준비가 되면, MetricReport 데이터를 상태 보고 도메인별로 그룹화하도록 선택할 수 있습니다. JSONEncoder를 구성하여 각 도메인별로 상태 항목을 그룹화하세요. 인코더의 userInfo 프로퍼티에 encodingFormatKey 키를 설정하세요 byStateReportingDomain 값으로요. 이제, 리포트가 인코딩될 때, 상태 항목과 인터벌 항목 모두, 앱의 성능 메트릭을 포함하게 됩니다, 리포트에 있는 각 도메인과 상태별로 그룹화되어요.
상태를 정의할 때 염두에 두어야 할 모범 사례들입니다.
도메인은 좁게 범위가 지정되어야 합니다 각 앱 영역이 자체 상태를 가질 수 있도록 그리고 해당 상태에 대한 데이터를 이해하는 능력을요.
상태 전환은 안정적이고 의미 있는 단계를 나타내야 합니다, 일시적인 UI 이벤트가 아니라요. 각 상태가 의미하는 바를 신중하게 고려하세요, 회귀가 나타난다면, 상태가 수정 대상을 찾기에 충분한 정보를 제공할 것입니다. 앱의 상태 전환 수를 신중하게 계획하고 각 상태와 도메인이 생성하는 데이터를 어떻게 해석할 계획인지도요. 너무 많은 상태는 데이터가 너무 세분화될 수 있고 전체적인 그림을 해석하기 더 어렵게 만들 수 있습니다. 오버헤드를 최소화하기 위해 상태 수에도 상한이 있습니다. 마지막으로, Points of Interest 도구를 사용하여 배포하기 전에 보고한 상태가 예상과 일치하는지 검증하세요. MetricKit을 통해 그 어느 때보다 빠르게 성능 문제를 찾고 수정할 수 있습니다. 지속적으로 데이터를 모니터링하여 성능 작업의 대상을 설정하고 계획하세요.
MetricManager를 사용하여 성능 메트릭 수집을 시작하세요 앱의 건강을 모니터링하기 위해서요.
진단을 분석하여 개선 기회를 구체적으로 파악하세요. 앱의 중요한 상태를 보고하여 얻는 데이터를 맥락화하세요. MetricKit이 제공하는 새로운 유형의 데이터를 탐색하세요, 메모리 진단과 Metal 프레임 레이트 메트릭 같은 것들이요. 마지막으로, MXMetricManager API를 사용하고 있다면, 새로운 MetricManager API로 마이그레이션하세요 이 모든 새로운 기능들을 활용하기 위해서요. 감사합니다, 즐거운 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.