Testar o desempenho da IU

O teste de desempenho da interface do usuário (IU) garante que o app não só esteja de acordo com os requisitos funcionais, como também que as interações de usuários com ele sejam muito mais suaves, executando a 60 frames por segundo de forma consistente (por que 60 fps?), sem nenhum frame atrasado ou descartado ou, como gostamos de chamar, jank. Este documento explica as ferramentas disponíveis para medir o desempenho da IU e apresenta uma abordagem para integrar as medidas de desempenho da IU nas práticas de teste.

Medir o desempenho da IU

Para melhorar o desempenho, primeiro é necessário ter a habilidade de medir o desempenho do sistema e, em seguida, diagnosticar e identificar problemas que podem ocorrer em várias partes do pipeline.

dumpsys é uma ferramenta do Android que é executada no dispositivo e despeja informações interessantes sobre o status dos serviços do sistema. A transmissão do comando gfxinfo para dumpsys fornece uma saída no logcat com informações de desempenho relacionadas a frames de animação que ocorrem durante a fase de registro.

    > adb shell dumpsys gfxinfo <PACKAGE_NAME>
    

Este comando pode produzir diversas variáveis de dados de precisão de frame.

Agregar estatísticas de frame

Com o Android 6.0 (API de nível 23), o comando emite a análise agregada dos dados do frame no logcat, coletados durante todo o ciclo de vida do processo. Exemplo:

    Stats since: 752958278148ns
    Total frames rendered: 82189
    Janky frames: 35335 (42.99%)
    90th percentile: 34ms
    95th percentile: 42ms
    99th percentile: 69ms
    Number Missed Vsync: 4706
    Number High input latency: 142
    Number Slow UI thread: 17270
    Number Slow bitmap uploads: 1542
    Number Slow draw: 23342
    

Essas estatísticas de alto nível carregam um desempenho superior de renderização do app, além de estabilidade em vários frames.

Informações de precisão de frame

Com o Android 6.0, há um novo comando para gfxinfo, o framestats, que fornece informações de precisão de frame extremamente detalhadas a partir dos frames recentes, para que você possa rastrear e depurar problemas de forma mais precisa.

    >adb shell dumpsys gfxinfo <PACKAGE_NAME> framestats
    

Esse comando emite informações de tempo de frame, com timestamps de nanossegundos dos últimos 120 frames produzidos pelo app. Veja abaixo um exemplo da saída bruta das estatísticas do frame adb dumpsys gfxinfo <PACKAGE_NAME>:

    0,27965466202353,27965466202353,27965449758000,27965461202353,27965467153286,27965471442505,27965471925682,27965474025318,27965474588547,27965474860786,27965475078599,27965479796151,27965480589068,
    0,27965482993342,27965482993342,27965465835000,27965477993342,27965483807401,27965486875630,27965487288443,27965489520682,27965490184380,27965490568703,27965491408078,27965496119641,27965496619641,
    0,27965499784331,27965499784331,27965481404000,27965494784331,27965500785318,27965503736099,27965504201151,27965506776568,27965507298443,27965507515005,27965508405474,27965513495318,27965514061984,
    0,27965516575320,27965516575320,27965497155000,27965511575320,27965517697349,27965521276151,27965521734797,27965524350474,27965524884536,27965525160578,27965526020891,27965531371203,27965532114484,
    

Cada linha desta saída representa um frame produzido pelo app. Cada linha tem um número fixo de colunas, que descrevem o tempo gasto em cada estágio do pipeline de produção de frames. A próxima seção descreve esse formato com detalhes, incluindo o que cada coluna representa.

Formato de dados de estatísticas de frame

Como o bloco de dados é a saída no formato CSV, basta colá-lo na ferramenta de planilha de sua preferência ou coletá-lo e analisá-lo com um script. A tabela a seguir explica o formato das colunas de dados de saída. Todas as marcações de data e hora estão em nanossegundos.

  • FLAGS
    • Linhas com "0" na coluna FLAGS podem ter o tempo total para a renderização do frame calculado subtraindo-se a coluna INTENDED_VSYNC da coluna FRAME_COMPLETED.
    • Se for um número diferente de zero, a linha deverá ser ignorada, porque o frame será determinado como outlier em comparação ao desempenho normal, quando o esperado é que o layout e o desenho demorem mais do que 16 ms. Veja aqui alguns motivos para que isso ocorra:
      • Layout da janela alterado (por exemplo, o primeiro frame do app ou após uma rotação).
      • Também é possível que o frame seja ignorado, quando alguns valores ainda terão carimbos de data e hora de lixo. Um frame pode ser ignorado se, por exemplo, estiver executando a 60 fps ou se nada na tela estiver incorreto, o que não necessariamente é um sinal de problema no app.
  • INTENDED_VSYNC
    • O ponto inicial planejado para o frame. Se esse valor for diferente de VSYNC, isso significa que há um trabalho ocorrendo na linha de execução da IU que não permitiu que ele respondesse ao sinal de vsync de forma precisa.
  • VSYNC
    • O valor de tempo usado em todos os listeners vsync e no desenho para o frame (callbacks do frame Choreographer, animações, View.getDrawingTime() etc.)
    • Para entender mais sobre o VSYNC e como ele influencia seu app, assista ao vídeo Entendimento do VSYNC.
  • OLDEST_INPUT_EVENT
    • O carimbo de data/hora do evento de entrada mais antigo na fila de entrada, ou Long.MAX_VALUE{} se não existir eventos de entrada para o frame.
    • Esse valor é destinado principalmente a trabalhos da plataforma e tem utilidade limitada para desenvolvedores de apps.
  • NEWEST_INPUT_EVENT
    • O carimbo de data/hora do evento de entrada mais recente na fila de entrada, ou 0 se não houver eventos de entrada para o frame.
    • Esse valor é destinado principalmente a trabalhos da plataforma e tem utilidade limitada para desenvolvedores de apps.
    • No entanto, é possível ter uma ideia da quantidade de latência que o app está adicionando verificando (FRAME_COMPLETED - NEWEST_INPUT_EVENT).
  • HANDLE_INPUT_START
    • O carimbo de data/hora em que os eventos de entrada foram despachados para o app.
    • Observando o período entre essa marcação e ANIMATION_START, é possível medir quanto tempo o aplicativo gastou para lidar com os eventos de entrada.
    • Se esse número for alto (> 2 ms), isso indica que o app está gastando tempo demais processando os eventos de entrada, como View.onTouchEvent(), o que pode indicar que esse trabalho precisa ser otimizado ou descarregado para uma linha de execução diferente. Observe que há algumas situações, como eventos de clique que iniciam novas atividades, em que é esperado e aceitável que esse número seja grande.
  • ANIMATION_START
    • O carimbo de data/hora em que as animações registradas com Choreographer foram executadas.
    • Observando o período entre esse carimbo e PERFORM_TRANVERSALS_START, é possível determinar quanto tempo foi gasto para avaliar todos os animadores (ObjectAnimator, ViewPropertyAnimator e Transitions sendo as mais comuns) que estão sendo executados.
    • Se esse número for alto (maior que 2 ms), verifique se o app gravou qualquer animação personalizada ou quais campos os ObjectAnimators estão animando e confirme se eles são adequados para uma animação.
    • Para saber mais sobre Choreographer, assista ao vídeo Para melhor ou pior (em inglês).
  • PERFORM_TRAVERSALS_START
    • Se você subtrair DRAW_START desse valor, será possível extrair o tempo que as fases de medida e layout levaram para serem concluídas. Observe que, durante uma rolagem ou animação, espera-se que esse valor seja próximo de zero.
    • Para saber mais sobre as fases de medida e layout do pipeline de renderização, assista ao vídeo Invalidações, layouts e desempenho.
  • DRAW_START
    • O horário em que a fase de desenho de performTraversals foi iniciada. Esse é o ponto inicial do registro de listas de exibição de visualizações que foram invalidadas.
    • O período entre esse horário e SYNC_START é o tempo que levou para chamar View.draw() em todas as visualizações invalidadas na árvore.
    • Para saber mais sobre o modelo de desenho, consulte Aceleração de hardware ou assista ao vídeo Invalidações, layouts e desempenho.
  • SYNC_QUEUED
    • O momento em que uma solicitação de sincronização foi enviada ao RenderThread.
    • Isso marca o ponto no qual uma mensagem para iniciar a fase de sincronização foi enviada a RenderThread. Se o período entre esse momento e SYNC_START for substancial (aproximadamente > 0,1 ms), isso significa que RenderThread estava ocupado trabalhando em um frame diferente. Internamente, isso é usado para diferenciar entre o frame que está realizando muito trabalho e excedendo o orçamento de 16 ms e o frame parado devido ao frame anterior ter excedido o orçamento de 16 ms.
  • SYNC_START
    • O horário em que a fase de sincronização do desenho foi iniciada.
    • Se o período entre esse horário e ISSUE_DRAW_COMMANDS_START for substancial (aproximadamente > 0,4 ms), isso geralmente indicará que vários Bitmaps novos que foram desenhados precisarão ser enviados à GPU.
    • Para entender mais sobre a fase de sincronização, assista ao vídeo Renderização de GPU de perfil.
  • ISSUE_DRAW_COMMANDS_START
    • O horário em que o renderizador de hardware começou a emitir comandos de desenho para a GPU.
    • O período entre esse horário e FRAME_COMPLETED dá uma breve ideia da quantidade de trabalho de GPU que o app está produzindo. Problemas como excesso ou efeitos de renderização ineficientes são exibidos aqui.
  • SWAP_BUFFERS
    • O horário em que o eglSwapBuffers foi chamado, relativamente desinteressante fora do trabalho da plataforma.
  • FRAME_COMPLETED
    • Pronto! O tempo total gasto trabalhando neste frame pode ser calculado realizando FRAME_COMPLETED - INTENDED_VSYNC.

É possível usar esses dados de várias formas. Uma visualização simples, mas útil, é um histograma que mostra a distribuição dos tempos dos frames (FRAME_COMPLETED - INTENDED_VSYNC) em diferentes intervalos de latência. Consulte a figura abaixo. O gráfico mostra resumidamente que a maioria dos frames estava boa, bem abaixo do limite de 16 ms (representado em vermelho), mas que alguns frames estavam bem acima do limite. Podemos verificar as alterações nesse histograma ao longo do tempo para encontrar as mudanças indiscriminadas ou novos outliers sendo criados. Também é possível representar graficamente a latência de entrada, o tempo gasto no layout ou outras medidas interessantes com base nas várias marcações de data e hora nos dados.

Despejo temporizado de frame simples

Se a renderização de GPU de perfil (ou renderização de HWUI de perfil) for definida como Em adb shell dumpys gfxinfo nas Opções do desenvolvedor, o comando adb shell dumpsys gfxinfo emitirá informações de precisão para os 120 frames mais recentes, divididas em algumas categorias com valores separados por guias. Esses dados podem ser úteis para indicar quais partes do pipeline de desenho podem estar lentas em um nível alto.

Semelhante às estatísticas de frames acima, basta colá-los na ferramenta de planilha de sua escolha ou coletá-los e analisá-los com um script. O gráfico a seguir mostra um detalhamento de onde os vários frames produzidos pelo app gastaram o tempo.

O resultado de executar gfxinfo, copiar a saída, colá-la em um app de planilha e gerar um gráfico dos dados como barras empilhadas.

Cada barra vertical representa um frame da animação, e sua altura representa a quantidade de milissegundos para calcular esse frame da animação. Cada segmento colorido da barra representa um estágio diferente do pipeline de renderização para que você possa ver quais partes do aplicativo podem estar criando um afunilamento. Para saber mais sobre o pipeline de renderização e como otimizá-lo, assista ao vídeo Invalidações, layouts e desempenho.

Controlar janela de coleta de estatísticas

As precisões de frame simples e as estatísticas de frame coletam dados em um período muito curto, cerca de dois segundos de renderização. Para controlar esse período de forma precisa, por exemplo, para restringir os dados para uma animação em particular, é possível redefinir todos os contadores e agregar as estatísticas coletadas.

    >adb shell dumpsys gfxinfo <PACKAGE_NAME> reset
    

Isso também pode ser usado com os próprios comandos de despejo para coletar e redefinir em uma cadência regular, capturando períodos de menos de dois segundos de frames continuamente.

Diagnosticar regressões de desempenho

A identificação de regressões é uma boa primeira etapa para rastrear problemas e manter a integridade do app. No entanto, o dumpsys identifica apenas a existência e a gravidade relativa dos problemas. Ainda é necessário diagnosticar a causa particular dos problemas de desempenho e encontrar maneiras adequadas de resolvê-los. Para isso, é altamente recomendado o uso da ferramenta systrace.

Outros recursos

Para saber mais sobre como o pipeline de renderização do Android funciona, problemas comuns que podem ser encontrados e como resolvê-los, alguns dos seguintes recursos podem ser úteis:

Testes automatizados de desempenho da IU

Uma abordagem para o teste de desempenho da IU é fazer com que um testador humano realize uma série de operações de usuário no app alvo e procure visualmente problemas ou gaste uma grande quantidade de tempo usando uma abordagem de ferramenta para encontrá-los. No entanto, essa abordagem manual é repleta de riscos: a habilidade humana de notar alterações na taxa de frames varia tremendamente, além de consumir tempo, ser tediosa e propensa a erros.

Uma abordagem mais eficiente é registrar e analisar as principais métricas de desempenho dos testes de IU automatizados. O Android 6.0 inclui novas capacidades de registro que facilitam a determinação da quantidade e da gravidade de erros nas animações do app e que podem ser usadas para criar um processo rigoroso para determinar o desempenho atual e rastrear os futuros objetivos de desempenho.

Outros recursos

Para saber mais sobre esse tópico, consulte os recursos a seguir.

Codelabs