Analisar com a Criação de perfil de renderização de GPU

A ferramenta Criação de perfil de renderização de GPU indica o tempo relativo que cada fase do pipeline de renderização leva para renderizar o frame anterior. Esse conhecimento pode ajudar a identificar gargalos no pipeline. Assim, você descobre o que precisa ser otimizado para melhorar o desempenho de renderização do app.

Esta página explica brevemente o que acontece durante cada fase de pipeline e discute problemas que podem causar gargalos. Antes de ler esta página, você precisa se familiarizar com as informações apresentadas em Criar perfil de renderização de GPU. Além disso, para entender como todas as fases se encaixam, pode ser útil revisar como o pipeline de renderização funciona.

Representação visual

A ferramenta Criação de perfil de renderização de GPU exibe as fases do pipeline e os tempos relativos delas na forma de um gráfico: um histograma codificado por cor. A Figura 1 mostra um exemplo.

Figura 1. Gráfico da Criação de perfil de renderização da GPU.

Cada segmento de cada barra vertical exibida no gráfico da Criação de perfil de renderização da GPU representa uma fase de pipeline e está destacada com uma cor específica no gráfico de barras. A Figura 2 mostra uma legenda para o significado de cada cor.

Figura 2. Legenda do gráfico da Criação de perfil de renderização da GPU.

Depois de entender o que cada cor significa, é possível segmentar aspectos específicos do seu app para tentar otimizar o desempenho de renderização.

As fases e o que elas significam

Esta seção explica o que acontece em cada fase correspondente a uma cor na Figura 2, assim como causas de gargalos a serem observadas.

Gerenciar a entrada (Input handling)

A fase de controle de entrada do pipeline mede quanto tempo o app passou controlando eventos de entrada. Essa métrica indica o tempo que o app passou executando o código chamado como resultado de callbacks de eventos de entrada.

Quando esse segmento é grande

Valores altos nessa área costumam ser resultado de muito trabalho ou de trabalho complexo demais e ocorrem nos callbacks de eventos de controle de entrada. Como esses callbacks sempre ocorrem na linha de execução principal, as soluções para esse problema se concentram em otimizar o trabalho diretamente ou descarregar o trabalho em uma linha de execução diferente.

Também vale a pena observar que a rolagem de RecyclerView pode aparecer nesta fase. RecyclerView rolará imediatamente quando consumir o evento de toque. Como resultado, ela pode inflar ou preencher novas exibições de itens. Por esse motivo, é importante fazer essa operação o mais rápido possível. Ferramentas de criação de perfil, como Traceview ou Systrace, podem ajudar você a investigar mais detalhadamente.

Animação

A fase de animações mostra o tempo necessário para avaliar todos os animadores que estavam sendo executados no frame. Os animadores mais comuns são ObjectAnimator, ViewPropertyAnimator e Transições.

Quando esse segmento é grande

Valores altos nessa área costumam ser resultado de trabalho que está sendo executado devido a alguma mudança na animação. Por exemplo, uma animação de rolagem, que rola sua ListView ou RecyclerView, causa grandes quantidades de inflação de visualização e preenchimento.

Medição/layout (Measurement/layout)

Para que o Android desenhe itens da sua exibição na tela, ele executa duas operações específicas em todos os layouts e exibições na sua hierarquia de exibição.

Em primeiro lugar, o sistema mede os itens de exibição. Cada exibição e layout tem dados específicos que descrevem o tamanho do objeto na tela. Algumas exibições podem ter um tamanho específico, enquanto outras têm um tamanho que se adapta ao tamanho do contêiner de layout pai.

Em segundo lugar, o sistema apresenta os itens de exibição. Depois que o sistema calcula os tamanhos das exibições filhas, ele pode prosseguir com o layout, dimensionando e posicionando as exibições na tela.

O sistema realiza a medição e o layout não apenas para que as exibições sejam desenhadas, mas também para as hierarquias pai dessas exibições. O processo vai até a exibição raiz.

Quando esse segmento é grande

Se o app gasta muito tempo por frame nessa área, geralmente é devido ao grande volume de exibições que precisam ser dispostas ou a problemas como tributação dupla no local errado da hierarquia. Em qualquer um desses casos, é necessário melhorar o desempenho das suas hierarquias de visualização.

O código que você adicionou a onLayout(boolean, int, int, int, int) ou onMeasure(int, int) também pode causar problemas de desempenho. Traceview e Systrace podem ajudar você a examinar as pilhas de chamadas para identificar problemas no código.

Desenho (Draw)

A fase de desenho traduz as operações de renderização de uma exibição, por exemplo, desenhos de plano de fundo ou texto, para uma sequência de comandos nativos de desenho. O sistema organiza esses comandos em uma lista de exibição.

A barra Draw registra quanto tempo leva para concluir a organização dos comandos na lista de exibição em relação a todas as exibições que precisavam de atualização na tela desse quadro. O tempo medido se aplica a qualquer código adicionado aos objetos de IU no seu app. Exemplos desse código podem ser onDraw(), dispatchDraw() e os vários draw ()methods pertencentes às subclasses da classe Drawable.

Quando esse segmento é grande

Em termos simplificados, você pode entender essa métrica como uma demonstração de quanto tempo levou para executar todas as chamadas para onDraw() para cada exibição invalidada. Essa medida inclui qualquer tempo gasto para despachar comandos de desenho para filhos e qualquer drawable que possa estar presente. Por esse motivo, quando você vir essa barra em destaque, poderá ser porque várias exibições se tornaram invalidadas de repente. A invalidação faz com que seja necessário regenerar as listas de exibição. Como alternativa, um tempo longo pode ser resultado de algumas visualizações personalizadas que têm alguma lógica extremamente complexa nos métodos onDraw().

Sincronização/upload (Sync/upload)

A métrica de sincronização e upload representa o tempo necessário para transferir objetos de bitmap da memória de CPU para a memória de GPU durante o frame atual.

Como processadores diferentes, a CPU e a GPU têm diferentes áreas de RAM dedicadas ao processamento. Quando você desenha um bitmap no Android, o sistema transfere o bitmap para a memória de GPU antes que a GPU possa renderizá-lo na tela. Depois disso, a GPU armazena o bitmap em cache para que o sistema não precise transferir os dados novamente, a não ser que a textura seja removida do cache de textura da GPU.

Observação: em dispositivos Lollipop, essa fase é roxa.

Quando esse segmento é grande

Todos os recursos de um frame precisam residir na memória da GPU antes de serem usados para desenhar um frame. Isso significa que um valor alto para essa métrica pode representar um grande número de pequenas cargas de recurso ou um pequeno número de recursos muito grandes. Um caso comum é quando um app exibe um único bitmap próximo ao tamanho da tela. Outro caso é quando um app exibe um grande número de miniaturas.

Para reduzir essa barra, você pode empregar as seguintes técnicas:

  • Garantir que as resoluções de bitmap não sejam muito maiores que o tamanho em que serão exibidas. Por exemplo, é recomendado que seu app evite exibir uma imagem de 1024x1024 como 48x48.
  • Aproveitar o prepareToDraw() para fazer o pré-upload assíncrono de um bitmap antes da próxima fase de sincronização.

Mandar comandos (Issue commands)

O segmento Issue Commands representa o tempo para emitir todos os comandos necessários para desenhar listas de exibição na tela.

Para que o sistema desenhe listas de exibição na tela, ele envia os comandos necessários para a GPU. Normalmente, ele executa essa ação usando a API OpenGL ES.

Esse processo leva algum tempo, já que o sistema realiza a transformação e o corte finais para cada comando antes de enviar o comando para a GPU. Há então uma sobrecarga adicional no lado da GPU, que computa os comandos finais. Esses comandos incluem transformações finais e outros recortes.

Quando esse segmento é grande

O tempo gasto nessa fase é uma medida direta da complexidade e da quantidade de listas de exibição renderizadas pelo sistema em um determinado frame. Por exemplo, um excesso de operações de desenho, principalmente em casos em que existe um pequeno custo inerente para cada desenho básico, pode inflar esse tempo. Exemplo:

Kotlin

for (i in 0 until 1000) {
    canvas.drawPoint()
}

Java

for (int i = 0; i < 1000; i++) {
    canvas.drawPoint()
}

é muito mais caro para emitir do que:

Kotlin

canvas.drawPoints(thousandPointArray)

Java

canvas.drawPoints(thousandPointArray);

Nem sempre há uma correlação direta entre a emissão de comandos e o desenho de listas de exibição. Ao contrário da Issue Commands, que captura o tempo para enviar comandos de desenho à GPU, a métrica Draw representa o tempo necessário para capturar os comandos emitidos na lista de exibição.

Essa diferença surge porque as listas de exibição são armazenadas em cache pelo sistema sempre que possível. Como resultado, existem situações em que uma rolagem, transformação ou animação precisa que o sistema reenvie uma lista de exibição, mas sem reconstruí-la (recapturar os comandos de desenho) do zero. Por isso, você pode ver uma alta na barra "Issue commands" sem uma alta na barra Draw commands.

Buffers de processo/troca (Process/swap buffers)

Quando o Android termina de enviar toda a lista de exibição para a GPU, o sistema emite um comando final para informar ao driver de gráficos que está tudo terminado no frame atual. Nesse ponto, o driver pode finalmente apresentar a imagem atualizada à tela.

Quando esse segmento é grande

É importante entender que a GPU é executada em paralelo com a CPU. O sistema Android emite comandos de desenho para a GPU e segue para a tarefa seguinte. A GPU lê esses comandos de desenho em uma fila e os processa.

Em situações em que a CPU emite comandos mais rápido do que a GPU consegue consumi-los, a fila de comunicações entre os processadores pode ficar cheia. Quando isso ocorre, a CPU bloqueia a emissão e espera até que haja espaço na fila para colocar o próximo comando. Esse estado de fila cheia acontece com frequência durante a fase Swap Buffers porque, nesse momento, todo um frame de comandos foi enviado.

O segredo para atenuar esse problema é reduzir a complexidade de trabalho que ocorre na GPU, de forma parecida com o que você faria na fase "Issue Commands".

Diversos (Miscellaneous)

Além do tempo que o sistema de renderização leva para executar o trabalho, há um conjunto adicional de trabalhos da linha de execução principal que não tem nada a ver com a renderização. O tempo que esses trabalhos consomem é registrado como misc time. O tempo diverso costuma representar trabalhos que podem estar ocorrendo na linha de execução de interface entre dois frames de renderização consecutivos.

Quando esse segmento é grande

Se esse valor é alto, é provável que seu app tenha callbacks, intents ou outros trabalhos que deveriam estar acontecendo em outra linha de execução. Ferramentas como Rastreamento de métodos ou Systrace podem fornecer visibilidade para as tarefas em execução na linha de execução principal. Essa informação pode ajudar você a focar em melhorias de desempenho.