Biblioteca de JankStats

La biblioteca de JankStats te ayuda a realizar un seguimiento de los problemas de rendimiento de tus aplicaciones y analizarlos. Jank ("bloqueo") hace referencia a los fotogramas de la aplicación que tardan demasiado en renderizarse. La biblioteca de JankStats brinda informes sobre las estadísticas de bloqueo de tu app.

Funciones

JankStats se basa en las funciones existentes de la plataforma de Android, incluida la API de FrameMetrics en Android 7 (nivel de API 24) y versiones posteriores, o bien OnPreDrawListener en versiones anteriores. Estos mecanismos pueden ayudar a las aplicaciones a realizar un seguimiento del tiempo que demoran en completarse los fotogramas. La biblioteca de JanksStats ofrece dos funciones adicionales que hacen que sea más dinámica y más fácil de usar: la heurística de bloqueos y el estado de la IU.

Heurística de bloqueos

Si bien puedes usar FrameMetrics para realizar un seguimiento de la duración de los fotogramas, esta no ofrece asistencia para determinar los bloqueos reales. JankStats, sin embargo, tiene mecanismos internos configurables para determinar cuándo se produce el bloqueo, lo que hace que los informes sean más útiles de inmediato.

Estado de la IU

A menudo, es necesario conocer el contexto de los problemas de rendimiento de tu app. Por ejemplo, si desarrollas una app multipantalla compleja que usa FrameMetrics y descubres que tu app suele tener fotogramas extremadamente inestables, asegúrate de contextualizar esa información con datos sobre dónde ocurrió el problema, qué hizo el usuario y cómo replicarlo.

Para solucionar este problema, JankStats introduce una API de state que te permite comunicarte con la biblioteca para proporcionar información sobre la actividad en la app. Cuando JankStats registra información sobre un fotograma con bloqueos, incluye el estado actual de la aplicación en los informes de bloqueo.

Uso

Para comenzar a usar JankStats, crea una instancia de la biblioteca y habilítala para cada Window. Cada objeto de JankStats realiza un seguimiento de los datos solo dentro de una Window. Para crear una instancia de la biblioteca, se requiere una instancia de Window junto con un objeto de escucha OnFrameListener, que se usan para enviar métricas al cliente. Se llama al objeto de escucha con FrameData en cada fotograma y se detalla lo siguiente:

  • Hora de inicio del fotograma
  • Valores de duración
  • Si el fotograma se debe considerar bloqueado o no
  • Un conjunto de pares de cadenas que contienen información sobre el estado de la aplicación durante el procesamiento de fotogramas

Para que JankStats sea más útil, las aplicaciones deben completar la biblioteca con información relevante sobre el estado de la IU para generar informes en FrameData. Puedes hacerlo a través de la API de PerformanceMetricsState (no directamente desde JankStats), donde se encuentran todas las APIs y la lógica de administración de estados.

Inicialización

Para comenzar a usar la biblioteca de JankStats, primero agrega la dependencia de JankStats a tu archivo de Gradle:

implementation "androidx.metrics:metrics-performance:1.0.0-beta01"

Luego, inicializa y habilita JankStats para cada Window. También debes pausar el seguimiento de JankStats cuando una actividad pasa a segundo plano. Crea y habilita el objeto JankStats en tus anulaciones de actividad:

class JankLoggingActivity : AppCompatActivity() {

    private lateinit var jankStats: JankStats


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // ...
        // metrics state holder can be retrieved regardless of JankStats initialization
        val metricsStateHolder = PerformanceMetricsState.getHolderForHierarchy(binding.root)

        // initialize JankStats for current window
        jankStats = JankStats.createAndTrack(window, jankFrameListener)

        // add activity name as state
        metricsStateHolder.state?.putState("Activity", javaClass.simpleName)
        // ...
    }

En el ejemplo anterior, se inserta información de estado sobre la actividad actual después de que se construye el objeto JankStats. Todos los informes de FrameData futuros creados para este objeto JankStats ahora también incluirán información sobre la actividad.

El método JankStats.createAndTrack toma a un objeto Window a modo de referencia, que es un proxy para la jerarquía de vistas dentro de esa Window, así como para la propia Window. Se llama a jankFrameListener en el mismo subproceso que se usa para entregar esa información de la plataforma a JankStats de forma interna.

Para habilitar el seguimiento y la generación de informes en cualquier objeto JankStats, llama a isTrackingEnabled = true. Aunque está habilitado de forma predeterminada, el seguimiento se inhabilita cuando se pausa una actividad. En este caso, asegúrate de volver a habilitar el seguimiento antes de continuar. Para detener el seguimiento, llama a isTrackingEnabled = false.

override fun onResume() {
    super.onResume()
    jankStats.isTrackingEnabled = true
}

override fun onPause() {
    super.onPause()
    jankStats.isTrackingEnabled = false
}

Informes

La biblioteca de JankStats informa todo el seguimiento de datos (de cada fotograma) al OnFrameListener para los objetos JankStats habilitados. Las apps pueden almacenar y agregar estos datos para subirlos más tarde. Si deseas obtener más información, consulta los ejemplos que se brindan en la sección Agregación.

Deberás crear y proporcionar un OnFrameListener para que tu app reciba los informes por fotograma. Se llama a este objeto de escucha en cada fotograma para proporcionarles a las apps datos de bloqueos en curso.

private val jankFrameListener = JankStats.OnFrameListener { frameData ->
    // A real app could do something more interesting, like writing the info to local storage and later on report it.
    Log.v("JankStatsSample", frameData.toString())
}

El objeto de escucha brinda información por fotograma sobre el bloqueo con el objeto FrameData. Incluye la siguiente información sobre el fotograma solicitado:

  • isjank: Marca booleana que indica si se produjo un bloqueo en el fotograma
  • frameDurationUiNanos: Duración del fotograma (en nanosegundos)
  • frameStartNanos: Hora en la que comenzó el fotograma (en nanosegundos)
  • states: Estado de la app durante el fotograma

Si utilizas Android 12 (nivel de API 31) o versiones posteriores, puedes usar lo siguiente para exponer más datos sobre la duración de los fotogramas:

Usa StateInfo en el objeto de escucha para almacenar información sobre el estado de la aplicación.

Ten en cuenta que se llama a OnFrameListener en el mismo subproceso que se usa internamente para entregar la información por fotograma a JankStats. En Android 6 (nivel de API 23) y versiones anteriores, ese es el subproceso principal (de IU). En Android 7 (nivel de API 24) y versiones posteriores, es el subproceso que se crea para FrameMetrics y el cual usa. En cualquier caso, es importante controlar la devolución de llamada y mostrarla rápidamente para evitar problemas de rendimiento en ese subproceso.

Además, ten en cuenta que el objeto FrameData enviado en la devolución de llamada se reutiliza en cada fotograma para evitar tener que asignar objetos nuevos para los informes de datos. Esto significa que debes copiar y almacenar en caché esos datos en otro lugar, ya que ese objeto se debe considerar inactivo y obsoleto en cuanto se muestra la devolución de llamada.

Agregación

Quizás te convenga que el código de tu app agregue los datos por fotograma, lo que te permite guardar y subir la información a tu discreción. Aunque los detalles sobre guardar y subir están fuera del alcance de la versión alfa de la API de JankStats, puedes usar JankAggregatorActivity, que está disponible en nuestro Repositorio de GitHub, para ver una actividad preliminar y agregar datos por fotograma en una colección más grande.

JankAggregatorActivity usa la clase JankStatsAggregator para superponer su propio mecanismo de informes sobre el mecanismo OnFrameListener de JankStats a fin de proporcionar una abstracción de nivel superior que informe solo acerca de una colección de información que abarca muchos fotogramas.

En lugar de crear un objeto JankStats directamente, JankAggregatorActivity crea un objeto JankStatsAggregator, que crea su propio objeto JankStats de forma interna:

class JankAggregatorActivity : AppCompatActivity() {

    private lateinit var jankStatsAggregator: JankStatsAggregator


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // ...
        // Metrics state holder can be retrieved regardless of JankStats initialization.
        val metricsStateHolder = PerformanceMetricsState.getHolderForHierarchy(binding.root)

        // Initialize JankStats with an aggregator for the current window.
        jankStatsAggregator = JankStatsAggregator(window, jankReportListener)

        // Add the Activity name as state.
        metricsStateHolder.state?.putState("Activity", javaClass.simpleName)
    }

En JankAggregatorActivity, se usa un mecanismo similar para pausar y reanudar el seguimiento (además del evento pause() como indicador para emitir un informe con una llamada a issueJankReport()), ya que los cambios del ciclo de vida parecen ser un momento adecuado con el objeto de capturar el estado de bloqueos en la aplicación:

override fun onResume() {
    super.onResume()
    jankStatsAggregator.jankStats.isTrackingEnabled = true
}

override fun onPause() {
    super.onPause()
    // Before disabling tracking, issue the report with (optionally) specified reason.
    jankStatsAggregator.issueJankReport("Activity paused")
    jankStatsAggregator.jankStats.isTrackingEnabled = false
}

El código de ejemplo anterior es todo lo que una app necesita para habilitar JankStats y recibir datos de fotogramas.

Administración del estado

Es posible que quieras llamar a otras APIs para personalizar JankStats. Por ejemplo, inyectar información del estado de la app hace que los datos del fotograma sean más útiles, ya que proporciona contexto para los fotogramas en los que se produce el bloqueo.

Este método estático recupera el objeto MetricsStateHolder actual de una jerarquía de vistas determinada.

PerformanceMetricsState.getHolderForHierarchy(view: View): MetricsStateHolder

Se puede usar cualquier vista en una jerarquía activa. De forma interna, se verifica si hay un objeto Holder existente asociado con esa jerarquía de vistas. Esta información se almacena en caché en una vista, en la parte superior de esa jerarquía. Si no existe tal objeto, getHolderForHierarchy() crea uno.

El método estático getHolderForHierarchy() te permite evitar tener que almacenar la instancia del contenedor en caché para su recuperación posterior y facilita la recuperación de un objeto de estado existente desde cualquier parte del código (o incluso desde el código de la biblioteca que, de lo contrario, no tendría acceso a la instancia original).

Ten en cuenta que el valor que se muestra es un objeto contenedor, no el objeto de estado en sí. El valor del objeto de estado dentro del contenedor está establecido solo por JankStats. Es decir, si una aplicación crea un objeto JankStats para la ventana que contiene esa jerarquía de vistas, se crea y configura el objeto de estado. De lo contrario, si JankStats no realiza un seguimiento de la información, el objeto de estado no es necesario y el código de la biblioteca o la app no necesita insertar el estado.

Este enfoque permite recuperar un contenedor que JankStats puede propagar posteriormente. El código externo puede solicitar el contenedor en cualquier momento. Los emisores pueden almacenar en caché el objeto Holder liviano y usarlo en cualquier momento para establecer el estado, según el valor de su propiedad state interna, como en el código de ejemplo que aparece a continuación, en el que solo se configura el estado cuando la propiedad de estado interna del contenedor no es nula:

val metricsStateHolder = PerformanceMetricsState.getHolderForHierarchy(binding.root)
// ...
metricsStateHolder.state?.putState("Activity", javaClass.simpleName)

Para controlar el estado de la IU o de la app, una app puede insertar (o quitar) un estado con los métodos putState y removeState. JankStats registra la marca de tiempo de estas llamadas. Si un fotograma superpone la hora de inicio y la hora de finalización del estado, JankStats da esa información de estado junto con los datos de latencia del fotograma.

Para cualquier estado, agrega dos datos: key (una categoría de estado, como "RecyclerView") y value (información sobre lo que estaba sucediendo en ese momento, como "desplazando").

Quita los estados con el método removeState() cuando ese estado ya no sea válido, para asegurarte de que no se dé información incorrecta o confusa con los datos del fotograma.

Llamar a putState() con un key que se agregó antes reemplaza el value existente de ese estado por el nuevo.

La versión putSingleFrameState() de la API de estado agrega un estado que se registra solo una vez, en el siguiente fotograma informado. El sistema lo quita automáticamente y se asegura de no tener un estado obsoleto en el código por accidente. Ten en cuenta que no hay un singleFrame equivalente a removeState(), ya que JankStats quita estados de un solo fotograma automáticamente.

private val scrollListener = object : RecyclerView.OnScrollListener() {
    override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
        // check if JankStats is initialized and skip adding state if not
        val metricsState = metricsStateHolder?.state ?: return

        when (newState) {
            RecyclerView.SCROLL_STATE_DRAGGING -> {
                metricsState.putState("RecyclerView", "Dragging")
            }
            RecyclerView.SCROLL_STATE_SETTLING -> {
                metricsState.putState("RecyclerView", "Settling")
            }
            else -> {
                metricsState.removeState("RecyclerView")
            }
        }
    }
}

Ten en cuenta que la clave que se usa para los estados debe ser lo suficientemente significativa para permitir un análisis posterior. En particular, dado que un estado con el mismo key que se agregó antes reemplazará ese valor anterior, debes intentar usar nombres key únicos para los objetos que pueden tener instancias diferentes en tu app o tu biblioteca. Por ejemplo, una app con cinco RecyclerViews diferentes podría querer proporcionar claves identificables para cada una de ellas en lugar de solo usar RecyclerView para cada una y, luego, no poder distinguir de forma fácil los datos resultantes a los que hacen referencia los datos del fotograma.

Heurística de bloqueos

Para ajustar el algoritmo interno a fin de determinar qué se considera un bloqueo, usa la propiedad jankHeuristicMultiplier.

De forma predeterminada, el sistema define el bloqueo como un fotograma que tarda el doble de tiempo en renderizarse que la frecuencia de actualización actual. No se considera bloqueo a cualquier tiempo superior a la frecuencia de actualización porque la información sobre el tiempo de renderización de la app no es del todo clara. Por lo tanto, se recomienda agregar un búfer y solo informar los inconvenientes cuando estos causen problemas de rendimiento perceptibles.

Ambos valores se pueden cambiar a través de estos métodos para adaptarse mejor a la situación de la app o en las pruebas con el objeto de forzar que el bloqueo ocurra o no, según sea necesario para la prueba.

Uso en Jetpack Compose

Actualmente, se requiere muy poca configuración para usar JankStats en Compose. Para conservar PerformanceMetricsState en todos los cambios de configuración, recuerda lo siguiente:

/**
 * Retrieve MetricsStateHolder from compose and remember until the current view changes.
 */
@Composable
fun rememberMetricsStateHolder(): PerformanceMetricsState.Holder {
    val view = LocalView.current
    return remember(view) { PerformanceMetricsState.getHolderForHierarchy(view) }
}

Para usar JankStats, agrega el estado actual a stateHolder, como se muestra a continuación:

val metricsStateHolder = rememberMetricsStateHolder()

// Reporting scrolling state from compose should be done from side effect to prevent recomposition.
LaunchedEffect(metricsStateHolder, listState) {
    snapshotFlow { listState.isScrollInProgress }.collect { isScrolling ->
        if (isScrolling) {
            metricsStateHolder.state?.putState("LazyList", "Scrolling")
        } else {
            metricsStateHolder.state?.removeState("LazyList")
        }
    }
}

Para obtener todos los detalles sobre el uso de JankStats en tu aplicación de Jetpack Compose, consulta nuestra app de ejemplo de rendimiento.

Envía comentarios

Usa estos recursos para compartir tus comentarios y tus ideas con nosotros:

Herramienta de seguimiento de errores
Informa los problemas para que podamos corregir los errores.