wandres.dev
RENDIMIENTO DE APPS · arranque, memoria, batería

Presupuestos y regresiones: gobernar el rendimiento

El rendimiento solo se mantiene si alguien lo defiende con números. Esta lección convierte las intuiciones de las lecciones anteriores en presupuestos explícitos por percentil, muestra cómo escribir pruebas de rendimiento que no mientan pese al ruido del hardware, conecta el laboratorio con los datos de campo de `MetricKit` y define el protocolo para detectar, atribuir y revertir una regresión antes de que llegue a todos.

⏱ 19 min

Toda propiedad transversal del software comparte el mismo destino cuando no se le asigna un guardián con autoridad y un número que defender: se degrada de forma monótona, sin que ninguna decisión individual sea culpable. El rendimiento es el caso arquetípico. Nadie introduce deliberadamente una regresión; lo que ocurre es que cada función nueva añade una inicialización razonable, una cache razonable, una consulta razonable y unos milisegundos razonables, y el agregado de treinta decisiones razonables es una aplicación que arranca al doble de lento que hace un año sin que exista un commit al que señalar. La única defensa conocida contra esa deriva no es la disciplina individual, que se agota, ni el perfilado ocasional, que llega tarde: es convertir el rendimiento en un contrato con umbrales explícitos, medición automática y una consecuencia definida cuando se incumple. Este es el paso que separa a los equipos que optimizan de los que gobiernan.

🎯 Al terminar esta lección sabrás
  • Formular presupuestos de rendimiento por percentil, no por media, y justificar el umbral.
  • Escribir pruebas de rendimiento estables pese al ruido del hardware y de la integración continua.
  • Combinar la señal del laboratorio con los datos de campo de MetricKit.
  • Definir el protocolo de detección, atribución y respuesta ante una regresión.

De la intuición al presupuesto

Un presupuesto de rendimiento es una afirmación falsable sobre el comportamiento del producto, y para serlo necesita cuatro componentes: la métrica, el percentil, el umbral y el contexto de medición. Faltando cualquiera de ellos, el enunciado deja de ser verificable y se convierte en una aspiración. Decir que la app debe arrancar rápido no compromete a nadie; decir que el tiempo hasta el primer fotograma en arranque en frío debe mantenerse por debajo de setecientos milisegundos en el percentil noventa de la base instalada es una afirmación que un sistema automático puede evaluar y que puede fallar.

La elección del percentil no es un detalle estadístico sino la decisión más importante del ejercicio. La media es sistemáticamente engañosa en distribuciones de latencia porque son asimétricas con cola larga: unos pocos dispositivos antiguos, térmicamente limitados o con la memoria bajo presión producen valores que arrastran la experiencia real de un porcentaje nada trivial de usuarios y que la media diluye. Trabajar con el percentil noventa o el noventa y cinco desplaza la atención a esa cola, que es exactamente donde vive el usuario que se plantea abandonar el producto.

🚀

Arranque en frío

Tiempo hasta el primer fotograma y, por separado, hasta que la pantalla tiene datos accionables. Percentil noventa, hardware más modesto soportado.

🌊

Fluidez

Razón de tiempo de tirón durante el desplazamiento, con umbral en cinco milisegundos por segundo y vigilancia por pantalla, no global.

🧠

Huella de memoria

Pico por pantalla crítica en el dispositivo más modesto, con margen suficiente respecto al límite del proceso para absorber variabilidad.

🔋

Energía

Tiempo de CPU acumulado, bytes en celular y tiempo de ubicación de alta precisión por sesión, agregados por versión.

El cuarto componente, el contexto de medición, es el que más se olvida y el que invalida más comparaciones. Un tiempo de arranque no significa nada sin especificar el modelo de dispositivo, la versión del sistema, si la compilación es de publicación, si el arranque es en frío y si el dispositivo estaba térmicamente estable. Dos equipos que discuten sobre si la app ha mejorado suelen estar comparando mediciones tomadas en contextos distintos, y esa discusión no se resuelve con más datos sino fijando el protocolo por escrito antes de tomarlos.

Fijar el umbral es donde más equipos se atascan, y la salida práctica es no intentar deducirlo de principios. El primer umbral se establece midiendo el estado actual y declarándolo techo: se toma el percentil noventa de la versión en producción, se le añade un margen del orden del diez por ciento y ese número se convierte en la línea que no se cruza. Ese presupuesto no afirma que el rendimiento actual sea bueno, afirma que no va a empeorar, que es una propiedad mucho más valiosa y mucho más defendible en una discusión de prioridades. Mejorarlo es un proyecto aparte, con su propio plan; impedir que se degrade es un invariante que puede automatizarse desde mañana.

Pruebas de rendimiento que no mienten

La objeción estándar contra automatizar el rendimiento es que las mediciones son ruidosas y las pruebas acaban fallando de forma intermitente hasta que alguien las desactiva. La objeción es correcta y tiene remedios conocidos. XCTest incorpora un marco de medición que ejecuta el bloque varias veces, descarta la primera iteración y calcula estadísticos sobre el resto, con métricas específicas para lanzamiento, memoria, tiempo de reloj y ciclos de CPU, además de la capacidad de medir cualquier intervalo delimitado por signposts.

import XCTest

final class PruebasDeRendimiento: XCTestCase {
    func testTiempoDeLanzamiento() {
        let opciones = XCTMeasureOptions()
        opciones.iterationCount = 10
        measure(metrics: [XCTApplicationLaunchMetric()], options: opciones) {
            XCUIApplication().launch()
        }
    }

    func testDesplazamientoDeLaLista() {
        let app = XCUIApplication()
        app.launch()
        let intervalo = XCTOSSignpostMetric.scrollDecelerationMetric
        measure(metrics: [intervalo]) {
            app.tables.firstMatch.swipeUp(velocity: .fast)
        }
    }
}

Cuatro condiciones separan una suite útil de una fuente de ruido. La primera es hardware dedicado y estable: medir en máquinas virtuales compartidas o en agentes con carga variable produce dispersiones que ninguna estadística arregla. La segunda es medir en dispositivo físico y siempre en el mismo modelo, porque comparar entre modelos no significa nada. La tercera es aceptar la variabilidad en el criterio: los umbrales relativos con banda de tolerancia, evaluados sobre la mediana de varias iteraciones, son mucho más estables que un valor absoluto. Y la cuarta, la más importante desde el punto de vista organizativo, es decidir con cuidado si la prueba bloquea o solo avisa.

⚠️
Una prueba de rendimiento que bloquea de forma intermitente se desactiva en dos semanas

Es una regularidad sociológica antes que técnica y conviene anticiparla. Si la barrera falla por ruido y no por regresión, el equipo aprende a reintentar hasta que pasa, y en cuanto ese hábito se instala la prueba ha dejado de aportar información aunque siga verde. El patrón que sobrevive es escalonado: publicar siempre la medición como comentario en la solicitud de cambios, bloquear únicamente en las dos o tres métricas más estables y con umbrales generosos, y llevar el resto a un panel de tendencia que se revisa por versión. Una tendencia visible es más eficaz que una barrera que nadie respeta.

Del laboratorio al campo

Ninguna suite de laboratorio reproduce la distribución real de tus usuarios, y esa limitación no es de grado sino de naturaleza. En el laboratorio no hay dispositivos con la memoria bajo presión de otras diez apps, ni estados térmicos degradados, ni redes de calidad variable, ni modelos de cuatro años con la batería envejecida y limitación de frecuencia activa. El laboratorio detecta regresiones antes de publicar; el campo mide la realidad. Se necesitan los dos y responden a preguntas distintas.

flowchart TD
A[Cambio propuesto] --> B[Medicion en integracion continua]
B --> C{Dentro del presupuesto}
C -- No --> D[Bloqueo o revision explicita]
C -- Si --> E[Publicacion escalonada]
E --> F[Metricas de campo por version]
F --> G{Regresion en percentil noventa}
G -- Si --> H[Detener despliegue y bisecar]
G -- No --> I[Ampliar despliegue y mover la linea base]

Ese ciclo tiene un elemento que muchos equipos omiten y que es el que convierte la vigilancia en una red de seguridad real: la publicación escalonada. Liberar primero a un porcentaje pequeño de la base instalada y esperar a tener datos suficientes antes de ampliar transforma una regresión que habría afectado a todos en un incidente contenido. Requiere que las métricas lleguen con la versión etiquetada y que exista un criterio previo de qué diferencia justifica detener el despliegue, decidido en frío y no en mitad del sobresalto.

MetricKit es la fuente canónica para esa comparación porque agrega por versión y por dispositivo sin coste de instrumentación adicional, aunque con una latencia de un día que conviene tener presente al planificar la duración de cada escalón. Al informe diario de métricas se suma el de diagnósticos, que entrega terminaciones por memoria, incumplimientos de tiempo de respuesta y colgados con sus pilas de llamadas, es decir, la evidencia con la que se atribuye una regresión a un cambio concreto.

import MetricKit

final class Vigilante: NSObject, MXMetricManagerSubscriber {
    func didReceive(_ payloads: [MXMetricPayload]) {
        for carga in payloads {
            let version = carga.latestApplicationVersion
            enviar(metricas: carga, etiquetadasCon: version)
        }
    }

    func didReceive(_ payloads: [MXDiagnosticPayload]) {
        for carga in payloads {
            carga.crashDiagnostics?.forEach(enviar)
            carga.hangDiagnostics?.forEach(enviar)   // respuesta bloqueada del hilo principal
        }
    }
}
Un presupuesto no es una medición: es una transferencia de la carga de la prueba

Merece la pena entender por qué los presupuestos funcionan cuando el perfilado periódico fracasa, porque la explicación no está en la técnica sino en la estructura de la decisión. Sin presupuesto, el rendimiento se discute caso por caso y siempre en el mismo formato: alguien propone una función que cuesta ochenta milisegundos de arranque y el equipo debate si compensa. Ese debate tiene un resultado predecible, porque los términos no son simétricos. El beneficio de la función es concreto, atribuible, defendido por una persona con incentivo para defenderlo y visible en la próxima demostración; el coste es difuso, compartido, se manifiesta dentro de seis meses y no tiene a nadie que lo represente en la sala. Un sistema que exige justificar cada degradación individualmente en esas condiciones siempre acaba degradándose, no porque sus miembros sean descuidados sino porque la asimetría está en el procedimiento. Un presupuesto explícito invierte la carga de la prueba y ahí está toda su potencia: deja de preguntarse si este cambio concreto vale sus ochenta milisegundos y pasa a preguntarse quién los cede. Si el techo de arranque son setecientos milisegundos y la app está en seiscientos ochenta, la función nueva no discute con una abstracción sino con un recurso finito que otro está usando, y la conversación se vuelve la que debería haber sido desde el principio: qué de lo que ya existe vale menos que esto. Es exactamente el mecanismo por el que un presupuesto económico convierte deseos en prioridades, y funciona por la misma razón: no porque nadie quiera gastar, sino porque hace visible que gastar aquí es no gastar allá. El número concreto importa menos de lo que parece; lo que transforma el comportamiento del equipo es la existencia de un límite que obliga a elegir.

Detectar, atribuir y responder

Cuando la vigilancia señala una regresión, el reflejo habitual es abrir el perfilador y buscar código lento. Es el orden equivocado y desperdicia días. La secuencia eficiente empieza por acotar: comparar la distribución de la versión afectada con la anterior, segmentar por modelo de dispositivo y por versión del sistema, y comprobar si la degradación es uniforme o se concentra en un subconjunto. Una regresión que solo aparece en dispositivos con poca memoria apunta a huella; una que solo aparece en conexiones lentas apunta a una dependencia de red en el camino crítico; una uniforme apunta a trabajo añadido en el arranque o en el hilo principal.

El segundo paso es atribuir mediante bisección sobre el conjunto de cambios de la versión, con la métrica automatizada como criterio. Si las mediciones de laboratorio están razonablemente correlacionadas con las de campo, la bisección es mecánica y termina en pocas iteraciones; si no lo están, la ausencia de correlación es en sí misma un hallazgo importante que indica que la suite mide algo distinto de lo que sufre el usuario y que hay que corregirla antes de seguir.

Solo en el tercer paso entra el perfilador, y entonces entra con una hipótesis concreta que confirmar o refutar, que es la única forma de usarlo con eficacia. La respuesta, además, no tiene por qué ser optimizar: revertir el cambio, ponerlo tras una bandera de funcionalidad, diferirlo fuera del camino crítico o aceptarlo de forma explícita moviendo el presupuesto son decisiones igualmente legítimas. Lo que no es legítimo es la cuarta opción tácita, dejar el presupuesto incumplido sin decisión, porque en cuanto un umbral se incumple sin consecuencia deja de existir como umbral y todo el sistema pierde su valor.

Conviene por último anticipar dos modos de fallo que arruinan programas de rendimiento por lo demás bien planteados. El primero es la proliferación de métricas. Un panel con treinta indicadores no se mira, y los que sí se miran acaban siendo los que suben, no los que importan; cuatro o cinco presupuestos vigilados de verdad rinden más que veinte declarados. El segundo es la desconexión silenciosa entre lo que se mide y lo que el usuario sufre: una suite que vigila el arranque en un dispositivo moderno mientras la base instalada envejece deja de correlacionar sin que nadie lo note, y el primer aviso llega en forma de reseñas. La contramedida para ambos es la misma y es de proceso, no de herramienta: revisar la lista de presupuestos una vez por trimestre, comprobar que cada uno sigue explicando variación observada en campo y retirar sin sentimentalismo el que ya no lo haga.

📝
Lo esencial

Un presupuesto necesita métrica, percentil, umbral y contexto para ser falsable, y el primer umbral se fija midiendo el estado actual y declarándolo techo. Las pruebas de rendimiento exigen hardware estable, dispositivo fijo, umbrales relativos y una decisión meditada sobre qué bloquea y qué solo avisa. El laboratorio detecta antes de publicar y el campo mide la realidad: hacen falta los dos, unidos por la publicación escalonada. Y ante una regresión, el orden es acotar, bisecar y solo entonces perfilar, con una decisión explícita al final.

⚔️ Instaurar el contrato de rendimiento
  1. Redacta cuatro presupuestos con métrica, percentil, umbral y contexto de medición, usando el estado actual de producción como techo.
  2. Escribe pruebas de rendimiento para las dos métricas más estables y ejecútalas veinte veces seguidas en el mismo dispositivo para cuantificar el ruido antes de fijar tolerancias.
  3. Publica las mediciones como comentario automático en cada solicitud de cambios y decide de forma explícita cuáles bloquean y cuáles solo informan.
  4. Etiqueta las métricas de MetricKit por versión y construye el panel de comparación entre las dos últimas.
  5. Define por escrito, en frío, el criterio que detendrá un despliegue escalonado y el protocolo de bisección que aplicaréis cuando ocurra.