Estudo de caso: como a equipe do Gmail para Wear OS melhorou a inicialização do app em 50%

A inicialização representa a primeira impressão que o app tem nos usuários. E os usuários não gostam de esperar, então você precisa garantir que seu app seja iniciado rapidamente. Para mostrar como uma equipe real de desenvolvimento de apps encontrou e diagnosticou problemas na inicialização de um app, veja o que a equipe do Gmail para Wear OS fez.

A equipe do Gmail para Wear OS realizou uma iniciativa de otimização, com foco principal na inicialização do app e no desempenho da renderização no tempo de execução, para atender aos critérios de desempenho da equipe. No entanto, mesmo que você não tenha limites específicos para direcionar, quase sempre haverá espaço para melhorar a inicialização do app se você dedicar algum tempo para investigar.

Capturar um rastro e analisar a inicialização do app

Para começar a análise, capture um rastro que inclua a inicialização do app para uma inspeção mais detalhada no Perfetto ou no Android Studio. Este estudo de caso usa o Perfetto porque mostra o que está acontecendo no sistema do dispositivo, além do seu app. Quando você faz upload do rastro no Perfetto, ele tem a seguinte aparência:

Figura 1. Visualização inicial do rastro no Perfetto.

Como o foco é melhorar a inicialização do app, localize a linha com a métrica personalizada Inicialização do app Android. É útil fixá-la na parte superior da visualização clicando no ícone de fixação , que aparece quando você passa o cursor sobre a linha. A barra, ou fatia, que você vê na linha Android App Startups indica o intervalo de tempo que a inicialização do app abrange, até que o primeiro frame seja exibido na tela. Portanto, procure problemas ou gargalos.

Linha "Android App Startups" com a opção de fixar em destaque.
Figura 2. Fixe a métrica personalizada "Android App Startups" na parte superior do painel para facilitar a análise.

Observe que a métrica Android App Startups representa o tempo para exibição inicial, mesmo que você esteja usando reportFullyDrawn(). Para identificar o tempo para exibição total, procure reportFullyDrawn() na caixa de pesquisa do Perfetto.

Verificar a linha de execução principal

Primeiro, examine o que está acontecendo na linha de execução principal. A linha de execução principal é muito importante porque geralmente é onde toda a renderização da interface acontece. Quando ela é bloqueada, nenhum desenho pode acontecer e seu app parece estar congelado. Portanto, é preciso garantir que nenhuma operação de longa duração esteja acontecendo na linha de execução principal.

Para encontrar a linha de execução principal, encontre e expanda a linha com o nome do pacote do app. As duas linhas com o mesmo nome do pacote (geralmente as duas primeiras linhas da seção) representam a linha de execução principal. Das duas linhas de linha de execução principais, a primeira representa o estado da CPU e a segunda representa os pontos de rastreamento. Fixe as duas linhas da linha de execução principais abaixo da métrica Android App Startups.

As linhas da linha de execução principal e as inicializações de apps Android foram fixadas.
Figura 3. Fixe as linhas da linha de execução principais abaixo da métrica personalizada "Android App Startups" para ajudar na análise.

Tempo gasto no estado executável e na contenção de CPU

Para ter uma visualização agregada da atividade da CPU durante a inicialização do app, arraste o cursor sobre a linha de execução principal para capturar o intervalo de tempo de inicialização do app. O painel Thread States que aparece mostra o tempo total gasto em cada estado da CPU dentro do intervalo de tempo selecionado.

Veja o tempo gasto no estado Runnable. Quando uma linha de execução está no estado Runnable, ela fica disponível para o trabalho, mas nenhum trabalho é programado. Isso pode indicar que o dispositivo está sobrecarregado e não consegue programar tarefas de alta prioridade. O app principal visível para o usuário tem a maior prioridade na programação. Portanto, uma linha de execução principal inativa geralmente indica que processos intensivos no app, como a renderização de animações, estão competindo com a linha de execução principal por tempo de CPU.

Linha de execução principal destacada com tempo total em estados diferentes no painel Estados da linha de execução.
Figura 4. Avalie o tempo relativo nos estados Runnable a Running para ter uma noção inicial da contenção da CPU.

Quanto maior a proporção de tempo no estado Runnable para o tempo no estado Running, maior a probabilidade de ocorrer contenção de CPU. Ao inspecionar problemas de desempenho dessa maneira, concentre-se primeiro no frame de execução mais longa e trabalhe para os menores.

Ao analisar o tempo gasto no estado Runnable, considere o hardware do dispositivo. Como o app retratado está sendo executado em um dispositivo wearable com duas CPUs, a espera-se de mais tempo gasto no estado Runnable e mais contenção de CPU com outros processos do que se estivéssemos analisando um dispositivo com mais CPUs. Embora seja gasto mais tempo no estado Runnable do que o esperado para um app típico de smartphones, isso pode ser compreensível no contexto de wearables.

Tempo gasto em OpenDexFilesFromOat*

Agora, verifique o tempo gasto no OpenDexFilesFromOat*. No trace, isso acontece ao mesmo tempo que a fração bindApplication. Essa fração representa o tempo necessário para ler os arquivos DEX do aplicativo.

Transações de vinculação bloqueadas

Em seguida, verifique as transações de vinculação. As transações de vinculação representam chamadas entre o cliente e o servidor. Nesse caso, o app (cliente) chama o sistema Android (servidor) com um binder transaction e o servidor responde com binder reply. Verifique se o app não faz transações de vinculação desnecessárias durante a inicialização, porque elas aumentam o risco de contenção de CPU. Se possível, adie o trabalho que envolve chamadas de vinculação após o período de inicialização do app. Se você precisar fazer transações de binder, elas não podem demorar mais do que a taxa de atualização de Vsync do dispositivo.

A primeira transação de vinculação, que geralmente ocorre ao mesmo tempo que a fração ActivityThreadMain, parece ser bastante longa nesse caso. Para saber mais sobre o que pode estar acontecendo, siga estas etapas:

  1. Para ver a resposta de binder associada e saber mais sobre como a transação de binder está sendo priorizada, clique na fração da transação de binder de interesse.
  2. Para conferir a resposta da vinculação, acesse o painel Seleção atual e clique em Resposta da vinculação na seção Conversas seguidas. O campo Linha de execução também informa a linha de execução em que a resposta de vinculação ocorre se você quiser navegar manualmente até ela. O processo será diferente. Será exibida uma linha que conecta a transação de vinculação e a resposta.

    Uma linha conecta a chamada de vinculação e a resposta.
    Figura 5. Identifique as transações de binder que ocorrem durante a inicialização do app e avalie se elas podem ser adiadas.
  3. Para conferir como o servidor do sistema está processando essa transação de binder, fixe as linhas de execução Cpu 0 e Cpu 1 na parte de cima da tela.

  4. Encontre os processos do sistema que processam a resposta de vinculação encontrando as frações que incluem o nome da linha de execução de resposta de vinculação. Neste caso, "Binder:687_11 [2542]". Clique nos processos relevantes do sistema para conferir mais informações sobre a transação do binder.

Confira este processo do sistema associado à transação de vinculação de interesse que ocorre na CPU 0:

Processo do sistema com o estado final "Executável (Anulado)".
Figura 6. O processo do sistema está no estado Runnable (Preempted), o que indica que ele está atrasado.

O estado final informa Runnable (Preempted), o que significa que o processo está atrasando porque a CPU está fazendo outra coisa. Para descobrir por que ele está sendo interrompido, expanda as linhas Eventos Ftrace. Na guia Ftrace Events disponível, role e procure eventos relacionados à linha de execução de vinculação de interesse "Binder:687_11 [2542]". Perto do momento em que o processo do sistema é interrompido, ocorrem dois eventos do servidor do sistema que incluem o argumento "decon", o que significa que estão relacionados ao controlador de exibição. Isso parece razoável porque o controlador de exibição coloca os frames na tela, uma tarefa importante. Deixe os eventos como estão.

Eventos do FTrace associados à transação de vinculação de interesse destacado.
Figura 7. Os eventos de FTrace indicam que a transação de binder está sendo atrasada por eventos do controlador de exibição.

Atividade JIT

Para investigar a atividade de compilação just-in-time (JIT), expanda os processos pertencentes ao app, encontre as duas linhas "Pool de linhas de execução Jit" e fixe-as na parte superior da visualização. Como esse app se beneficia dos perfis de referência durante a inicialização, pouca atividade JIT ocorre até que o primeiro frame seja renderizado, o que é indicado pelo fim da primeira chamada Choreographer.doFrame. No entanto, observe o motivo de inicialização lenta JIT compiling void, que sugere que a atividade do sistema que acontece durante o tracepoint rotulado como Application creation está causando muita atividade JIT em segundo plano. Para resolver isso, adicione os eventos que acontecem logo após o primeiro frame ser renderizado no perfil de referência expandindo a coleção de perfis até um ponto em que o app esteja pronto para ser usado. Em muitos casos, é possível fazer isso adicionando uma linha ao final do teste da biblioteca Macrobenchmark para coleta de perfis de referência. Ele aguarda até que um widget de interface específico apareça na tela, indicando que ela está totalmente preenchida.

Pools de linhas de execução Jit com a fração "Jit compile void" destacada.
Figura 8. Se você vir muita atividade JIT, expanda seu perfil de referência até que o app esteja pronto para ser usado.

Resultados

Como resultado dessa análise, a equipe do Gmail para Wear OS fez as seguintes melhorias:

  • Como eles notaram contenção durante a inicialização do app ao analisar a atividade da CPU, substituíram a animação do ícone de carregamento usada para indicar que o app está sendo carregado com uma única imagem estática. Eles também prolongaram a tela de apresentação para adiar o estado de brilho, o segundo estado de tela usado para indicar que o app está sendo carregado, para liberar recursos da CPU. Isso melhorou a latência de inicialização do app em 50%.
  • Ao analisar o tempo gasto em OpenDexFilesFromOat* e atividade JIT, foi possível ativar a reescrita do R8 dos perfis de referência. Isso melhorou a latência de inicialização do app em 20%.

Veja algumas dicas da equipe sobre como analisar o desempenho do app de forma eficiente:

  • Configure um processo contínuo que seja capaz de coletar rastros e resultados automaticamente. Configure o rastreamento automatizado para seu app usando a comparação.
  • Use o teste A/B para fazer as mudanças que você acha que podem melhorar os resultados e rejeite-as caso não melhorarem. É possível medir o desempenho em diferentes cenários usando a biblioteca Macrobenchmark.

Para saber mais, consulte os seguintes recursos: