wandres.dev
ANALÍTICA Y CRASHES · saber qué pasa

MetricKit: las métricas que Apple ya está midiendo por ti

El sistema operativo mide de forma continua el arranque, los saltos de fotograma, el consumo energético, la escritura en disco y la memoria de tu aplicación en dispositivos reales, y te entrega ese material una vez al día sin coste de rendimiento ni de privacidad. Esta lección explica qué contiene exactamente esa entrega, cómo suscribirse y persistirla sin perder cargas, por qué los histogramas y los percentiles cuentan una historia distinta de la de una media, y cómo convertir el paquete diario en una señal accionable.

⏱ 19 min

Existe una asimetría curiosa en el ecosistema de Apple: el sistema operativo instrumenta tu aplicación con un detalle que ningún equipo se podría permitir replicar, mide el tiempo hasta el primer fotograma, cuenta los saltos de la interfaz, atribuye energía por subsistema y registra cada escritura en disco, y luego deja ese material esperando a que alguien lo recoja. La mayoría de las aplicaciones no lo recogen nunca. La razón es casi siempre la misma: la entrega ocurre una vez al día, en un momento que no controlas, con un modelo de datos que exige entenderse antes de usarse, y en un formato agregado que resiste la tentación de mirarlo como si fuera un registro de eventos. Esta lección trata de recorrer esa distancia, porque es la telemetría de campo más barata y más honesta a la que vas a tener acceso: sin SDK de terceros, sin coste de batería añadido y sin una sola decisión de privacidad que declarar.

🎯 Al terminar esta lección sabrás
  • Enumerar las familias de métricas que entrega MetricKit y saber qué pregunta de producto responde cada una.
  • Implementar la suscripción, recibir la carga diaria y persistirla con garantías frente a cierres del proceso.
  • Interpretar histogramas, percentiles y unidades tipadas sin caer en el error de razonar con medias.
  • Distinguir las métricas agregadas de los diagnósticos de bloqueo, fallo y disco, y saber cuándo sirve cada uno.

El catálogo: qué mide el sistema sin que se lo pidas

La carga diaria llega organizada en grupos temáticos, y conviene conocerlos por lo que responden más que por su nombre técnico. El grupo de tiempos de lanzamiento distingue el arranque en frío, el arranque desde suspensión y la reanudación, que son tres experiencias distintas y con causas distintas; mezclarlas en un solo número es la forma más rápida de no entender nada. El grupo de capacidad de respuesta mide el tiempo que la aplicación tarda en volver a atender al hilo principal, y el de animación cuenta el tiempo de desplazamiento con saltos de fotograma, esa métrica que Apple llama proporción de trabajo con hitches y que se correlaciona con la percepción de fluidez mejor que cualquier medida de fotogramas por segundo.

Después vienen los grupos de recursos. El de memoria registra el pico y la huella media en suspensión, que es la que decide si el sistema te elige como víctima cuando hay presión. El de energía atribuye consumo por subsistema —pantalla, red, ubicación, procesamiento gráfico, cómputo— y permite responder a la pregunta de qué parte de tu aplicación está gastando la batería, que sin esa atribución es indecidible. El de acceso a disco cuenta bytes escritos con un desglose que sorprende a casi todos los equipos la primera vez, porque las bases de datos locales y los registros de depuración olvidados suelen aparecer con cifras absurdas. Y el de red separa carga y descarga por tipo de conexión, lo que convierte una queja vaga sobre consumo de datos en una cifra defendible.

🚀

Arranque

Tres modos distintos con causas distintas. El frío depende de bibliotecas dinámicas y de trabajo en el primer ciclo; la reanudación depende de lo que hagas al volver a primer plano.

📈

Hitches y respuesta

Proporción de tiempo de desplazamiento con salto de fotograma, y tiempo hasta recuperar el hilo principal. Es la traducción numérica de la sensación de lentitud.

🔋

Energía y disco

Consumo atribuido por subsistema y bytes escritos acumulados. Las dos métricas que más veces revelan trabajo que nadie recordaba haber dejado activo.

Hay dos propiedades del conjunto que determinan cómo se puede usar. La primera es que todo llega agregado por periodo y por versión de la aplicación, nunca por sesión ni por persona usuaria, lo que hace imposible responder preguntas individuales y a cambio elimina cualquier problema de identificación. La segunda es que la carga corresponde a las últimas veinticuatro horas y se entrega una sola vez al día, típicamente cuando la aplicación pasa a primer plano tras la ventana de recogida, de modo que la latencia entre un despliegue y su lectura de métricas es de al menos un día y en la práctica de dos o tres. Ese retardo condiciona cualquier proceso de decisión que quieras construir encima.

ℹ️
Extensiones y aplicaciones distintas

Las cargas incluyen métricas segmentadas por identificador de la aplicación y de sus extensiones, de forma que un widget o una extensión de notificación aparecen con su propia contabilidad. Es la única manera práctica de descubrir que una extensión que se ejecuta cien veces al día está consumiendo más energía que la aplicación entera, un caso más habitual de lo que parece porque las extensiones se prueban poco y se miden menos.

Suscribirse y no perder la carga

El mecanismo de recepción es deliberadamente simple y tiene una sola trampa. Se implementa el protocolo de suscriptor, se registra en el gestor compartido durante el arranque y el sistema invoca el método de entrega cuando le conviene. La trampa es que ese método se llama en un momento arbitrario, con la aplicación posiblemente recién despertada, y que si el proceso muere antes de que hayas persistido o enviado la carga, esa carga se pierde para siempre: no hay reintento ni cola del sistema. La regla operativa es escribir a disco de forma síncrona y sin transformaciones antes de hacer cualquier otra cosa.

import MetricKit

final class RecolectorDeMetricas: NSObject, MXMetricManagerSubscriber {
    static let compartido = RecolectorDeMetricas()

    func activar() {
        MXMetricManager.shared.add(self)
    }

    func didReceive(_ payloads: [MXMetricPayload]) {
        for carga in payloads {
            // Primero persistir en crudo, despues ya se transformara
            almacen.guardar(carga.jsonRepresentation(),
                            nombre: "metricas-\(carga.timeStampEnd.timeIntervalSince1970)")
        }
        colaDeEnvio.intentarVaciar()
    }

    func didReceive(_ payloads: [MXDiagnosticPayload]) {
        for diagnostico in payloads {
            almacen.guardar(diagnostico.jsonRepresentation(),
                            nombre: "diagnostico-\(diagnostico.timeStampEnd.timeIntervalSince1970)")
        }
    }
}

La representación en JSON que ofrecen ambas cargas es el formato que conviene almacenar y enviar, por tres razones que se refuerzan. Es estable entre versiones del sistema, es autodescriptiva y evita que tu código de transformación se convierta en un cuello de botella donde se pierden campos nuevos que Apple añade cada año. La transformación a tu modelo interno se hace en el servidor, donde se puede rehacer sobre datos históricos si descubres que estabas leyendo mal una unidad.

El envío merece la misma cautela que la persistencia. Una carga puede pesar cientos de kilobytes, llega en un momento que no eliges y con frecuencia coincide con una aplicación recién abierta que tiene cosas más urgentes que hacer con la red. La estrategia que funciona es acumular en disco, enviar en segundo plano con una tarea de transferencia que sobreviva al cierre de la aplicación y borrar solo tras confirmación del servidor. Y conviene medir el propio recolector: si tu telemetría de rendimiento empeora el arranque, has convertido el termómetro en parte de la fiebre.

// Simulador de carga en depuracion: no esperes veinticuatro horas para probar
#if DEBUG
MXMetricManager.shared.add(RecolectorDeMetricas.compartido)
// En Xcode: menu Debug, opcion Simulate MetricKit Payloads
#endif
💡
Marcas propias dentro del marco de Apple

Además de lo que mide el sistema, puedes instrumentar intervalos propios con las firmas de registro de MetricKit, y esos intervalos llegan agregados en la misma carga diaria con el mismo tratamiento estadístico. Es la vía correcta para medir cosas específicas de tu dominio —el tiempo de una sincronización, el de una exportación— sin montar una infraestructura paralela ni pagar el coste de enviar un evento por ejecución.

Histogramas, percentiles y la mentira de la media

Buena parte de las métricas no llegan como un número sino como un histograma: una lista de cubos con su rango y el recuento de observaciones que cayeron dentro. Esa forma no es un capricho de diseño, es la consecuencia de que las distribuciones de latencia en software son casi siempre asimétricas y con cola larga, y una media sobre una distribución así carece de significado operativo. Si el arranque tarda medio segundo para el noventa por ciento de los casos y once segundos para el uno por ciento restante, la media dirá seiscientos milisegundos y describirá una experiencia que no vive nadie.

La lectura correcta se hace por percentiles, y la elección del percentil es una decisión de producto, no técnica. El percentil cincuenta describe la experiencia típica y sirve para comunicar; el noventa describe la experiencia de la cola razonable y es el que conviene poner en un objetivo de servicio; el noventa y cinco o el noventa y nueve describen la experiencia de los dispositivos viejos, las conexiones malas y los estados degradados, que es donde se generan las desinstalaciones. Un equipo que solo mira medianas cree que su aplicación va bien mientras pierde silenciosamente a quienes peor lo pasan.

flowchart TD
a[Sistema mide en dispositivo] --> b[Carga diaria agregada por version]
b --> c[Persistencia inmediata en disco]
c --> d[Envio en segundo plano]
d --> e[Histogramas por percentil en el servidor]
e --> f[Comparacion entre versiones]
f --> g[Alerta si el percentil noventa empeora]

Las unidades exigen una advertencia práctica. MetricKit devuelve valores tipados con unidad explícita —milisegundos, kilobytes, milivatios hora— y la tentación de extraer el valor bruto y guardarlo como número desnudo produce errores de tres órdenes de magnitud que sobreviven meses porque nadie mira un gráfico esperando que esté mal por mil. Conviene convertir siempre de forma explícita a una unidad canónica en el momento de la ingesta y dejar constancia de cuál es.

El uso final del material es comparativo. Un percentil noventa de arranque en frío de mil doscientos milisegundos no significa nada en abstracto; significa mucho si la versión anterior estaba en ochocientos. Por eso la dimensión más valiosa de la carga es la versión de la aplicación, y por eso la práctica que más rendimiento da es fijar dos o tres indicadores, mirarlos por versión en cada publicación y tratar un empeoramiento sostenido del percentil noventa como un defecto de regresión con la misma seriedad que un fallo funcional.

Diagnósticos: la otra mitad del paquete

Junto a las métricas agregadas, el sistema entrega diagnósticos individuales con trazas de pila, y esa parte se lee con la técnica de la lección anterior. Incluye fallos, bloqueos prolongados del hilo principal, terminaciones por exceso de recursos, escrituras masivas en disco y, en versiones recientes, informes de arranque anómalamente lento. La diferencia esencial con las métricas es que aquí sí hay una traza que apunta a código, en forma de árbol de llamadas que puede contener varios hilos y varias ramas.

Los diagnósticos de bloqueo son, con diferencia, los más infravalorados. Un bloqueo de tres segundos no genera informe de fallo, no aparece en ninguna tienda y sin embargo es la causa más frecuente de que alguien describa una aplicación como lenta. El árbol de llamadas del diagnóstico muestra exactamente qué estaba ejecutando el hilo principal durante la congelación, y en la mayoría de los casos la respuesta pertenece a un catálogo corto: entrada y salida síncrona, decodificación de imágenes, una consulta a base de datos sin índice, una espera sobre un semáforo o el descifrado de un archivo grande.

// Recorrer el arbol y quedarse con la rama del hilo culpable
func resumir(_ arbol: MXCallStackTree) -> Data {
    // La representacion en JSON conserva la estructura completa
    // y se simboliza en el servidor con el dSYM de esa version
    arbol.jsonRepresentation()
}

Las trazas llegan sin símbolos por el mismo motivo que las de un informe de fallo, así que el circuito de dSYM archivados por versión que montaste en la lección anterior sirve tal cual aquí. Merece la pena insistir en que el diagnóstico de escritura en disco es el que más veces produce un hallazgo inmediato y barato: aparece una ruta de código escribiendo cientos de megabytes al día, casi siempre un registro de depuración que quedó activo en publicación o una caché sin política de expiración, y el arreglo cuesta diez minutos.

Medir sin identificar es una restricción, y la restricción es lo que hace fiable la medida

Hay una lectura profunda de MetricKit que se pierde si uno lo trata como una analítica pobre. Todo el diseño está construido sobre una renuncia deliberada: no vas a saber quién, no vas a saber cuándo exactamente, no vas a poder seguir a una persona a través del tiempo. A cambio recibes una medición hecha por el propio sistema operativo, con acceso a contadores que ninguna biblioteca de espacio de usuario podría leer, sin coste de batería porque la instrumentación ya estaba corriendo, y sin una sola implicación de privacidad que negociar. Esa renuncia no es una limitación que Apple no supo evitar: es exactamente lo que convierte el dato en fiable. Un SDK de terceros que mide arranque desde dentro del proceso altera el arranque que mide, compite por recursos con lo que observa y necesita un identificador para agregar, lo que arrastra consentimiento, retención y declaración. El observador del sistema no participa en la carrera. Y hay una consecuencia que trasciende lo técnico y toca la epistemología del oficio: la mayoría de las preguntas que un equipo cree necesitar responder a nivel individual —quién sufrió el bloqueo, qué hacía esa persona— se responden igual de bien con distribuciones, porque la decisión que se va a tomar es siempre sobre poblaciones y no sobre individuos. Se optimiza el percentil noventa, no la sesión de nadie en concreto. Cuando un equipo pide granularidad individual para un problema de rendimiento, casi siempre está pidiendo el instrumento que le resulta familiar en lugar del que su decisión requiere, y el precio de esa comodidad se paga en un modelo de datos que hay que justificar ante quien confía en tu aplicación.

📝
Lo esencial

MetricKit entrega una carga diaria agregada por versión con arranque, respuesta, hitches, memoria, energía, disco y red, más diagnósticos con trazas de fallo, bloqueo, recursos y escritura. Se suscribe con el gestor compartido, se persiste en crudo y de inmediato porque no hay reintento, y se envía en segundo plano. Los histogramas se leen por percentiles y nunca por media, las unidades se normalizan en la ingesta, y el valor real aparece al comparar versiones consecutivas. Todo ello sin identificar a nadie.

⚔️ Montar el circuito completo
  1. Implementa el suscriptor, actívalo en el arranque y usa la simulación de cargas de Xcode para recibir una entrega sin esperar un día.
  2. Persiste la representación en JSON sin transformarla y comprueba que sobrevive a un cierre forzado del proceso justo después de la entrega.
  3. Calcula el percentil cincuenta y el noventa del arranque en frío a partir del histograma y compáralos con la media, anotando la diferencia.
  4. Instrumenta con firmas propias el intervalo de una operación relevante de tu dominio y verifica que aparece agregada en la siguiente carga.
  5. Provoca un bloqueo deliberado del hilo principal de más de tres segundos, recoge el diagnóstico y simboliza su árbol de llamadas con el dSYM de esa compilación.