Biblioteca JankStats

A biblioteca JankStats ajuda a monitorar e analisar problemas de desempenho nos seus aplicativos. "Jank" (instabilidade) se refere aos frames do aplicativo que levam muito tempo para renderizar, e a biblioteca JankStats fornece relatórios sobre as estatísticas de instabilidade do seu app.

Recursos

A JankStats é baseada nos recursos da Plataforma Android, incluindo a API FrameMetrics do Android 7 (nível 24 da API) e versões mais recentes ou OnPreDrawListener em versões anteriores. Esses mecanismos podem ajudar os aplicativos a monitorar o tempo necessário para que os frames sejam concluídos. Essa biblioteca oferece dois outros recursos que a tornam mais dinâmica e fácil de usar: a heurística de instabilidade e o estado da interface.

Heurística de instabilidade

Embora você possa usar a FrameMetrics para monitorar a duração do frame, ela não oferece nenhuma ajuda para determinar a instabilidade em si. No entanto, a JankStats tem mecanismos internos configuráveis para determinar quando ocorre a instabilidade, tornando os relatórios úteis de modo mais imediato.

Estado da interface

Muitas vezes, é necessário conhecer o contexto dos problemas de performance no app. Por exemplo, se você desenvolver um app complexo multitelas que usa a FrameMetrics e descobrir que ele costuma ter frames extremamente instáveis, vai precisar contextualizar essas informações entendendo onde o problema ocorreu, o que o usuário estava fazendo e como replicar a situação.

A JankStats resolve esse problema introduzindo uma API state, que possibilita a comunicação com a biblioteca para fornecer informações sobre a atividade no app. Quando a JankStats registra informações sobre um frame instável, ela inclui o estado atual do aplicativo em relatórios de instabilidade.

Uso

Para começar a usar a JankStats, instancie e ative a biblioteca para cada Window. Cada objeto JankStats acompanha os dados em apenas uma Window. Para instanciar a biblioteca é necessário uma instância Window e um listener OnFrameListener, que são usados para enviar métricas ao cliente. O listener é chamado com a classe FrameData em cada frame e detalha:

  • o horário de início do frame;
  • os valores de duração;
  • se o frame deve ou não ser considerado instável;
  • um conjunto de pares de strings contendo informações sobre o estado do aplicativo durante o frame.

Para tornar a JankStats mais útil, os aplicativos precisam preencher a biblioteca com informações de estado da interface relevantes para a geração de relatórios na classe FrameData. Você pode fazer isso usando a API PerformanceMetricsState (não diretamente a JankStats), que contém todas as APIs e a lógica de gerenciamento de estado.

Inicialização

Para começar a usar a biblioteca JankStats, primeiro adicione a dependência JankStats ao seu arquivo do Gradle:

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

Em seguida, inicialize e ative a JankStats para cada Window. Também é necessário pausar o monitoramento da JankStats quando uma atividade entra em segundo plano. Crie e ative o objeto JankStats nas substituições da atividade:

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)
        // ...
    }

O exemplo acima injeta informações de estado sobre a atividade atual depois de ela construir o objeto JankStats. Todos os relatórios da FrameData futuros criados para esse objeto JankStats agora também incluem informações sobre a atividade.

O método JankStats.createAndTrack usa uma referência a um objeto Window, que é um proxy para a hierarquia de visualização dentro dessa Window, assim como para a própria Window. O jankFrameListener é chamado na mesma linha de execução usada para enviar essas informações da plataforma à JankStats internamente.

Para ativar o monitoramento e a geração de relatórios em qualquer objeto JankStats, chame isTrackingEnabled = true. Embora ele fique ativado por padrão, a pausa de uma atividade desativa o monitoramento. Nesse caso, ative novamente antes de continuar. Para interromper o rastreamento, chame isTrackingEnabled = false.

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

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

Relatórios

A biblioteca JankStats relata todo o monitoramento dos dados, para cada frame, ao OnFrameListener para objetos JankStats ativados. Os apps podem armazenar e agregar esses dados para upload em outro momento. Para mais informações, confira os exemplos fornecidos na seção Agregação.

É preciso criar e fornecer o OnFrameListener para que o app receba os relatórios por frame. Esse listener é chamado em cada frame para fornecer dados de instabilidade contínuos aos apps.

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())
}

O listener fornece informações por frame sobre instabilidade com o objeto FrameData. Ele contém as seguintes informações sobre o frame solicitado:

  • isjank: uma flag booleana que indica se a instabilidade ocorreu no frame.
  • frameDurationUiNanos: duração do frame (em nanossegundos).
  • frameStartNanos: horário em que o frame começou (em nanossegundos).
  • states: estado do seu app durante o frame.

Se você está usando o Android 12 (nível 31 da API) ou versões mais recentes, pode usar o seguinte para expor mais dados sobre a duração de frames:

Use StateInfo no listener para armazenar informações sobre o estado do aplicativo.

Observe que o OnFrameListener é chamado na mesma linha de execução usada internamente para enviar as informações por frame para a JankStats. No Android 6 (nível 23 da API) e versões anteriores, ela corresponde à linha de execução principal de interface. No Android 7 (nível 24 da API) e versões mais recentes, corresponde à linha de execução criada e usada pela FrameMetrics. Nos dois casos, é importante processar o callback e retornar rapidamente para evitar problemas de desempenho nessa linha de execução.

Além disso, o objeto FrameData enviado no callback é reutilizado em cada frame para que novos objetos não precisem ser alocados para gerar relatórios de dados. Isso significa que é necessário copiar e armazenar em cache esses dados em outro lugar, porque esse objeto precisa ser considerado inativo e obsoleto assim que o callback retornar.

Agregação

O código do app provavelmente vai agregar os dados por frame, o que permite salvar e fazer upload das informações por conta própria. Embora os detalhes sobre como salvar e fazer upload estejam além do escopo da versão Alfa da API JankStats, você pode conferir uma atividade preliminar para agregar dados por frame em uma coleção maior usando a JankAggregatorActivity disponível no nosso Repositório do GitHub (link em inglês).

A JankAggregatorActivity (link em inglês) usa a classe JankStatsAggregator para sobrepor o próprio mecanismo de geração de relatórios sobre o mecanismo OnFrameListener da JankStats para fornecer uma abstração de nível superior e relatar apenas uma coleta de informações que abrange vários frames.

Em vez de criar um objeto JankStats diretamente, a JankAggregatorActivity cria um objeto JankStatsAggregator (link em inglês), que cria o próprio 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)
    }

Um mecanismo parecido é usado na JankAggregatorActivity para pausar e retomar o rastreamento, com o acréscimo do evento pause() como um indicador para emitir um relatório com uma chamada para issueJankReport(), já que as mudanças do ciclo de vida parecem ser um momento adequado para capturar o estado de instabilidade no aplicativo:

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
}

O código de exemplo acima é tudo que um app precisa para ativar a JankStats e receber dados de frame.

Gerenciamento do estado

É possível chamar outras APIs para personalizar a JankStats. Por exemplo, injetar informações de estado do app faz com que os dados do frame sejam mais úteis, fornecendo contexto para os frames em que a instabilidade ocorre.

Esse método estático extrai o objeto MetricsStateHolder atual de uma determinada hierarquia de visualização.

PerformanceMetricsState.getHolderForHierarchy(view: View): MetricsStateHolder

Qualquer visualização em uma hierarquia ativa pode ser usada. Internamente, isso confere se há um objeto Holder associado a essa hierarquia de visualização. Essas informações são armazenadas em cache em uma visualização na parte de cima da hierarquia. Se não houver nenhum objeto assim, o getHolderForHierarchy() vai criar um.

O método estático getHolderForHierarchy() permite evitar o armazenamento em cache da instância para depois, além de facilitar a recuperação de um objeto de estado de qualquer lugar no código, ou até mesmo do código da biblioteca, que de outra forma não teria acesso à instância original.

Observe que valor de retorno é um objeto detentor, não o próprio objeto de estado. O valor do objeto de estado dentro do detentor é definido apenas pela JankStats. Ou seja, se um aplicativo cria um objeto JankStats para a janela que contém essa hierarquia de visualização, o objeto de estado é criado e definido. Se a JankStats não monitorar as informações, o objeto de estado não vai ser necessário, e o código do app ou da biblioteca não vai precisar injetar o estado.

Essa abordagem permite a recuperação de um detentor que a JankStats pode preencher. O código externo pode solicitar o detentor a qualquer momento. Os autores da chamada podem armazenar o objeto leve Holder em cache e usá-lo a qualquer momento para definir o estado, dependendo do valor da propriedade interna state dele, como no exemplo de código abaixo, em que o estado é definido apenas quando a propriedade do estado interno do detentor não é nula:

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

Para controlar o estado da interface ou do app, um app pode injetar (ou remover) um estado com os métodos putState e removeState. A JankStats registra o carimbo de data/hora dessas chamadas. Se um frame se sobrepõe aos horários de início e término do estado, a JankStats relata essa informação com os dados de tempo do frame.

Para qualquer estado, adicione duas informações: key, uma categoria de estado (como "RecyclerView"), e value, informações sobre o que estava acontecendo no momento (como "rolagem").

Remova estados usando o método removeState() quando eles não forem mais válidos para garantir que informações incorretas ou enganosas não sejam relatadas com os dados do frame.

Ao chamar putState() usando uma key adicionada anteriormente, o value existente do estado é substituído pelo novo.

A versão putSingleFrameState() da API de estado adiciona um estado que é registrado apenas uma vez no próximo frame relatado. O sistema o remove de forma automática depois disso, garantindo que você não tenha acidentalmente um estado obsoleto no seu código. Observe que não há um singleFrame equivalente de removeState(), já que a JankStats remove os estados de frames únicos automaticamente.

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")
            }
        }
    }
}

A chave usada para os estados precisa ter informações suficientes para permitir uma análise futura. Como um estado com a mesma key já adicionada substitui o valor anterior, tente usar nomes de key exclusivos para objetos que possam ter instâncias diferentes no app ou na biblioteca. Por exemplo, um app com cinco RecyclerViews diferentes pode querer fornecer chaves identificáveis para cada uma delas em vez de simplesmente usar RecyclerView para cada uma e não conseguir distinguir facilmente os dados resultantes a que instância os dados do frame se referem.

Heurística de instabilidade

Para ajustar o algoritmo interno para determinar o que é considerado instabilidade, use a propriedade jankHeuristicMultiplier.

Por padrão, o sistema define instabilidade como um frame que leva duas vezes mais tempo na renderização quanto à a taxa de atualização atual. Ele não trata a instabilidade como nada além da taxa de atualização porque as informações sobre o tempo de renderização do app não estão totalmente claras. Por isso, é melhor adicionar um buffer e relatar dificuldades apenas quando houver problemas de desempenho perceptíveis.

Os dois valores podem ser mudados usando esses métodos para se adaptar à situação do app de maneira mais precisa ou em testes para forçar a ocorrência ou não da instabilidade, conforme necessário.

Uso no Jetpack Compose

No momento, há pouca configuração necessária para usar o JankStats no Compose. Para manter o PerformanceMetricsState nas mudanças de configuração, faça o seguinte:

/**
 * 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) }
}

E, para usar o JankStats, adicione o estado atual ao stateHolder, conforme mostrado aqui:

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 ver detalhes completos sobre o uso do JankStats no app Jetpack Compose, consulte nosso app de exemplo de desempenho.

Enviar feedback

Envie comentários e ideias usando os recursos abaixo:

Issue tracker
Informe os problemas para que possamos corrigir os bugs.