Saber que una aplicación ha sufrido un cuelgue, algún tirón de interfaz o una terminación inesperada es útil, pero muchas veces no es suficiente. El dato importante suele ser otro: qué estaba haciendo realmente la aplicación cuando ocurrió el problema. iOS 27 introduce StateReporting, un nuevo framework que permite añadir ese contexto funcional a los diagnósticos que recogen MetricKit e Instruments.
Hasta ahora, MetricKit podía decirnos que una aplicación había acumulado cierto tiempo estando bloqueada o que una animación había perdido fluidez. El problema es que esas cifras mezclaban situaciones muy distintas. Una pantalla sencilla podía funcionar perfectamente y otra mucho más pesada concentrar casi todos los problemas, pero el agregado global ocultaba esa diferencia. StateReporting permite definir estados propios de la aplicación para que MetricKit pueda relacionar determinadas métricas con ellos.
La idea no es convertir StateReporting en otro sistema de analítica ni utilizarlo como sustituto de Logger, OSSignposter o las herramientas que ya tengamos para telemetría. Su objetivo es mucho más concreto: describir el contexto de ejecución que resulta relevante para analizar rendimiento y diagnósticos. Apple lo integra directamente con MetricKit y con el instrumento Points of Interest de Instruments, de forma que los cambios de estado aparecen junto al resto de la información de rendimiento.
La llegada de StateReporting también encaja con la renovación de MetricKit en iOS 27. Apple ha introducido una nueva API orientada a Swift basada en MetricManager, MetricReport y DiagnosticReport, con medición mediante secuencias asíncronas. Dentro de ese nuevo modelo, las métricas pueden analizarse no solo por intervalos temporales, sino también utilizando los estados que la propia aplicación haya reportado.
Un estado se compone principalmente de un dominio, una etiqueta y, opcionalmente, metadatos. El dominio identifica una parte funcional de la aplicación mediante una cadena en formato DNS inverso. Por ejemplo, un editor de vídeo podría tener un dominio dedicado a la exportación:
let domain = "com.example.video-editor.export"
Cada dominio puede tener un único estado activo al mismo tiempo. Esto significa que dentro del dominio anterior podríamos pasar por estados como Preparing, Encoding y Saving, pero no tener Preparing y Encoding activos simultáneamente.
Si existen procesos independientes que pueden ocurrir a la vez, lo correcto es utilizar dominios diferentes. Una aplicación podría estar exportando un vídeo mientras reproduce una previsualización, de modo que tendría más sentido separar ambos contextos:
com.example.video-editor.export
State: Encoding
com.example.video-editor.playback
State: Playing
Esta separación evita tener que inventar estados combinados como encodingAndPlaying, encodingAndPaused o todas las combinaciones posibles entre subsistemas independientes.
Los reporters no se crean directamente. StateReporter proporciona una instancia única para cada dominio mediante reporter(for:stableMetadata:volatileMetadata:). Apple recomienda conservar esa instancia en un objeto de larga duración, como un modelo, un controlador a nivel de Scene o una infraestructura de diagnóstico que siga instanciada mientras el dominio pueda estar activo.
import StateReporting
@ReportableMetadata
struct ExportConfiguration {
let codec: String
let preset: String
let isHDR: Bool
}
@ReportableMetadata
struct ExportProgress {
let progress: Double
let estimatedSecondsRemaining: Double
let droppedFrames: Int
}
let reporter = StateReporter.reporter(
for: "com.example.video-editor.export",
stableMetadata: ExportConfiguration.self,
volatileMetadata: ExportProgress.self
)
Hay aquí un detalle importante que conviene tratar como parte del contrato de la API: un mismo dominio siempre debe utilizar los mismos tipos de metadatos. StateReporter devuelve el mismo objeto para una cadena de dominio determinada y solicitar posteriormente ese dominio con otros argumentos genéricos provoca un error fatal en tiempo de ejecución. Por eso los nombres de dominio y sus tipos asociados deberían definirse de forma centralizada y estable.
StateReporting distingue entre metadatos estables y metadatos volátiles, y esta separación es probablemente la parte más importante de todo el diseño. Los metadatos estables forman parte de la identidad del estado y se utilizan al segmentar los datos. Si cambian, se considera que existe una nueva transición aunque la etiqueta sea la misma.
En el ejemplo anterior, codec, preset e isHDR describen una configuración que puede tener sentido al comparar rendimiento. Exportar con HEVC y HDR puede comportarse de manera distinta que exportar H.264 en SDR. Son valores adecuados para diferenciar grupos de métricas porque deberían tener una variación relativamente baja y estable.
Los metadatos volátiles describen lo que está ocurriendo dentro del estado actual. El progreso de una exportación, el tiempo restante o el número de fotogramas descartados pueden cambiar continuamente, pero no queremos que cada nuevo valor cree otro estado distinto. Para eso existe reportVolatileMetadataUpdate(_:).
let configuration = ExportConfiguration(
codec: "HEVC",
preset: "4K",
isHDR: true
)
reporter.reportTransition(
to: "Encoding",
stableMetadata: configuration,
volatileMetadata: ExportProgress(
progress: 0,
estimatedSecondsRemaining: 48,
droppedFrames: 0
)
)
Cuando avanza la codificación, la etiqueta sigue siendo Encoding y la configuración estable tampoco ha cambiado. Solo necesitamos actualizar el contexto volátil:
reporter.reportVolatileMetadataUpdate(
ExportProgress(
progress: 0.62,
estimatedSecondsRemaining: 19,
droppedFrames: 2
)
)
En cambio, cuando la aplicación termina de codificar y empieza a guardar el resultado, sí existe una transición real:
reporter.reportTransition(
to: "Saving",
stableMetadata: configuration,
volatileMetadata: ExportProgress(
progress: 1,
estimatedSecondsRemaining: 2,
droppedFrames: 2
)
)
La diferencia es importante porque MetricKit agrega métricas utilizando el estado y sus metadatos estables. Si utilizásemos etiquetas como Encoding-10, Encoding-11, Encoding-12 o incluyésemos identificadores prácticamente únicos en los metadatos estables, terminaríamos fragmentando los datos en grupos demasiado pequeños para ser útiles.
Una buena regla práctica es considerar los metadatos estables como variantes del estado y los volátiles como su situación momentánea. El tipo de codec o el modo de exportación pueden explicar diferencias estructurales de rendimiento; el porcentaje completado o el tiempo restante simplemente ayudan a entender qué estaba ocurriendo dentro de esa fase.
Para definir estos metadatos Apple incluye la macro @ReportableMetadata, que genera automáticamente la conformidad con el protocolo ReportableMetadata y construye el diccionario que utiliza el framework. La macro reconoce directamente tipos escalares como String, Int, Double, Date y Bool.
@ReportableMetadata
struct RenderingContext {
@ReportableMetadataKey("preset")
let exportPreset: String
let isHDR: Bool
@ReportableMetadataIgnored
let internalSessionID: String
}
@ReportableMetadataKey permite mantener una clave estable aunque cambie el nombre de una propiedad en el código, mientras que @ReportableMetadataIgnored excluye completamente un valor. Esto último resulta especialmente útil para datos internos, valores derivados o información que no debería terminar formando parte de un informe de diagnóstico.
Tampoco conviene tratar los metadatos como si fueran meros datos arbitrarios para analítica. Identificadores de usuario, UUID de sesiones, nombres de documentos o cualquier dato que pueda tomar prácticamente infinitos valores son malos candidatos, especialmente como metadatos estables. Además de las implicaciones de privacidad, destruirían la capacidad de obtener agregados comparables.
Otro límite fundamental es la frecuencia. Apple aplica rate limiting a StateReporting y recomienda reportar cambios en una escala similar a la interacción humana: navegar a otra pantalla, iniciar una actividad o pasar a otra fase significativa dentro de un proceso. No está pensado para recibir actualizaciones en cada frame ni dentro de un bucle de procesamiento intensivo. Si se llama a la API con demasiada frecuencia, el sistema puede descartar actualizaciones.
Por eso una exportación puede actualizar su barra de progreso varias veces por segundo sin trasladar cada cambio a StateReporting. La aplicación puede muestrear ese progreso y enviar únicamente puntos suficientemente representativos:
func reportProgressIfNeeded(_ progress: Double) {
let bucket = Int(progress * 10)
guard bucket != lastReportedBucket else {
return
}
lastReportedBucket = bucket
reporter.reportVolatileMetadataUpdate(
ExportProgress(
progress: progress,
estimatedSecondsRemaining: estimateRemainingTime(),
droppedFrames: droppedFrames
)
)
}
La relación con MetricKit requiere un paso adicional: registrar los dominios que queremos utilizar para agregación cuando creamos MetricManager. Sin esa configuración seguiremos recibiendo las métricas habituales por intervalos, pero stateEntries no contendrá la segmentación basada en estados.
import MetricKit
let metricManager = MetricManager(
enabledStateReportingDomains: [
"com.example.video-editor.export"
]
)
Task {
for await report in metricManager.metricReports {
for entry in report.stateEntries {
processStateEntry(entry)
}
}
}
Hay un matiz que evita una interpretación demasiado optimista de la nueva API: no todas las métricas de MetricKit aparecen segmentadas por estado. Los StateEntry incluyen un subconjunto de métricas como tiempo de cuelgue, tiempo de interfaz lenta, terminaciones de la aplicación, intervalos de signposts y determinadas métricas de tiempo de ejecución. Métricas como CPU, memoria, red, E/S de disco, GPU o lanzamiento permanecen en intervalEntries.
Aun así, esta segmentación puede cambiar por completo un análisis. Un promedio global podría indicar que una aplicación tiene una tasa moderada de bloqueos, mientras que los datos por estado podrían revelar que casi todo el problema se concentra durante Encoding con un preset concreto. La métrica deja entonces de ser un simple indicador de “salud” y se convierte en una pista accionable para localizar el trabajo que necesita optimización.
MetricReport es además Codable, por lo que Apple facilita enviar los informes completos a nuestra propia infraestructura. Si queremos que la representación JSON quede organizada por dominios de StateReporting, podemos seleccionar explícitamente ese formato al codificar:
let encoder = JSONEncoder()
encoder.userInfo[MetricReport.encodingFormatKey] =
MetricReport.EncodingFormat.byStateReportingDomain
let data = try encoder.encode(report)
Esto permite mantener agrupados los estados de cada dominio al procesar los informes en un servidor, algo especialmente interesante cuando una aplicación utiliza varios subsistemas instrumentados de forma independiente.
Durante el desarrollo, Instruments ofrece una forma mucho más inmediata de comprobar que el modelado de estados tiene sentido. Las transiciones de StateReporting aparecen dentro del instrumento Points of Interest, con una pista por dominio. Al seleccionar un estado pueden inspeccionarse sus metadatos estables y volátiles y compararlos con actividad de CPU, bloqueos u otras señales recogidas durante la sesión.
Esta validación previa es importante porque el principal riesgo de StateReporting no está en su API, que es bastante pequeña, sino en diseñar estados poco útiles. Si reportamos cada evento de interfaz como una transición terminaremos con demasiado ruido; si usamos estados excesivamente genéricos no obtendremos información que ayude a aislar regresiones.
Cuando un dominio deja de tener un estado significativo, también podemos limpiarlo explícitamente pasando nil como etiqueta:
reporter.reportTransition(to: nil)
Mantener indefinidamente un estado como Completed puede hacer que un diagnóstico posterior parezca asociado a una operación que ya había terminado. Limpiar el dominio cuando la actividad concluye mantiene el contexto más preciso.
Las extensiones de una aplicación también pueden participar en este modelo. Sus estados pueden aparecer en los informes de diagnóstico que recibe la aplicación principal, aunque la propia extensión debe registrar sus dominios con su instancia de MetricManager y emitir sus transiciones. Esto permite correlacionar problemas de rendimiento con trabajo realizado en extensiones sin construir un sistema paralelo de contexto.
Tanto StateReporting como las nuevas APIs de MetricKit forman parte actualmente del software beta de iOS 27 y macOS 27, por lo que Apple advierte de que la API todavía puede cambiar antes de las versiones finales. Conviene encapsular esta instrumentación en una capa propia y evitar que detalles del framework se propaguen por toda la interfaz o la lógica de negocio.
StateReporting cubre un hueco muy concreto en la observabilidad de aplicaciones Apple: convertir una métrica de rendimiento en una métrica acompañada del contexto funcional que la produjo. Bien utilizado, permite pasar de «la interfaz se vuelve lenta» a «la lentitud se concentra durante esta fase, con esta configuración», que es precisamente el tipo de información que reduce el tiempo necesario para encontrar una regresión real.
La clave no está en reportar más datos, sino en escoger pocos dominios, pocos estados y metadatos con una variación controlada que sigan teniendo significado cuando toque analizar miles de informes reales de MetricKit meses después.