Cómo inspeccionar el rendimiento de una app con Macrobenchmark

1. Antes de comenzar

En este codelab, aprenderás a usar la biblioteca de Macrobenchmark. Medirás el tiempo de inicio de la app, que es una métrica clave para la participación del usuario, y la latencia de fotogramas, que indica dónde podrían ocurrir bloqueos en tu app.

Requisitos

Actividades

  • Agregar un módulo de comparativas a una aplicación existente
  • Medir los tiempos de inicio y la latencia de fotogramas de una app

Qué aprenderás

  • Cómo medir el rendimiento de la aplicación de manera confiable

2. Cómo prepararte

Para comenzar, clona el repositorio de GitHub desde la línea de comandos usando el siguiente comando:

$ git clone https://github.com/googlecodelabs/android-performance.git

Como alternativa, puedes descargar dos archivos ZIP:

Abre el proyecto en Android Studio

  1. En la ventana Welcome to Android Studio, selecciona c01826594f360d94.png Open an Existing Project.
  2. Selecciona la carpeta [Download Location]/android-performance/benchmarking (sugerencia: Asegúrate de seleccionar el directorio benchmarking que contiene build.gradle).
  3. Cuando Android Studio haya importado el proyecto, asegúrate de que puedes ejecutar el módulo app para compilar la aplicación de ejemplo que compararemos.

3. Introducción a Jetpack Macrobenchmark

La biblioteca de Jetpack Macrobenchmark mide el rendimiento de las interacciones del usuario final más grandes, como el inicio, la interacción con la IU y las animaciones. La biblioteca ofrece un control directo sobre el entorno de rendimiento que estás probando. Te permite controlar la compilación, el inicio y la detención de la aplicación para medir directamente el inicio, la latencia de fotogramas y las secciones del código con seguimiento.

Con Jetpack Macrobenchmark, puedes hacer lo siguiente:

  • medir la app varias veces con patrones de lanzamiento y velocidades de desplazamiento deterministas
  • pulir la variación del rendimiento promediando los resultados de múltiples ejecuciones de prueba
  • controlar el estado de compilación de tu app (un factor importante en la estabilidad del rendimiento)
  • comprobar el rendimiento real con la reproducción local de optimizaciones en el momento de la instalación que realiza Google Play Store

Las instrumentaciones que usan esta biblioteca no llaman directamente al código de la aplicación. En su lugar, navegan por la aplicación como lo haría un usuario: tocan, hacen clic, realizan deslizamientos, etc. La medición se realiza en el dispositivo durante estas interacciones. Si quieres medir partes del código de la aplicación de forma directa, consulta Jetpack Microbenchmark.

Escribir comparativas es como escribir una prueba de instrumentación, excepto que no necesitas verificar el estado de tu app. Las comparativas usan la sintaxis JUnit (@RunWith, @Rule, @Test, etc.), pero las pruebas se ejecutan en un proceso independiente para permitir el reinicio o la compilación previa de tu app. Esto nos permite ejecutar la app sin interferir en sus estados internos, tal como lo haría un usuario. Para ello, usamos UiAutomator a fin de interactuar con la aplicación de destino.

La app de ejemplo

En este codelab, trabajarás con la aplicación de ejemplo de JetSnack. Es una app virtual de pedidos de bocadillos que usa Jetpack Compose. Con el fin de medir el rendimiento de la aplicación, no es necesario que conozcas los detalles de la arquitectura de la app. Lo que debes comprender es cómo se comporta la app y la estructura de la IU de modo que puedas acceder a los elementos de la IU desde las comparativas. Ejecuta la app y realiza pedidos de algunos bocadillos para familiarizarte con las pantallas básicas.

70978a2eb7296d54.png

4. Cómo agregar la biblioteca de Macrobenchmark

Macrobenchmark requiere que se agregue un nuevo módulo de Gradle a tu proyecto. La manera más fácil de agregarlo al proyecto es mediante el asistente de módulos de Android Studio.

Abre el diálogo del módulo nuevo (por ejemplo, haz clic con el botón derecho en tu proyecto o módulo desde el panel Project y selecciona New > Module).

54a3ec4a924199d6.png

Selecciona Benchmark en el panel Templates. Asegúrate de que esté seleccionada la opción Macrobenchmark como tipo de módulo de Benchmark y comprueba que los detalles sean los esperados:

Se seleccionó Macrobenchmark, el tipo de módulo de Benchmark.

  • Target application: app de la que se realizará la comparativa
  • Module name: nombre del módulo de Gradle de comparativas
  • Package name: nombre del paquete para las comparativas
  • Minimum SDK: se requiere, al menos, Android 6 (nivel de API 23) o versiones posteriores

Haz clic en Finish.

Cambios realizados por el asistente de módulos

El asistente de módulos realiza varios cambios en tu proyecto.

Se agrega un módulo de Gradle llamado macrobenchmark (o el nombre que hayas seleccionado en el asistente). Este módulo usa el complemento com.android.test, que le indica a Gradle que no lo incluya en tu app, de modo que solo pueda contener código de prueba (o comparativas).

El asistente también realiza cambios en el módulo de la aplicación de destino que seleccionaste. Específicamente, agrega un nuevo tipo de compilación de benchmark al build.gradle del módulo :app, como en el siguiente fragmento:

benchmark {
   initWith buildTypes.release
   signingConfig signingConfigs.debug
   matchingFallbacks = ['release']
   debuggable false
}

Este buildType debe emular tu tipo de compilación de release al máximo posible. La diferencia del buildType de release es que signingConfig se establece en debug, lo cual se necesita solo a los efectos de que puedas compilar la app de manera local sin requerir un almacén de claves de producción.

Sin embargo, debido a que la marca debuggable está inhabilitada, el asistente agrega la etiqueta <profileable> a tu AndroidManifest.xml a fin de permitir que las comparativas generen perfiles de tu app con el rendimiento de la versión.

<application>

  <profileable
     android:shell="true"
     tools:targetApi="q" />

</application>

Para obtener más información sobre lo que hace <profileable>, consulta nuestra documentación.

Por último, el asistente crea un andamiaje a fin de comparar los tiempos de inicio (con los que trabajaremos en el siguiente paso).

Ahora está todo listo para que comiences a escribir las comparativas.

5. Cómo medir el inicio de la app

El tiempo de inicio de la app, es decir, el tiempo que tardan los usuarios en comenzar a usar la app, es una métrica fundamental que afecta la participación. El asistente de módulos crea una clase de prueba ExampleStartupBenchmark, que es capaz de medir el tiempo de inicio de la app y tiene el siguiente aspecto:

@RunWith(AndroidJUnit4::class)
class ExampleStartupBenchmark {
   @get:Rule
   val benchmarkRule = MacrobenchmarkRule()

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

¿Qué significan todos los parámetros?

Cuando escribes una comparativa, tu punto de entrada es la función measureRepeated de MacrobenchmarkRule. Esta función se encarga de todo lo relativo a las comparativas, pero debes especificar los siguientes parámetros:

  • packageName: Las comparativas se ejecutan en un proceso independiente de la app que estás probando, por lo que debes especificar qué aplicación medir.
  • metrics: Se trata del tipo de información que deseas medir durante las comparativas. En nuestro caso, nos interesa establecer los tiempos de inicio de la app. Consulta la documentación para conocer otros tipos de métricas.
  • iterations: Es la cantidad de veces que se repetirán las comparativas. Más iteraciones implican resultados más estables, pero a costa de un tiempo de ejecución más largo. La cantidad ideal dependerá del nivel de ruido que tenga esta métrica en particular para tu app.
  • startupMode: Te permite definir cómo debería iniciarse la aplicación al inicio de tus comparativas. Modos disponibles: COLD, WARM y HOT. Usamos COLD, ya que representa la mayor cantidad de trabajo que debe hacer la app.
  • measureBlock (el último parámetro de la lambda): En esta función, definirás las acciones que desees medir durante las comparativas (iniciar una Actividad, hacer clic en los elementos de la IU, desplazar o deslizar contenido, etc.) y Macrobenchmark recopilará las metrics definidas durante este bloque.

Cómo escribir las acciones de comparación

Macrobenchmark volverá a instalar y reiniciará tu app. Asegúrate de que las interacciones sean independientes del estado de tu app. Macrobenchmark ofrece varias funciones y parámetros útiles para interactuar con tu app.

La más importante es startActivityAndWait(). Esta función iniciará tu Actividad predeterminada y esperará a que se renderice el primer fotograma antes de continuar con las instrucciones de tu comparativa. Si quieres iniciar una Actividad diferente o modificar el intent de inicio, puedes usar el intent opcional o el parámetro block para hacerlo.

Otra función útil es pressHome(). Esta te permite restablecer las comparativas a una condición base en los casos en que no finalices la app en cada iteración (por ejemplo, cuando usas StartupMode.HOT).

Para cualquier otra interacción, puedes usar el parámetro device, que te permite encontrar elementos de la IU, desplazar elementos, esperar a ver contenido, etcétera.

Ya definimos una comparativa de inicio. En el siguiente paso, la ejecutarás.

6. Cómo ejecutar las comparativas

Antes de ejecutar la prueba de comparativas, asegúrate de haber seleccionado la variante de compilación correcta en Android Studio:

  1. Selecciona el panel Build Variants.
  2. Cambia Active Build Variant a benchmark.
  3. Espera a que Android Studio se sincronice.

b8a622b5a347e9f3.gif

Si no hubieras hecho esto, las comparativas habrían fallado en el tiempo de ejecución con un error que indicaría que no deberías comparar una aplicación debuggable:

java.lang.AssertionError: ERRORS (not suppressed): DEBUGGABLE
WARNINGS (suppressed):

ERROR: Debuggable Benchmark
Benchmark is running with debuggable=true, which drastically reduces
runtime performance in order to support debugging features. Run
benchmarks with debuggable=false. Debuggable affects execution speed
in ways that mean benchmark improvements might not carry over to a
real user's experience (or even regress release performance).

Puedes suprimir temporalmente este error con el argumento de instrumentación androidx.benchmark.suppressErrors = "DEBUGGABLE". Puedes seguir los mismos pasos que los indicados en Cómo ejecutar comparativas en Android Emulator.

Ahora puedes ejecutar las comparativas de la misma manera en que lo harías con las pruebas de instrumentación. Puedes ejecutar la función de prueba o toda la clase con el ícono de margen junto a ella.

e72cc74b6fecffdb.png

Asegúrate de tener seleccionado un dispositivo físico, ya que la ejecución de comparativas en Android Emulator fallará durante el tiempo de ejecución y mostrará una advertencia que indica que arrojará resultados incorrectos. Si bien técnicamente puedes ejecutarlo en un emulador, básicamente estarías midiendo el rendimiento de tu máquina anfitrión. En caso de que esté muy cargada, tus comparativas tendrán un rendimiento más lento y viceversa.

e28a1ff21e9b45b4.png

Una vez que ejecutes las comparativas, se volverá a compilar tu app y se ejecutarán las comparativas. Estas se iniciarán, se detendrán e incluso reinstalarán la app varias veces según las iterations que hayas definido.

7. Cómo ejecutar comparativas en Android Emulator (opcional)

Si no tienes un dispositivo físico y aún deseas ejecutar las comparativas, puedes suprimir el error en el tiempo de ejecución con el argumento de instrumentación androidx.benchmark.suppressErrors = "EMULATOR".

Para suprimir el error, edita la configuración de ejecución:

  1. Selecciona "Edit Configurations…" en el menú de ejecución: 354500cd155dec5b.png
  2. En la ventana abierta, selecciona el ícono de "opciones" d628c071dd2bf454.png junto a "Instrumentation arguments" a4c3519e48f4ac55.png.
  3. Para agregar el parámetro adicional de instrumentación, haz clic en ➕ y escribe los detalles a06c7f6359d6b92c.png.
  4. Haz clic en Aceptar para confirmar la elección. Deberías ver el argumento en la fila de "Instrumentation arguments" c30baf54c420ed79.png.
  5. Confirma la configuración de ejecución haciendo clic en Aceptar.

Como alternativa, si necesitas tener esto de forma permanente en tu base de código, puedes hacerlo desde build.gradle en el módulo :macrobenchmark:

defaultConfig {
    // ...
    testInstrumentationRunnerArguments["androidx.benchmark.suppressErrors"] = 'EMULATOR'
}

8. Cómo comprender los resultados del inicio

Cuando la comparativa haya terminado de ejecutarse, obtendrás los resultados directamente en Android Studio, como en la siguiente captura de pantalla:

f934731d29dcd25c.png

Puedes observar que, en nuestro caso, el tiempo de inicio del Google Pixel 7 tiene un valor mínimo de 294.8 ms, una mediana de 301.5 ms y un máximo de 314.8 ms. Ten en cuenta que, en el dispositivo, es posible que se muestren resultados diferentes si ejecutas las mismas comparativas. Los resultados pueden verse afectados por muchos factores, como los siguientes:

  • qué tan potente es el dispositivo
  • qué versión de sistema usa
  • qué apps se ejecutan en segundo plano

Debido a esto, es importante comparar los resultados en el mismo dispositivo, idealmente en el mismo estado. De lo contrario, se pueden ver grandes diferencias. Si no puedes garantizar el mismo estado, te recomendamos que aumentes la cantidad de iterations para tratar los valores atípicos de resultados de forma adecuada.

A fin de permitir la investigación, la biblioteca de Macrobenchmark graba los registros del sistema durante la ejecución de las comparativas. Para tu comodidad, Android Studio marca cada iteración y los tiempos medidos como vínculos al registro del sistema, de modo que puedas abrirlos fácilmente e investigar.

9. Cómo declarar que tu app está lista para usarse (ejercicio opcional)

Macrobenchmark puede medir automáticamente el tiempo que transcurre hasta que la app renderiza el primer fotograma (timeToInitialDisplay). Sin embargo, suele suceder que el contenido de tu app no termine de cargarse hasta que se renderice el primer fotograma. Además, es posible que quieras saber cuánto debe esperar el usuario para usar la app. Esto se denomina tiempo de visualización completa: se alcanza cuando la app cargó por completo el contenido y el usuario puede interactuar con él. La biblioteca de Macrobenchmark puede detectar automáticamente ese momento, pero debes modificar la app de modo que sepa cuándo ocurrió con la función Activity.reportFullyDrawn().

En el ejemplo, se muestra una barra de progreso simple hasta que los datos se cargan, por lo que deberás esperar hasta que los datos estén listos y la lista de bocadillos se prepare y se dibuje. Modifiquemos la aplicación de ejemplo y agreguemos la llamada a reportFullyDrawn().

Abre el archivo Feed.kt en el paquete .ui.home del panel Project.

800f7390ca53998d.png

En ese archivo, busca el elemento SnackCollectionList componible y que es responsable de componer nuestra lista de bocadillos.

Debes verificar que los datos estén listos. Como sabes, hasta que el contenido esté listo, obtendrás una lista vacía del parámetro ​​snackCollections, por lo que podrás usar el elemento componible ReportDrawnWhen y que se encargará de generar informes una vez que el predicado sea verdadero.

ReportDrawnWhen { snackCollections.isNotEmpty() }

Box(modifier) {
   LazyColumn {
   // ...
}

Como alternativa, también puedes usar un elemento componible ReportDrawnAfter{} que acepte la función suspend y espere hasta que esta función se complete. De esta manera, puedes esperar a que se carguen algunos datos de forma asíncrona o que finalice algún tipo de animación.

Una vez que tengas esto, deberás ajustar la ExampleStartupBenchmark para esperar el contenido; de lo contrario, las comparativas finalizarían con el primer fotograma procesado y podrían omitir la métrica.

Las comparativas de inicio actuales esperan solo la renderización del primer fotograma. La espera se incluye en la función startActivityAndWait().

@Test
fun startup() = benchmarkRule.measureRepeated(
   packageName = "com.example.macrobenchmark_codelab",
   metrics = listOf(StartupTimingMetric()),
   iterations = 5,
   startupMode = StartupMode.COLD,
) {
   pressHome()
   startActivityAndWait()

   // TODO wait until content is ready
}

En nuestro caso, puedes esperar hasta que la lista de contenido tenga algunos elementos secundarios, por lo que debes agregar wait() tal como se ve en el siguiente fragmento:

@Test
fun startup() = benchmarkRule.measureRepeated(
   //...
) {
   pressHome()
   startActivityAndWait()

   val contentList = device.findObject(By.res("snack_list"))
   val searchCondition = Until.hasObject(By.res("snack_collection"))
   // Wait until a snack collection item within the list is rendered
   contentList.wait(searchCondition, 5_000)
}

Para explicar qué sucede en el fragmento, haremos lo siguiente:

  1. Encontramos la lista de bocadillos gracias a Modifier.testTag("snack_list").
  2. Definimos la condición de búsqueda que usa snack_collection como elemento para esperar.
  3. Usamos la función UiObject2.wait a fin de aguardar la condición dentro del objeto de la IU con un tiempo de espera de 5 segundos.

Ahora, puedes volver a ejecutar las comparativas, y la biblioteca medirá automáticamente timeToInitialDisplay y timeToFullDisplay, como se muestra en la siguiente captura de pantalla:

3655d7199e4f678b.png

Puedes ver que la diferencia entre TTID y TTFD en nuestro caso es de 413 ms. Esto significa que, aunque los usuarios vean un primer fotograma renderizado en 319.4 ms, no podrán desplazarse por la lista durante los siguientes 413 ms adicionales.

10. Comparativas de latencia de fotogramas

Una vez que los usuarios acceden a tu app, la segunda métrica que encuentran es la fluidez que ofrece la app. En nuestros términos, indica si la app descarta algún fotograma. Para medirlo, usaremos FrameTimingMetric.

Supongamos que deseas medir el comportamiento de desplazamiento de la lista de elementos y no quieres medir nada antes de esa situación. Debes dividir las comparativas en interacciones medidas y no medidas. Para ello, usaremos el parámetro lambda setupBlock.

En las interacciones no medidas (definidas en el setupBlock), iniciaremos la Actividad predeterminada y, en las interacciones medidas (definidas en el measureBlock), buscaremos el elemento de lista de la IU, desplazaremos la lista y esperaremos hasta que la pantalla renderice el contenido. Si no hubieras dividido las interacciones en las dos partes, no podrías diferenciar entre los fotogramas generados durante el inicio de la aplicación y los que se produjeron durante el desplazamiento de la lista.

Cómo crear comparativas de latencia de fotogramas

Para lograr el flujo mencionado, crearemos una nueva clase ScrollBenchmarks con una prueba de scroll() que contenga la comparativa de latencia de fotogramas tras un desplazamiento. Primero, crearás la clase de prueba con la regla comparativa y el método de prueba vacío:

@RunWith(AndroidJUnit4::class)
class ScrollBenchmarks {
   @get:Rule
   val benchmarkRule = MacrobenchmarkRule()

   @Test
   fun scroll() {
       // TODO implement scrolling benchmark
   }
}

Luego, agrega el esqueleto de la comparativa con los parámetros obligatorios.

@Test
fun scroll() {
   benchmarkRule.measureRepeated(
       packageName = "com.example.macrobenchmark_codelab",
       iterations = 5,
       metrics = listOf(FrameTimingMetric()),
       startupMode = StartupMode.COLD,
       setupBlock = {
           // TODO Add not measured interactions.
       }
   ) {
       // TODO Add interactions to measure list scrolling.
   }
}

La comparativa usa los mismos parámetros que la de startup, excepto el parámetro metrics y el setupBlock. FrameTimingMetric recopila la latencia de los fotogramas que produce la aplicación.

Ahora, completemos el setupBlock. Como se mencionó antes, en esta lambda, la comparativa no mide las interacciones. Puedes usar este bloque para abrir la app y esperar a que se renderice el primer fotograma.

@Test
fun scroll() {
   benchmarkRule.measureRepeated(
       packageName = "com.example.macrobenchmark_codelab",
       iterations = 5,
       metrics = listOf(FrameTimingMetric()),
       startupMode = StartupMode.COLD,
       setupBlock = {
           // Start the default activity, but don't measure the frames yet
           pressHome()
           startActivityAndWait()
       }
   ) {
       // TODO Add interactions to measure list scrolling.
   }
}

Ahora, escribamos el measureBlock (el último parámetro de lambda). En primer lugar, como el envío de artículos a la lista de bocadillos es una operación asíncrona, debes esperar a que el contenido esté listo.

benchmarkRule.measureRepeated(
   // ...
) {
    val contentList = device.findObject(By.res("snack_list"))

    val searchCondition = Until.hasObject(By.res("snack_collection"))
    // Wait until a snack collection item within the list is rendered
    contentList.wait(searchCondition, 5_000)

   // TODO Scroll the list
}

De manera opcional, si no estás interesado en medir la configuración de diseño inicial, puedes esperar que el contenido esté listo en el setupBlock.

A continuación, establece los márgenes de gestos en la lista de bocadillos. Debes hacer esto porque, de lo contrario, la app podría activar la navegación del sistema y salir de ella en lugar de desplazarse por el contenido.

benchmarkRule.measureRepeated(
   // ...
) {
   val contentList = device.findObject(By.res("snack_list"))

   val searchCondition = Until.hasObject(By.res("snack_collection"))
   // Wait until a snack collection item within the list is rendered
   contentList.wait(searchCondition, 5_000)

   // Set gesture margin to avoid triggering system gesture navigation
   contentList.setGestureMargin(device.displayWidth / 5)

   // TODO Scroll the list
}

Al final, realmente te desplazas por la lista con el gesto fling() (también puedes usar scroll() o swipe(), según la velocidad y el tiempo que quieras desplazarte) y esperas a que la IU quede inactiva.

benchmarkRule.measureRepeated(
   // ...
) {
   val contentList = device.findObject(By.res("snack_list"))

   val searchCondition = Until.hasObject(By.res("snack_collection"))
   // Wait until a snack collection item within the list is rendered
   contentList.wait(searchCondition, 5_000)

   // Set gesture margin to avoid triggering gesture navigation
   contentList.setGestureMargin(device.displayWidth / 5)

   // Scroll down the list
   contentList.fling(Direction.DOWN)

   // Wait for the scroll to finish
   device.waitForIdle()
}

La biblioteca medirá la latencia de fotogramas que produce nuestra app a la hora de realizar las acciones definidas.

Ahora tienes las comparativas listas para ejecutarse.

Cómo ejecutar las comparativas

Puedes ejecutar la comparativa de la misma manera que la de inicio. Haz clic en el ícono de margen junto a la prueba y selecciona Run 'scroll()'.

30043f8d11fec372.png

Si necesitas más información para ejecutar las comparativas, consulta el paso Cómo ejecutar la comparativa.

Cómo interpretar los resultados

FrameTimingMetric muestra la duración de los fotogramas en milisegundos (frameDurationCpuMs) en los percentiles 50, 90, 95 y 99. En Android 12 (nivel de API 31) y versiones posteriores, también muestra cuánto tiempo los fotogramas superaron el límite (frameOverrunMs). El valor puede ser negativo, lo que significa que hubo un resto de tiempo disponible para producir el fotograma.

2e02ba58e1b882bc.png

Según los resultados, puedes ver que el valor de la mediana (P50) para crear un fotograma en un Google Pixel 7 era de 3.8 ms, que es 6.4 ms por debajo del límite de latencia de fotogramas. Sin embargo, es posible que se hayan omitido algunos fotogramas en el percentil de más de 99 (P99), ya que la producción de fotogramas tomó 35.7 ms, valor que supera el límite de 33.2 ms.

Al igual que con los resultados del inicio de la app, puedes hacer clic en iteration a fin de abrir el registro del sistema durante la comparativa e investigar lo que contribuyó a la latencia resultante.

11. Felicitaciones

¡Felicitaciones! Completaste correctamente este codelab para medir el rendimiento con Jetpack Macrobenchmark.

¿Qué sigue?

Consulta el codelab Cómo mejorar el rendimiento de la app con los perfiles de Baseline. Además, consulta nuestro repositorio de GitHub de ejemplos de rendimiento que contiene Macrobenchmark y otros ejemplos de rendimiento.

Documentos de referencia