Caso de éxito: Cómo el equipo de Gmail Wear OS mejoró el inicio de su app en un 50%

El inicio de la app representa la primera impresión que tuvo tu app en los usuarios. Además, a los usuarios no les gusta esperar, por lo que debes asegurarte de que tu app se inicie rápido. Para mostrarte cómo un equipo de desarrollo de apps real encontró y diagnosticó problemas con el inicio de la app, esto es lo que hizo el equipo de Gmail para Wear OS.

El equipo de Gmail para Wear OS realizó un esfuerzo de optimización, en particular, en el inicio de la app y en el rendimiento de la renderización en el entorno de ejecución, para cumplir con los criterios de rendimiento de las apps de su equipo. Sin embargo, incluso si no tienes límites específicos, casi siempre hay espacio para mejorar el inicio de la app si te tomas un tiempo para investigarlo.

Cómo capturar un registro y observar el inicio de la app

Para comenzar el análisis, captura un registro que incluya el inicio de la app para realizar una inspección más detallada en Perfetto o Android Studio. En este caso de éxito, se usa Perfetto porque muestra lo que sucede en el sistema del dispositivo, más allá de tu app. Cuando subes el registro en Perfetto, se ve de la siguiente manera:

Figura 1: Vista inicial del registro en Perfetto.

Como el enfoque es mejorar el inicio de la app, ubica la fila con la métrica personalizada Android App Startups. Para fijarla en la parte superior de la vista, haz clic en el ícono de fijar que aparece cuando colocas el cursor sobre la fila. La barra o slice que ves en la fila Inicio de la app para Android indica el período que cubre el inicio de la app hasta que se dibuja el primer fotograma de la app en la pantalla, por lo que deberías buscar problemas o cuellos de botella allí.

Fila de inicio de la app para Android con la opción para fijar destacada.
Figura 2: Fija la métrica personalizada de inicio de la app para Android en la parte superior del panel para facilitar el análisis.

Ten en cuenta que la métrica Inicio de la app para Android representa el tiempo transcurrido hasta la visualización inicial, incluso si usas reportFullyDrawn(). Para identificar el tiempo para la visualización completa, busca reportFullyDrawn() en el cuadro de búsqueda de Perfetto.

Verifica el subproceso principal

Primero, examina lo que sucede en el subproceso principal. El subproceso principal es muy importante porque generalmente es donde se produce toda la renderización de la IU. Cuando se bloquea, no se puede realizar ningún dibujo y tu app parece estar bloqueada. Por lo tanto, debes asegurarte de que no se ejecuten operaciones de larga duración en el subproceso principal.

Para encontrar el subproceso principal, busca la fila con el nombre del paquete de tu app y expándela. Las dos filas con el mismo nombre que el paquete (por lo general, las dos primeras filas de la sección) representan el subproceso principal. De las dos filas de subprocesos principales, la primera representa el estado de la CPU y la segunda representa los puntos de seguimiento. Fija las dos filas de subprocesos principales debajo de la métrica Inicio de la app para Android.

Inicio de la app para Android y filas de subprocesos principales fijadas.
Figura 3: Fija las filas del subproceso principal debajo de la métrica personalizada de inicio de la app para Android a fin de facilitar el análisis.

Tiempo dedicado en estado ejecutable y contención de CPU

Para obtener una vista agregada de la actividad de la CPU durante el inicio de la app, arrastra el cursor sobre el subproceso principal a fin de capturar el intervalo de tiempo de inicio de la app. En el panel ThreadStates que aparece, verás la cantidad total de tiempo empleado en cada estado de la CPU dentro del intervalo de tiempo seleccionado.

Observa el tiempo que se pasó en el estado Runnable. Cuando un subproceso tiene el estado Runnable, está disponible para realizar tareas, pero no hay ninguna tarea programada. Eso podría indicar que el dispositivo tiene una carga pesada y no puede programar tareas de prioridad alta. La app superior visible para el usuario tiene la prioridad más alta en programación. Por lo tanto, un subproceso principal inactivo a menudo indica que los procesos intensivos dentro de tu app, como la renderización de animaciones, compiten con el subproceso principal por el tiempo de CPU.

Subproceso principal destacado con tiempo total en diferentes estados en el panel de estados de la conversación
Figura 4: Evalúa el tiempo relativo en los estados de Runnable a Running para obtener una idea inicial de cuánta contención de CPU hay.

Cuanto más alta sea la proporción de tiempo en el estado Runnable a tiempo en el estado Running, más probable es que se produzca una contención de CPU. Cuando inspecciones los problemas de rendimiento de esta manera, enfócate primero en el fotograma de mayor duración y trabaja en los más pequeños.

Cuando analices el tiempo empleado en el estado Runnable, ten en cuenta el hardware del dispositivo. Como la app que se muestra se ejecuta en un dispositivo wearable con dos CPUs, se espera que se pase más tiempo en el estado Runnable y que haya más contención de CPU con otros procesos que si estuviéramos en un dispositivo con más CPU. Si bien se pasa más tiempo en el estado Runnable de lo esperado para una app para teléfonos típica, podría ser comprensible en el contexto de wearables.

Tiempo dedicado en OpenDexFilesFromOat*

Ahora, verifica el tiempo empleado en OpenDexFilesFromOat*. En el registro, sucede al mismo tiempo que la porción bindApplication. Esta porción representa el tiempo necesario para leer los archivos DEX de la aplicación.

Transacciones de Binder bloqueadas

A continuación, verifica las transacciones de Binder. Las transacciones de Binder representan llamadas entre el cliente y el servidor: en este caso, la app (cliente) llama al sistema Android (servidor) con un binder transaction, y el servidor responde con un binder reply. Asegúrate de que la app no realice transacciones de Binder innecesarias durante el inicio, ya que aumentan el riesgo de contención de la CPU. Si puedes, aplaza el trabajo que implique llamadas a Binder después del período de inicio de la app. Si tienes que realizar transacciones de Binder, asegúrate de que no tarden más que la frecuencia de actualización de Vsync del dispositivo.

La primera transacción de Binder, que suele ocurrir al mismo tiempo que la porción ActivityThreadMain, parece bastante larga en este caso. Para obtener más información sobre lo que podría suceder, sigue estos pasos:

  1. Para ver la respuesta de Binder asociada y obtener más información sobre cómo se prioriza la transacción de Binder, haz clic en la porción de interés de la transacción de Binder.
  2. Para ver la respuesta de Binder, ve al panel Selección actual y haz clic en Respuesta de Binder en la sección Conversaciones siguientes. El campo Thread también te indica el subproceso en el que se produce la respuesta de Binder si deseas navegar allí de forma manual; estará en un proceso diferente. Aparecerá una línea que conecta la transacción y la respuesta de Binder.

    Una línea conecta la llamada y la respuesta a Binder.
    Figura 5: Identifica las transacciones de Binder que ocurren durante el inicio de la app y evalúa si puedes diferirlas.
  3. Para ver cómo el servidor del sistema controla esta transacción de Binder, fija los subprocesos de Cpu 0 y Cpu 1 en la parte superior de la pantalla.

  4. Busca los procesos del sistema que controlan la respuesta de Binder. Para ello, busca las secciones que incluyen el nombre del subproceso de respuesta de Binder, en este caso "Binder:687_11 [2542]". Haz clic en los procesos del sistema relevantes para obtener más información sobre la transacción de Binder.

Observa este proceso del sistema asociado con la transacción de interés de Binder que se produce en la CPU 0:

Proceso del sistema con estado final "Runnable (Interrumpido)".
Figura 6: El proceso del sistema se encuentra en estado Runnable (Preempted), lo que indica que se está retrasando.

El Estado final dice Runnable (Preempted), lo que significa que el proceso se retrasa porque la CPU está haciendo otra cosa. Para saber cuál es la interrupción, expande las filas Eventos de Ftrace. En la pestaña Eventos de Ftrace que está disponible, desplázate y busca eventos relacionados con el subproceso de Binder de interés "Binder:687_11 [2542]". Cerca del momento en que se interrumpe el proceso del sistema, ocurrieron dos eventos del servidor del sistema que incluyen el argumento "decon", lo que significa que están relacionados con el controlador de pantalla. Esto parece razonable porque el controlador de pantalla coloca los marcos en la pantalla, una tarea importante. Luego, deja los eventos tal como están.

Se destacan los eventos de FTrace asociados con la transacción de Binder de interés.
Figura 7: Los eventos de FTrace indican que los eventos de controlador de pantalla retrasan la transacción de Binder.

Actividad JIT

Para investigar la actividad de compilación justo a tiempo (JIT), expande los procesos que pertenecen a tu app, busca las dos filas del "grupo de subprocesos de Jit" y fíjalas en la parte superior de tu vista. Como esta app se beneficia de los perfiles de Baseline durante el inicio de la app, se produce muy poca actividad de JIT hasta que se dibuja el primer fotograma, representado por el final de la primera llamada a Choreographer.doFrame. Sin embargo, observa el motivo de inicio lento JIT compiling void, que sugiere que la actividad del sistema que se produce durante el punto de seguimiento etiquetado como Application creation está causando mucha actividad de JIT en segundo plano. Para resolver esto, agrega los eventos que ocurran poco después de que se dibuje el primer fotograma al perfil de Baseline expandiendo la colección de perfiles hasta un punto en el que la app esté lista para usarse. En muchos casos, puedes agregar una línea al final de la prueba de Macrobenchmark de la colección de perfiles de Baseline que espera a que aparezca un widget de la IU en particular en la pantalla para indicar que la pantalla está completamente propagada.

Conjuntos de subprocesos de Jit con la porción “Jit compiling void” destacada.
Figura 8: Si ves mucha actividad JIT, expande tu perfil de Baseline hasta el punto en que la app esté lista para usarse.

Resultados

Como resultado de este análisis, el equipo de Wear OS en Gmail realizó las siguientes mejoras:

  • Como notaron contención durante el inicio de la app cuando observaban la actividad de la CPU, reemplazaron la animación del ícono giratorio que se usa para indicar que la app se está cargando con una sola imagen estática. También prolongaron la pantalla de presentación para diferir el estado de brillo, el estado de la segunda pantalla que se usa para indicar que la app se está cargando, para liberar recursos de CPU. Esto mejoró la latencia de inicio de la app en un 50%.
  • A partir de la observación del tiempo dedicado a OpenDexFilesFromOat* y la actividad de JIT, se habilitaron la reescritura de perfiles de Baseline en R8. Esto mejoró la latencia de inicio de la app en un 20%.

Estas son algunas sugerencias del equipo sobre cómo analizar el rendimiento de las apps de manera eficiente:

  • Configura un proceso continuo que pueda recopilar automáticamente seguimientos y resultados. Considera configurar el seguimiento automatizado para tu app con comparativas.
  • Usa las pruebas A/B para los cambios que creas que mejorarán las cosas y recházalos si no lo hacen. Puedes medir el rendimiento en diferentes situaciones con la biblioteca de Macrobenchmark.

Para obtener más información, consulta los siguientes recursos: