Métricas de performance que importan

No toda la performance importa igual. Las métricas que el usuario percibe directamente:

  • Startup time — cuánto tarda en mostrarse contenido útil desde que el usuario toca el ícono. Cada 100ms de mejora aumenta la retención.
  • Frame rate (jank) — Android dibuja a 60fps (16ms por frame). Si una operación tarda más de 16ms en el hilo principal, el usuario ve un "salto" o "tartamudeo".
  • Scroll performance — el RecyclerView debería scrollear a 60fps sin drops.
  • Uso de memoria — demasiada memoria causa que el sistema mate tu app en background (OOM).

Tipos de startup y cómo medirlos

# Android diferencia tres tipos de startup:
# - Cold start: app no está en memoria, proceso nuevo. El más lento.
# - Warm start: proceso existe pero Activity fue destruida.
# - Hot start: Activity está en background, solo necesita ir a foreground.

# Medir con adb:
adb shell am start-activity \
  -S -W \  # -S fuerza cold start matando el proceso anterior
  -n ar.pensa.miapp/.MainActivity

# Resultado:
# ThisTime: 487      ← tiempo de esta Activity específica
# TotalTime: 623     ← tiempo total desde launch hasta displayed
# WaitTime: 631

# Ver en Android Studio:
# Profiler → CPU → App startup

Time to Initial Display (TTID) vs Time to Full Display (TTFD)TTID es cuando el primer frame se dibuja. TTFD es cuando el contenido real está listo (después de cargar datos). Podés reportar el TTFD manualmente con reportFullyDrawn() en tu Activity para que Android lo mida correctamente.

Jank — cómo detectarlo y eliminarlo

# Ver frame stats en tiempo real:
adb shell dumpsys gfxinfo ar.pensa.miapp

# Buscar en la salida:
# Janky frames: 12 (4.17%)   ← frames que tardaron más de 16ms
# 50th percentile: 6ms
# 90th percentile: 10ms
# 95th percentile: 14ms
# 99th percentile: 32ms       ← el 1% más lento tardó 32ms (jank)

# En Android Studio Profiler:
# Profiler → Display → Frame rendering
# Barras rojas = jank frames

Las causas más comunes de jank:

  • Trabajo en el hilo principal: queries a base de datos, lecturas de disco, llamadas de red
  • RecyclerView.onBindViewHolder con lógica pesada
  • Layouts sobrecomplicados con demasiados niveles de anidamiento
  • Bitmaps grandes sin cachear ni redimensionar

Macrobenchmark — medir de forma reproducible

El Macrobenchmark es la librería oficial de Jetpack para medir performance de forma automatizada y reproducible, en un proceso separado:

// build.gradle (módulo macrobenchmark — módulo separado)
plugins {
    id("com.android.test")
    id("androidx.baselineprofile")
}

android {
    targetProjectPath = ":app"
    experimentalProperties["android.experimental.self-instrumenting"] = true
}

dependencies {
    implementation("androidx.benchmark:benchmark-macro-junit4:1.2.4")
}

// Test de startup:
@RunWith(AndroidJUnit4::class)
class StartupBenchmark {

    @get:Rule
    val benchmarkRule = MacrobenchmarkRule()

    @Test
    fun startup() = benchmarkRule.measureRepeated(
        packageName = "ar.pensa.miapp",
        metrics = listOf(StartupTimingMetric()),
        iterations = 5,
        startupMode = StartupMode.COLD
    ) {
        pressHome()
        startActivityAndWait()
    }
}

// Test de scroll:
@Test
fun scrollLista() = benchmarkRule.measureRepeated(
    packageName = "ar.pensa.miapp",
    metrics = listOf(FrameTimingMetric()),
    iterations = 5,
    startupMode = StartupMode.WARM
) {
    startActivityAndWait()
    val lista = device.findObject(By.res("ar.pensa.miapp:id/recyclerView"))
    lista.setGestureMargin(device.displayWidth / 5)
    lista.fling(Direction.DOWN)
    lista.fling(Direction.DOWN)
}
# Ejecutar los benchmarks (en dispositivo físico, release build):
./gradlew :macrobenchmark:connectedReleaseAndroidTest

# Los resultados aparecen en Android Studio → Benchmark tab
# con percentiles de tiempo y comparación entre runs

Baseline Profiles — acelerar el startup sin cambiar código

Android compila el bytecode a código nativo (ART) la primera vez que se ejecuta la app. Esto hace que el primer uso sea más lento. Un Baseline Profile le dice a ART qué código pre-compilar al instalar la app — mejoras de startup del 20-40% son comunes.

# El Baseline Profile es un archivo de texto que lista las clases y métodos
# que se usan en los flujos críticos (startup, principales pantallas):

# app/src/main/baseline-prof.txt (generado automáticamente)
HSPLar/pensa/miapp/MainActivity;->onCreate(Landroid/os/Bundle;)V
HSPLar/pensa/miapp/ui/productos/ProductosFragment;->onViewCreated(...)V
HSPLar/pensa/miapp/data/repository/ProductoRepositoryImpl;->getProductos()...

# H = Hot (método llamado frecuentemente — se JIT-compila)
# S = Startup (se llama en el startup — se AOT-compila)
# P = Post-startup

Crear un Baseline Profile automáticamente

// En el módulo macrobenchmark, crear un generador de profile:
@RunWith(AndroidJUnit4::class)
class BaselineProfileGenerator {

    @get:Rule
    val rule = BaselineProfileRule()

    @Test
    fun generate() = rule.collect(
        packageName = "ar.pensa.miapp"
    ) {
        // Recorrer los flujos principales de la app
        pressHome()
        startActivityAndWait()

        // Navegar a la pantalla de productos
        device.findObject(By.text("Productos")).click()
        device.waitForIdle()

        // Scrollear la lista
        val lista = device.findObject(By.res("ar.pensa.miapp:id/recyclerView"))
        lista.fling(Direction.DOWN)
        device.waitForIdle()
    }
}
# Generar el profile (en dispositivo físico con Android 9+):
./gradlew :macrobenchmark:connectedReleaseAndroidTest \
  -P android.testInstrumentationRunnerArguments.androidx.benchmark.enabledRules=BaselineProfile

# El profile generado se copia automáticamente a:
# app/src/main/baseline-prof.txt

# Al hacer el build de release, el profile se compila dentro del AAB.
# Play Store lo distribuye y ART lo usa al instalar la app.

Impacto realGoogle reporta mejoras de startup del 15-40% con Baseline Profiles, sin cambios en el código de la app. Para apps con mucho código Kotlin, la mejora puede ser mayor porque hay más bytecode a pre-compilar. Es una de las optimizaciones de mayor retorno por esfuerzo.

Memoria y leaks

// LeakCanary — detectar memory leaks automáticamente en debug
// Solo agregar la dependencia, no requiere código adicional
dependencies {
    debugImplementation("com.squareup.leakcanary:leakcanary-android:2.14")
}
// LeakCanary muestra una notificación cuando detecta un leak
// con el stack trace completo de quién retiene la referencia

// Los leaks más comunes en Android:
// 1. Fragment binding no anulado en onDestroyView (ya lo vimos en el curso Intermedio)
// 2. Listeners no removidos (LocationManager, SensorManager, BroadcastReceiver)
// 3. Contexto de Activity guardado en un singleton o companion object
// 4. Coroutines sin cancelar que retienen referencia a la Activity

// Para monitorear memoria en producción:
// Android Vitals en Play Console → Core vitals → Memory anomalies