wandres.dev
RENDIMIENTO · arranque, R8 y baseline

Baseline profiles: precompilar el camino crítico

Desde que la plataforma abandonó la compilación anticipada total en la instalación, todo el código de una aplicación nace interpretado y solo se compila a lenguaje máquina cuando el entorno de ejecución observa que se usa lo suficiente. Eso significa que los primeros días de vida de cada instalación son sistemáticamente más lentos que el resto. Un `baseline profile` es la lista de clases y métodos del camino crítico que se empaqueta con la aplicación para que la compilación anticipada ocurra el día cero. Esta lección explica el modelo de compilación de ART, la anatomía exacta del fichero de perfil, cómo se genera con `Macrobenchmark`, cómo se verifica que llegó al dispositivo y cuál es su ganancia real medida y sus límites honestos.

⏱ 22 min

Existe una intuición muy arraigada, heredada de los lenguajes compilados de escritorio, según la cual el código que se instala en un dispositivo ya está traducido a instrucciones de la máquina y listo para correr. En Android eso dejó de ser cierto hace casi una década, y la mayoría de los desarrolladores no llegó a enterarse porque el cambio se hizo por debajo, sin romper nada visible. Lo que hoy se instala es fundamentalmente código intermedio, y el entorno de ejecución decide sobre la marcha qué merece traducirse y qué no, observando lo que el usuario hace. La consecuencia es incómoda y rara vez se enuncia con claridad: durante los primeros días de vida de cada instalación, tu aplicación corre medida sistemáticamente peor que en cualquier prueba que hayas hecho sobre un dispositivo que llevaba semanas con ella. Un baseline profile es el mecanismo por el cual esa penalización de novedad se puede eliminar de antemano.

🎯 Al terminar esta lección sabrás
  • Explicar el modelo de compilación de ART y por qué el código nace interpretado en cada instalación.
  • Leer y escribir la sintaxis de un fichero baseline profile, incluidos sus indicadores de uso.
  • Generar el perfil con el complemento de Gradle y una prueba de Macrobenchmark que recorra el camino crítico.
  • Verificar en el dispositivo que el perfil se instaló y se compiló, y cuantificar su ganancia real.

Por qué tu código nace lento en cada instalación

Hasta Android 6 la instalación compilaba anticipadamente toda la aplicación. Funcionaba, pero producía instalaciones lentísimas, actualizaciones del sistema que tardaban horas en optimizar el catálogo entero y un consumo de almacenamiento considerable en código traducido que en su mayoría nunca se ejecutaba. A partir de Android 7 el modelo cambió a uno híbrido y guiado por perfiles, que es el que rige hoy.

En ese modelo, la instalación no compila prácticamente nada. El código empieza interpretado. El compilador de tiempo de ejecución observa qué métodos se ejecutan con frecuencia y los traduce sobre la marcha, y al mismo tiempo el entorno va anotando en un perfil local qué clases y qué métodos resultaron calientes. Periódicamente, cuando el dispositivo está inactivo y cargando, un servicio en segundo plano toma ese perfil y compila anticipadamente lo que contiene, de modo que a partir de entonces esas rutas ya nacen traducidas en cada arranque.

El sistema funciona bien y es el motivo por el que las instalaciones son rápidas y el almacenamiento razonable. Pero tiene un agujero temporal evidente: hasta que ese ciclo de observación y compilación se completa, cosa que puede llevar días de uso real, el usuario está ejecutando código interpretado en las rutas más importantes. Y esa ventana coincide exactamente con el periodo en el que decide si conserva la aplicación o la desinstala.

🐌

Interpretado

Estado inicial de todo el código tras instalar. Correcto pero lento, y es lo que el usuario sufre los primeros días.

⚙️

Compilado en caliente

El compilador de tiempo de ejecución traduce lo que observa frecuente. Ayuda dentro de la sesión, no antes de ella.

📜

Perfil local

Anotación acumulada de lo caliente, que un servicio en segundo plano convierte en compilación anticipada al cabo de días.

🚀

Baseline profile

Ese mismo perfil, calculado por ti y empaquetado con la aplicación, aplicado en la instalación. El día cero deja de ser lento.

Existe además un mecanismo complementario que conviene conocer para no atribuir al perfil ganancias que no le corresponden: la tienda agrega perfiles anónimos de usuarios reales y los distribuye a instalaciones nuevas del mismo binario. Es un buen mecanismo, pero solo actúa cuando ya hay una masa de usuarios con esa versión concreta, lo que deja desprotegida cada nueva publicación durante sus primeros días, que suelen ser los de mayor volumen de instalación.

Anatomía del fichero: qué se declara y con qué indicador

Un baseline profile es, en su forma fuente, un fichero de texto plano con una regla por línea. Cada regla identifica una clase o un método con la notación de descriptores del formato de código intermedio, y puede llevar delante uno o varios indicadores que expresan cómo se usa. El indicador de método caliente marca aquello que se invoca muchas veces. El indicador de arranque marca lo que participa en el arranque. El indicador posterior al arranque marca lo que se usa poco después. Una línea sin indicador que nombra una clase completa expresa simplemente que esa clase debe cargarse anticipadamente.

# Contenido tipico de baseline-prof.txt
# H caliente, S arranque, P posterior al arranque
HSPLcom/ejemplo/app/MainActivity;-><init>()V
HSPLcom/ejemplo/app/MainActivity;->onCreate(Landroid/os/Bundle;)V
Lcom/ejemplo/app/datos/RepositorioInicio;
HSPLcom/ejemplo/app/ui/ListaKt;->Fila(Lcom/ejemplo/app/Modelo;)V

Durante el ensamblado, el complemento convierte ese texto en el formato binario que el entorno de ejecución entiende y lo empaqueta dentro del artefacto. En el momento de la instalación, el sistema lee ese perfil y compila anticipadamente lo que contiene, sin esperar a observar nada. Conviene subrayar que las bibliotecas también publican sus propios perfiles: Compose incluye el suyo, y buena parte de la mejora que se atribuye al perfil propio proviene en realidad de que el perfil de la biblioteca se aplica correctamente.

Hay un pariente cercano que se confunde a menudo con el perfil de referencia y que merece nombre propio: el perfil de arranque. No sirve para decidir qué se compila, sino para reordenar físicamente las clases dentro del fichero de código intermedio, de modo que las que se necesitan durante el arranque queden contiguas. La ganancia no viene de la traducción sino de la localidad: menos fallos de página al leer del almacenamiento. Se genera con el mismo mecanismo y se declara aparte.

💡
Un perfil es una hipótesis sobre el uso, no un volcado

La tentación al generar un perfil es recorrer toda la aplicación para cubrirlo todo. Es un error. Un perfil que lo contiene todo equivale a la compilación anticipada total, con su coste de instalación y de almacenamiento, y diluye la ventaja de localidad. El perfil debe recorrer el arranque y las dos o tres interacciones que la inmensa mayoría de los usuarios realiza en los primeros segundos. Todo lo demás es mejor dejarlo al compilador de tiempo de ejecución, que ya hace bien su trabajo cuando tiene tiempo de observar.

Generar, incluir y verificar

La generación se apoya en el complemento de Gradle correspondiente y en un módulo de Macrobenchmark que contiene una prueba cuyo único cometido es recorrer el camino crítico con la aplicación real. El sistema arranca la aplicación varias veces, ejercita el recorrido que le indiques y captura el perfil que el entorno de ejecución produjo, filtrándolo y consolidándolo en el fichero de reglas. Requiere un dispositivo o emulador con imagen de sistema sin restricciones, porque hay que acceder al perfil interno.

@RunWith(AndroidJUnit4::class)
class GeneradorDePerfil {

    @get:Rule val regla = BaselineProfileRule()

    @Test
    fun recorridoCritico() = regla.collect(
        packageName = "com.ejemplo.app",
        includeInStartupProfile = true,
    ) {
        pressHome()
        startActivityAndWait()
        // Solo lo que hace la mayoria en los primeros segundos
        device.findObject(By.res("lista_inicio")).fling(Direction.DOWN)
        device.waitForIdle()
    }
}

Una vez generado, el fichero se ubica en el directorio de fuentes de la variante correspondiente y el complemento se encarga de empaquetarlo. La parte que casi todo el mundo omite es la verificación, y omitirla es peligroso porque un perfil mal ubicado, mal formado o invalidado por una firma distinta no produce ningún error visible: simplemente no hace nada, y el equipo celebra durante meses una mejora que no existe.

Hay dos formas complementarias de verificar. Desde la terminal se puede consultar el estado de optimización del paquete y comprobar que la razón de compilación corresponde a la instalación con perfil y no a un modo genérico. Desde el propio código existe una interfaz de verificación que informa en tiempo de ejecución de si el perfil se instaló, si se compiló y si está desactualizado respecto al binario, y ese dato conviene registrarlo junto con la telemetría de arranque para poder correlacionar.

# Estado de compilacion del paquete en el dispositivo
adb shell dumpsys package dexopt | grep -A 3 com.ejemplo.app

# Forzar la compilacion con el perfil empaquetado para probar en local
adb shell cmd package compile -f -m speed-profile com.ejemplo.app

# Volver al estado de instalacion limpia
adb shell cmd package compile --reset com.ejemplo.app
flowchart TD
A[Prueba de Macrobenchmark que recorre el camino critico] --> B[Perfil capturado del entorno de ejecucion]
B --> C[Fichero de reglas en texto plano]
C --> D[Empaquetado en el artefacto durante el ensamblado]
D --> E[Instalacion en el dispositivo]
E --> F[Compilacion anticipada del camino critico]
F --> G[Primer arranque ya traducido]
E --> H[Verificacion con la interfaz de perfil]
H --> I[Registrado junto a la telemetria de arranque]
style C fill:#89b4fa,color:#11111b
style G fill:#a6e3a1,color:#11111b
style H fill:#f9e2af,color:#11111b

La ganancia real y los límites honestos

Las cifras que suelen citarse sitúan la mejora del arranque entre el quince y el treinta por ciento, con casos mejores en aplicaciones grandes construidas sobre Compose y casos peores en aplicaciones pequeñas cuyo arranque ya era ligero. La mejora en la fluidez del primer desplazamiento suele ser todavía más visible que la del arranque, porque el desplazamiento inicial es justamente donde el código interpretado produce fotogramas perdidos en ráfaga.

Pero conviene enunciar los límites con la misma claridad. Un perfil no acelera el hot start, porque ahí no se carga ni se compila nada. No corrige código lento: si tu arranque hace una lectura de disco en el hilo principal, compilarla anticipadamente la hace marginalmente más rápida y sigue siendo un error. No cubre lo que la prueba de generación no recorrió, de modo que un perfil generado sobre una pantalla que después se rediseñó por completo aporta poco. Y envejece con el código, lo que obliga a regenerarlo como parte del proceso de publicación y no como una tarea que alguien hizo una vez.

La medición correcta de la ganancia exige comparar el mismo binario bajo dos modos de compilación distintos, uno sin perfil y otro con compilación parcial guiada por el perfil empaquetado, y hacerlo con suficientes iteraciones para que la diferencia supere el ruido. Comparar contra una compilación totalmente anticipada resulta útil como límite superior teórico: si el perfil ya te sitúa cerca de ese número, no queda margen por ese camino y el problema está en otra parte.

ℹ️
El perfil no sustituye a la disciplina, la revela

Cuando un equipo mide su arranque con compilación total anticipada y descubre que apenas mejora respecto al estado sin perfil, la conclusión es clara y desagradable: el arranque no está limitado por la traducción de código sino por trabajo bloqueante, entrada y salida, o inicializaciones excesivas. En ese escenario, invertir en el perfil produce ganancias marginales y la energía debe ir a otro sitio. Ese contraste entre los tres modos de compilación es, además de una medición, un diagnóstico arquitectónico muy barato.

La compilación dejó de ser un momento y pasó a ser una negociación

Lo que un baseline profile revela, más allá de su utilidad práctica, es que en una plataforma móvil moderna la frontera entre lo que se decide en tiempo de compilación y lo que se decide en tiempo de ejecución se ha vuelto porosa hasta el punto de dejar de ser una frontera. El modelo clásico era limpio y binario: había un momento de traducción, anterior a la ejecución y ajeno a ella, y después había ejecución. Ese modelo se rompió cuando el coste de traducirlo todo resultó insoportable en un dispositivo con batería y almacenamiento finitos y con un catálogo de aplicaciones que el usuario apenas usa. Lo que lo sustituyó es un régimen de decisión continua en el que el entorno de ejecución observa, mide, apuesta y revisa sus apuestas, y en el que el desarrollador no elige entre compilar o no compilar sino que participa en una negociación aportando información previa sobre lo que va a ocurrir. El perfil es exactamente eso: una declaración de expectativas sobre el comportamiento futuro del usuario, formulada por quien conoce el producto, entregada a un sistema que de otro modo tendría que descubrirla por observación y a costa de la experiencia de los primeros días. Esto tiene una consecuencia intelectual que trasciende Android y que conviene interiorizar, porque reaparece en la compilación anticipada de otros entornos, en las cachés de código de los navegadores y en cualquier sistema con optimización guiada por perfiles: el conocimiento del dominio se ha convertido en una entrada legítima del compilador. Ya no basta con escribir código correcto y confiar en que alguien lo traduzca bien. Hay que saber qué hace la gente con él, en qué orden y con qué frecuencia, y ser capaz de expresarlo en un formato que la máquina pueda usar. El perfil no es un fichero de configuración: es la parte del diseño que hasta ahora vivía solo en la cabeza del equipo, escrita por fin en un sitio donde la máquina puede leerla.

⚔️ Cuantifica lo que tu perfil aporta de verdad
  1. Mide el arranque en frío de tu aplicación bajo los tres modos de compilación disponibles y construye la tabla comparativa completa.
  2. Genera un perfil que recorra solo el arranque y el primer desplazamiento, y compáralo con otro que recorra media aplicación.
  3. Verifica en el dispositivo que el perfil llegó y se compiló, y registra ese estado junto con tu telemetría de arranque.
  4. Provoca deliberadamente un perfil desactualizado renombrando una clase del camino crítico y observa qué informa la verificación.
  5. Escribe la tarea de integración continua que regenera el perfil y falla la construcción si la ganancia medida cae por debajo del umbral acordado.