Macrobenchmark: medir arranque y scroll sin engañarse
Una medición de rendimiento que no es reproducible no es una medición, es una anécdota. `Macrobenchmark` existe para convertir el arranque y el desplazamiento en números estables, obtenidos sobre un artefacto equivalente al de publicación, con el proceso realmente frío, con el modo de compilación bajo control y con suficientes iteraciones para que la variabilidad del dispositivo quede acotada. Esta lección distingue la medición macroscópica de la microscópica, monta el módulo y el tipo de construcción necesarios, escribe las dos pruebas que de verdad importan, explica cómo se leen las métricas de desbordamiento de fotograma y cierra con la parte que casi nadie hace bien: detectar regresiones en integración continua sin ahogarse en falsos positivos.
Existe una conversación que se repite en todos los equipos y que nunca termina bien. Alguien dice que la aplicación va más lenta desde la última versión. Otro responde que en su dispositivo va igual. Un tercero abre la aplicación, la cierra, la vuelve a abrir y declara que va perfectamente. Nadie miente y nadie tiene razón, porque los tres están comparando percepciones obtenidas con procesos calientes, dispositivos distintos, estados térmicos distintos y modos de compilación distintos. La discusión no se resuelve con más opiniones sino con un procedimiento que produzca el mismo número dos veces seguidas en las mismas condiciones. Eso es exactamente lo que Macrobenchmark aporta, y su valor real no está en la sofisticación de sus métricas sino en la disciplina que impone al acto de medir.
- Distinguir la medición macroscópica de la microscópica y saber qué pregunta responde cada una.
- Montar el módulo de pruebas y el tipo de construcción necesarios para medir sobre un artefacto realista.
- Escribir mediciones de arranque y de desplazamiento controlando modo de inicio y modo de compilación.
- Integrar la medición en el proceso automático de forma que detecte regresiones sin producir falsos positivos.
Dos preguntas distintas, dos herramientas distintas
La medición microscópica responde a cuánto cuesta una función determinada ejecutada muchas veces en aislamiento. Sirve para comparar dos implementaciones de un algoritmo, para verificar que una serialización mejoró o para acotar el coste de una operación criptográfica. Corre en el mismo proceso que la aplicación, calienta la ruta antes de medir y devuelve un tiempo por invocación con una precisión notable.
La medición macroscópica responde a otra cosa: cuánto tarda el usuario en ver la primera pantalla, o cuántos fotogramas se pierden al desplazar una lista. No mide funciones sino recorridos, y por eso corre desde un proceso separado que dirige la aplicación real mediante automatización de la interfaz, sobre un artefacto equivalente al de publicación, con la posibilidad de matar el proceso entre iteraciones y de fijar el modo de compilación. Ambas son necesarias, pero confundirlas produce conclusiones absurdas: una función que mejora un cuarenta por ciento en la medición microscópica puede no mover ni un milisegundo el arranque, porque nunca estuvo en el camino crítico.
Medición microscópica
Una función en aislamiento, muchas repeticiones, ruta caliente. Responde a qué implementación es más rápida.
Medición macroscópica
Un recorrido completo sobre la aplicación real, dirigida desde otro proceso. Responde a qué percibe el usuario.
Modo de inicio
Frío, templado o caliente. Determina qué se destruye entre iteraciones y por tanto qué se está midiendo.
Modo de compilación
Sin perfil, parcial con perfil o total. Sin fijarlo, el número depende de cuánto se usó el dispositivo antes.
Hay un requisito que conviene aceptar pronto: el dispositivo físico. Los emuladores comparten los recursos del anfitrión, no reproducen el comportamiento térmico ni el escalado de frecuencia, y producen cifras cuya varianza supera con holgura la magnitud de las regresiones que interesa detectar. Sirven para comprobar que la prueba corre; no sirven para decidir si algo empeoró.
Montar el andamiaje: módulo, tipo de construcción y condiciones
La medición macroscópica vive en un módulo de pruebas independiente que apunta al módulo de la aplicación. Ese módulo no produce una aplicación instalable sino un paquete de pruebas que instrumenta al otro. En paralelo hace falta un tipo de construcción específico, derivado del de publicación, que mantenga la minificación y las optimizaciones pero permita instrumentar el proceso, y que use la firma de depuración para poder instalarlo con facilidad.
// build.gradle.kts del modulo de la aplicacion
android {
buildTypes {
create("benchmark") {
initWith(buildTypes.getByName("release"))
signingConfig = signingConfigs.getByName("debug")
matchingFallbacks += listOf("release")
isDebuggable = false
proguardFiles("benchmark-rules.pro")
}
}
}
El detalle que más resultados falsea es el estado del dispositivo durante la medición. Un teléfono que lleva media hora midiendo se calienta, el sistema reduce la frecuencia de los núcleos para protegerlo y los últimos resultados salen peores que los primeros sin que nada haya cambiado en el código. A eso se suman las animaciones del sistema, que introducen esperas variables; el nivel de batería, que altera las políticas de energía; las aplicaciones en segundo plano, que compiten por procesador y memoria; y la conectividad, que introduce latencias ajenas al recorrido que se mide.
# Condiciones minimas para que dos ejecuciones sean comparables
adb shell settings put global window_animation_scale 0
adb shell settings put global transition_animation_scale 0
adb shell settings put global animator_duration_scale 0
# Fijar las frecuencias de los nucleos si el dispositivo lo permite
./gradlew lockClocks
# Comprobar el estado termico antes de dar por buena una tanda
adb shell dumpsys thermalservice | grep -i status
El error metodológico más frecuente consiste en guardar el resultado y no guardar el contexto. Un arranque de seiscientos milisegundos no significa nada por sí solo: significa algo si se sabe el modelo exacto de dispositivo, la versión del sistema, el modo de inicio, el modo de compilación, el número de iteraciones y si el dispositivo estaba en régimen térmico normal. Cuando esos datos no se registran junto con la medida, la comparación con la semana siguiente es una ilusión, porque cualquiera de esas variables puede haber cambiado y explicar por sí sola la diferencia observada.
Las dos mediciones que importan
La primera es el arranque, y su interés está menos en el número absoluto que en la matriz de combinaciones. Medir el arranque en frío sin perfil y con perfil parcial cuantifica lo que aporta la precompilación. Medir en frío y en templado separa el coste de crear el proceso del coste de reconstruir la actividad. Y medir con compilación total establece el límite inferior alcanzable por esa vía, que sirve para saber cuándo dejar de invertir en compilación y empezar a mirar el trabajo bloqueante.
@RunWith(AndroidJUnit4::class)
class MedicionDeArranque {
@get:Rule val regla = MacrobenchmarkRule()
@Test
fun arranqueEnFrioConPerfil() = regla.measureRepeated(
packageName = "com.ejemplo.app",
metrics = listOf(StartupTimingMetric()),
iterations = 20,
startupMode = StartupMode.COLD,
compilationMode = CompilationMode.Partial(),
) {
pressHome()
startActivityAndWait()
}
}
La segunda es el desplazamiento, y aquí la métrica clave merece explicación. La duración de fotograma en el hilo de interfaz indica cuánto trabajo hizo tu proceso, pero no dice si llegó a tiempo, porque el plazo depende de la frecuencia de refresco vigente en ese instante. Desde Android 12 existe una métrica de desbordamiento que mide directamente cuánto se pasó cada fotograma respecto a su plazo real, y cuyos valores negativos indican margen sobrante. Es la métrica que hay que vigilar, porque es la única que se mantiene interpretable en dispositivos con frecuencia adaptativa.
@Test
fun desplazamientoDeLaLista() = regla.measureRepeated(
packageName = "com.ejemplo.app",
metrics = listOf(FrameTimingMetric(), TraceSectionMetric("CargaDeFila")),
iterations = 15,
startupMode = StartupMode.WARM,
compilationMode = CompilationMode.Partial(),
setupBlock = { startActivityAndWait() },
) {
val lista = device.findObject(By.res("lista_inicio"))
lista.setGestureMargin(device.displayWidth / 5)
repeat(3) {
lista.fling(Direction.DOWN)
device.waitForIdle()
}
}
La sección de traza personalizada que aparece en el ejemplo es el mecanismo que convierte una medición en un diagnóstico. Marcando en el código de la aplicación los tramos que sospechas caros, la medición devuelve su duración agregada junto a las métricas de fotograma, y eso permite correlacionar un aumento del desbordamiento con el tramo concreto que creció, en lugar de tener que abrir una traza a mano cada vez.
flowchart TD
A[Cambio fusionado en la rama principal] --> B[Construccion del tipo benchmark]
B --> C[Dispositivo fisico dedicado con condiciones fijadas]
C --> D[Mediciones de arranque y desplazamiento]
D --> E[Resultados en formato estructurado con su contexto]
E --> F[Comparacion contra la linea base movil]
F --> G{Desviacion por encima del umbral}
G -->|No| H[Se actualiza la linea base]
G -->|Si| I[Se marca la construccion y se adjunta la traza]
I --> J[Bisecar entre los cambios de la ventana]
style D fill:#89b4fa,color:#11111b
style I fill:#f38ba8,color:#11111b
style H fill:#a6e3a1,color:#11111bDetectar regresiones sin ahogarse en falsos positivos
Aquí es donde fracasa la mayoría de los intentos, y el motivo es estadístico. La varianza natural entre dos tandas idénticas en el mismo dispositivo ronda con facilidad el cinco por ciento, y a veces más si el control térmico no es estricto. Un umbral de fallo del tres por ciento produce alertas constantes que el equipo aprende a ignorar en dos semanas, y a partir de ahí la instrumentación existe pero no protege nada. La primera tarea, antes de fijar ningún umbral, consiste en medir el suelo de ruido ejecutando la misma tanda contra el mismo binario varias veces y observando cuánto se mueve el resultado.
Con ese suelo conocido, la política razonable tiene cuatro piezas. Se compara la mediana de un número suficiente de iteraciones, nunca una ejecución aislada. Se compara contra una línea base móvil de las últimas construcciones buenas y no contra un valor fijo escrito hace un año. Se exige que la desviación supere claramente el suelo de ruido antes de marcar nada. Y se ejecuta siempre sobre el mismo modelo de dispositivo, porque comparar resultados de modelos distintos no informa de nada sobre tu código.
Queda una decisión de proceso que conviene tomar de forma explícita. Bloquear la fusión de un cambio por una regresión de rendimiento es tentador y suele ser contraproducente, porque la latencia de estas mediciones es alta y el coste de un falso positivo recae sobre alguien que probablemente no es el culpable. La alternativa que funciona mejor consiste en medir después de la fusión sobre la rama principal, con alerta y bisección automática cuando se detecta el escalón. Se pierde algo de inmediatez y se gana un sistema en el que las alertas siguen significando algo al cabo de seis meses.
Cada ejecución deja tras de sí una traza completa del sistema que puede abrirse en el visor correspondiente. Ese es el auténtico producto de la medición: el número sirve para saber que algo cambió, pero la traza es lo que dice qué cambió. Conservar las trazas de las construcciones marcadas como regresión, y no solo sus cifras, transforma la investigación posterior de una reconstrucción arqueológica en una simple lectura, y es la diferencia entre un sistema de medición que el equipo usa y uno que el equipo tolera.
Detrás de toda la maquinaria de iteraciones, modos de compilación, bloqueo de frecuencias y umbrales estadísticos hay una idea que merece enunciarse sin adornos: en ingeniería de rendimiento, lo que no se mide de forma reproducible sencillamente no existe como hecho, por muy convencido que esté quien lo afirma. Esto no es una exageración retórica sino una consecuencia directa de cómo funcionan estos sistemas. Un dispositivo móvil moderno es un entorno con planificación adaptativa, escalado dinámico de frecuencia, control térmico, compilación guiada por perfiles, gestión de memoria con expulsión de procesos y una capa de fabricante que interviene en casi todo lo anterior. En un sistema así, la misma operación ejecutada dos veces seguidas puede diferir en un factor considerable por razones que no tienen ninguna relación con el código. La percepción individual, incluso la de un ingeniero experimentado, carece por completo de la resolución necesaria para separar la señal de ese ruido, y sin embargo se invoca constantemente como si fuera evidencia. El resultado son equipos que dedican semanas a optimizaciones que nunca importaron, que revierten cambios inocentes, y que conviven durante años con una degradación real que nadie detectó porque llegó repartida en incrementos del uno por ciento a lo largo de treinta versiones. La instrumentación reproducible no es, por tanto, una práctica de higiene ni un requisito burocrático: es el instrumento que decide qué afirmaciones sobre el rendimiento tienen derecho a entrar en la conversación técnica. Cuando un equipo instala esa disciplina, ocurre algo que va mucho más allá del rendimiento: las discusiones dejan de resolverse por antigüedad o por vehemencia y pasan a resolverse por evidencia, y esa forma de trabajar, una vez adoptada, tiende a contagiarse a todas las demás decisiones del proyecto.
- Monta el módulo de medición y el tipo de construcción correspondiente, y consigue que la medición de arranque corra en un dispositivo físico.
- Genera la matriz completa de arranque combinando los tres modos de inicio con los tres modos de compilación, y explica cada celda.
- Mide el suelo de ruido ejecutando cinco tandas idénticas del mismo binario y calcula la dispersión real de tu montaje.
- Añade secciones de traza a los tres tramos que más sospechas y correlaciónalas con la métrica de desbordamiento de fotograma.
- Escribe la tarea automática que compara contra una línea base móvil, alerta solo por encima del suelo de ruido y conserva la traza de cada alerta.