Visão geral da avaliação do desempenho de apps

Este documento ajuda a identificar e corrigir os principais problemas de desempenho do seu app.

Principais problemas de desempenho

Há muitos problemas que podem contribuir para o desempenho ruim em um app. Confira alguns problemas comuns:

Latência de inicialização

A latência de inicialização é o tempo decorrido entre o toque no ícone do app, na notificação ou em outro ponto de entrada e a exibição dos dados do usuário na tela.

Estas são as metas de inicialização recomendadas para seus apps:

  • Inicialização a frio em menos de 500 ms. Uma inicialização a frio ocorre quando o app que está sendo iniciado não está presente na memória do sistema. Isso acontece quando é a primeira inicialização do app desde a reinicialização ou desde quando o processo do app foi interrompido pelo usuário ou pelo sistema.

    Por outro lado, uma inicialização com estado salvo ocorre quando o app já está sendo executado em segundo plano. Uma inicialização a frio exige o máximo de trabalho do sistema, já que ela precisa carregar tudo do armazenamento e inicializar o app. Tente programar a inicialização a frio com uma duração de até 500 ms.

  • Latências P95 e P99 muito próximas à latência mediana. Quando o app demora muito para iniciar, ele prejudica a experiência do usuário. As comunicações entre processos (IPCs) e a E/S desnecessária durante o caminho crítico de inicialização do app podem enfrentar contenção de bloqueios e introduzir inconsistências.

Instabilidade de rolagem

Instabilidade é o termo que descreve o problema visual que ocorre quando o sistema não consegue criar e fornecer frames a tempo de direcioná-los à tela na cadência solicitada de 60 hz ou mais. A instabilidade é mais aparente na rolagem, quando, em vez de um fluxo animado suave, podem ocorrer falhas. Ela aparece quando o movimento é pausado ao longo do caminho por um ou mais frames, já que o app leva mais tempo para renderizar conteúdo do que a duração de um frame no sistema.

O valor de taxas de atualização do app precisa ser de 90 hz. As taxas de renderização convencionais são de 60 hz, mas muitos dispositivos mais recentes operam no modo 90 hz durante interações do usuário, como a rolagem. Alguns dispositivos oferecem suporte a taxas ainda mais altas de até 120 hz.

Para conferir a taxa de atualização usada por um dispositivo em um determinado momento, ative uma sobreposição em Opções do desenvolvedor > Mostrar taxa de atualização na seção Depuração.

Transições instáveis

Isso é aparente durante interações, como ao alternar entre guias ou carregar uma nova atividade. Esses tipos de transição precisam ter animação suave, sem incluir atrasos ou oscilação visual.

Ineficiências de energia

Atividades reduzem a carga da bateria, e ações desnecessárias reduzem a duração.

As alocações de memória, provenientes da criação de novos objetos no código, podem ser o motivo de uma atividade significativa no sistema. Isso ocorre não só porque as alocações exigem esforço do Android Runtime (ART), mas liberar esses objetos mais tarde (na coleta de lixo) também exige tempo e esforço. A alocação e a coleta são muito mais rápidas e eficientes, principalmente para objetos temporários. Alocar objetos sempre que possível era uma prática recomendada, mas agora sugerimos seguir o que fizer mais sentido para seu app e sua arquitetura. Salvar em alocações com o risco de um código sem manutenção não é a prática recomendada, considerando os recursos do ART.

No entanto, isso exige esforço. Portanto, lembre-se de que isso poderá contribuir para problemas de desempenho se você estiver alocando muitos objetos no loop interno.

Identificar problemas

Recomendamos o fluxo de trabalho a seguir para identificar e corrigir problemas de desempenho:

  1. Identifique e inspecione as seguintes jornadas ideais do usuário:
    • Fluxos de inicialização comuns, incluindo os da tela de início e notificação.
    • Telas em que o usuário rola os dados.
    • Transições entre telas.
    • Fluxos de longa duração, como navegação ou reprodução de música.
  2. Inspecione o que está acontecendo durante os fluxos anteriores usando as seguintes ferramentas de depuração:
    • Perfetto: permite conferir o que está acontecendo em todo o dispositivo com dados de tempo precisos.
    • Memory Profiler: permite conferir quais alocações de memória estão acontecendo no heap.
    • Simpleperf: mostra um diagrama das chamadas de função que usam a maior parte da CPU durante um determinado período. Quando você identifica algo que está demorando muito no Systrace, mas não sabe o motivo, o Simpleperf pode fornecer mais informações.

Para entender e depurar esses problemas de desempenho, é fundamental depurar manualmente as execuções de teste individuais. Não é possível substituir as etapas anteriores analisando dados agregados. No entanto, para entender o que é realmente mostrado aos usuários e identificar quando podem ocorrer regressões, é importante configurar a coleta de métricas em testes automatizados e de campo:

  • Fluxos de inicialização
  • Instabilidade
    • Métricas de campo
      • Frames vitais no Play Console: não é possível restringir as métricas a uma jornada do usuário específica no Play Console. Ele só relata a instabilidade geral do app.
      • Medição personalizada com FrameMetricsAggregator: é possível usar FrameMetricsAggregator para registrar métricas de instabilidade durante um fluxo de trabalho específico.
    • Testes de laboratório
      • Rolagem com a Macrobenchmark.
      • A biblioteca Macrobenchmark coleta o tempo para a renderização do frame usando os comandos dumpsys gfxinfo que formam uma única jornada do usuário. Essa é uma maneira de entender a variação na instabilidade em uma jornada específica. As métricas RenderTime, que destacam o tempo que os frames estão demorando para carregar, são mais importantes que a contagem de frames instáveis para identificar regressões ou melhorias.

Links do app são links diretos baseados no URL do seu site que foram verificados como pertencentes ao seu site. Confira a seguir os motivos que podem causar falhas nas verificações de links do app.

  • Escopos de filtro de intent: adicione autoVerify somente aos filtros de intent para URLs a que seu app pode responder.
  • Trocas de protocolo não verificadas: os redirecionamentos não verificados do lado do servidor e de subdomínio são considerados riscos de segurança e falham na verificação. Eles fazem com que todos os links autoVerify falhem. Por exemplo, redirecionar links de HTTP para HTTPS, como example.com para www.example.com, sem verificar os links HTTPS pode causar uma falha na verificação. Adicione filtros de intent para verificar os links do app.
  • Links não verificáveis: adicionar links não verificáveis para fins de teste pode fazer com que o sistema não verifique os Links do app para seu app.
  • Servidores não confiáveis: verifique se os seus servidores conseguem se conectar aos seus apps clientes.

Configurar o app para análise de desempenho

É essencial fazer a configuração correta para conseguir comparações precisas, repetíveis e acionáveis de um app. Faça o teste em um sistema o mais próximo possível da produção, suprimindo fontes de ruído. As próximas seções mostram uma série de etapas específicas do APK e do sistema que você pode seguir para preparar uma configuração de teste. Algumas delas são específicas para determinados caso de uso.

Pontos de rastreamento

Os apps podem instrumentar o código com eventos de rastreamento personalizados.

Enquanto os rastros estão sendo capturados, o rastreamento incorre em uma pequena sobrecarga (aproximadamente 5 μs) por seção. Portanto, não o coloque em cada método. O rastreamento de blocos de trabalho maiores de mais de 0,1 ms pode fornecer insights significativos sobre gargalos.

Considerações sobre o APK

As variantes de depuração podem ser úteis para solucionar problemas e simbolizar amostras de pilha, mas têm impactos graves no desempenho. Dispositivos com o Android 10 (nível 29 da API) e versões mais recentes podem usar profileable android:shell="true" no manifesto para ativar a criação de perfis em builds de lançamento.

Use sua configuração de redução de código no nível da produção. Dependendo dos recursos usados pelo app, isso pode ter um impacto significativo no desempenho. Algumas configurações do ProGuard removem pontos de rastreamento. Por isso, considere remover essas regras para a configuração em que os testes serão executados.

Compilação

Compile seu app no dispositivo para um estado conhecido, geralmente speed ou speed-profile. A atividade just-in-time (JIT) em segundo plano pode ter uma sobrecarga de desempenho significativa, e você a alcança com frequência ao reinstalar o APK entre execuções de teste. Confira um comando para fazer isso:

adb shell cmd package compile -m speed -f com.google.packagename

O modo de compilação speed compila o app totalmente. O modo speed-profile compila o app de acordo com um perfil dos caminhos de código utilizados coletado durante o uso do app. Pode ser difícil coletar perfis de forma consistente e correta. Portanto, se você optar pelo uso deles, confirme se estão coletando o que você espera. Os perfis ficam no seguinte local:

/data/misc/profiles/ref/[package-name]/primary.prof

A biblioteca Macrobenchmark permite especificar diretamente o modo de compilação.

Considerações do sistema

Para medidas de baixo nível e alta fidelidade, calibre seus dispositivos. Execute comparações A/B no mesmo dispositivo e na mesma versão do SO. Podem existir variações significativas no desempenho, mesmo em dispositivos do mesmo tipo.

Em dispositivos com acesso root, use um script lockClocks para microcomparações. Entre outras coisas, esses scripts podem fazer o seguinte:

  • colocar CPUs com uma frequência fixa;
  • desativar núcleos pequenos e configurar a GPU;
  • desativar a limitação térmica.

Não recomendamos o uso de um script lockClocks para testes focados na experiência do usuário, como inicialização do app, testes DoU e testes de instabilidade, mas ele pode ser essencial para reduzir o ruído em testes de Microbenchmark.

Sempre que possível, use um framework de teste como o Macrobenchmark, que pode reduzir o ruído nas medições e evitar imprecisão.

Inicialização lenta do app: atividade trampolim desnecessária

Uma atividade trampolim pode estender o tempo de inicialização do app desnecessariamente, e é importante saber se o app está fazendo isso. Conforme mostrado no exemplo de rastro abaixo, uma activityStart é imediatamente seguido por outra activityStart, sem que nenhum frame seja renderizado pela primeira atividade.

alt_text Figura 1. Um rastro mostrando uma atividade trampolim.

Isso pode acontecer em um ponto de entrada de notificação ou em um ponto de entrada normal de inicialização do app, e muitas vezes é possível resolver o problema com a refatoração. Por exemplo, se você estiver usando essa atividade para fazer a configuração antes de executar outra atividade, fatore o código em um componente ou biblioteca reutilizável.

Alocações desnecessárias acionam GCs frequentes

Coletas de lixo (GCs, na sigla em inglês) podem acontecer com mais frequência que o esperado em um Systrace.

No exemplo abaixo, cada 10 segundos durante uma operação de longa duração são um indicador de que o app pode estar alocando de forma desnecessária, mas consistente ao longo do tempo:

alt_text Figura 2. Um rastro mostrando espaço entre eventos de GC.

Ao usar o Memory Profiler, você também pode notar que uma pilha de chamadas específica está fazendo a grande maioria das alocações. Você não precisa eliminar todas as alocações de forma agressiva, já que isso pode dificultar a manutenção do código. Em vez disso, comece trabalhando em pontos de acesso de alocações.

Frames com instabilidade

O pipeline de gráficos é relativamente complicado, e pode haver algumas nuances para determinar se um usuário pode perceber um frame descartado. Em alguns casos, a plataforma pode "resgatar" um frame usando armazenamento em buffer. No entanto, você pode ignorar a maior parte dessas nuances para identificar facilmente frames problemáticos da perspectiva do app.

Quando os frames estão sendo exibidos com pouco trabalho do app, os pontos de rastreamento Choreographer.doFrame() ocorrem em uma cadência de 16,7 ms em um dispositivo de 60 QPS:

alt_text Figura 3. Um rastro mostrando frames frequentes.

Se você diminuir o zoom e navegar pelo rastro, vai notar que os frames demoram um pouco para serem concluídos, mas tudo bem, porque eles não estão levando mais que 16,7 ms:

alt_text Figura 4. Um rastro mostrando frames rápidos frequentes com bursts de trabalho periódicos.

Quando há uma interrupção nessa cadência regular, o problema é um frame instável, como mostrado na Figura 5:

alt_text Figura 5. Um rastro mostrando um frame instável.

Pratique a identificação deles.

alt_text Figura 6. Um rastro mostrando mais frames instáveis.

Em alguns casos, você precisa aumentar o zoom em um ponto de rastreamento para conferir mais informações sobre quais visualizações estão infladas ou o que RecyclerView está fazendo. Em outros, pode ser necessário inspecionar mais.

Para saber mais sobre como identificar frames instáveis e depurar os motivos, consulte Renderização lenta.

Erros comuns do RecyclerView

Invalidar todos os dados de apoio de RecyclerView desnecessariamente pode levar a instabilidade e longos tempos de renderização de frame. Em vez disso, para minimizar o número de visualizações que precisam ser atualizadas, invalide apenas os dados que mudam.

Consulte Apresentar dados dinâmicos para saber como evitar chamadas caras de notifyDatasetChanged(), que fazem com que o conteúdo seja atualizado em vez de totalmente substituído.

Se você não oferecer suporte a cada RecyclerView aninhado corretamente, o RecyclerView interno poderá ser totalmente recriado todas as vezes. Cada RecyclerView interno aninhado precisa ter um RecycledViewPool definido para garantir que as visualizações possam ser recicladas entre cada RecyclerView interno.

Não fazer a pré-busca de dados suficientes ou não fazer a pré-busca em tempo hábil pode tornar o acesso ao fim de uma lista de rolagem desagradável quando o usuário precisa esperar mais dados do servidor. Embora isso não seja tecnicamente uma instabilidade, já que nenhum prazo de frame está sendo perdido, é possível melhorar significativamente a UX modificando o tempo e a quantidade de pré-busca para que o usuário não tenha que esperar os dados.

Depurar o app

Confira a seguir métodos diferentes para depurar o desempenho do app. Assista o vídeo a seguir para ter uma visão geral do rastreamento do sistema e como usar o criador de perfil do Android Studio.

Depurar a inicialização do app com o Systrace

Consulte Tempo de inicialização do app para ter uma visão geral desse processo. Assista ao vídeo abaixo para mais informações sobre o rastreamento do sistema.

É possível eliminar a ambiguidade dos tipos de inicialização nos estágios a seguir:

  • Inicialização a frio: comece pela criação de um novo processo sem estado salvo.
  • Inicialização com estado salvo: recria a atividade ao reutilizar o processo ou recria o processo com o estado salvo.
  • Inicialização a quente: reinicia a atividade e começa na inflação.

Recomendamos a captura de Systraces com o app Rastreamento do sistema no dispositivo. No Android 10 e versões mais recentes, use o Perfetto. Para o Android 9 e versões anteriores, use o Systrace. Também recomendamos a visualização de arquivos de rastreamento com o visualizador de rastros baseado na Web do Perfetto (link em inglês). Para mais informações, consulte Visão geral do rastreamento do sistema.

Verifique se:

  • Contenção de monitoramento: a concorrência por recursos protegidos pelo monitor pode introduzir atraso significativo na inicialização do app.
  • Transações de binder síncronas: procure transações desnecessárias no caminho crítico do app. Se uma transação necessária for cara, trabalhe com a equipe da plataforma associada para fazer melhorias.

  • GC simultânea: esse é um impacto comum e relativamente baixo. No entanto, se você tiver esse problema com frequência, considere analisar com o Memory Profiler do Android Studio.

  • E/S: verifique se a E/S foi realizada durante a inicialização e procure pausas longas.

  • Atividade significativa em outras linhas de execução: pode interferir na linha de execução de interface. Portanto, tenha cuidado com trabalhos em segundo plano durante a inicialização.

Recomendamos chamar reportFullyDrawn quando a inicialização for concluída da perspectiva do app para melhorar os relatórios de métricas de inicialização. Consulte a seção Tempo para exibição total para mais informações sobre o uso de reportFullyDrawn. É possível extrair os horários de início definidos pelo RFD usando o processador de rastreamento Perfetto, e um evento de rastreamento visível ao usuário é emitido.

Usar o Rastreamento do sistema no dispositivo

Você pode usar o app de nível do sistema chamado Rastreamento do sistema para capturar um rastro do sistema em um dispositivo. Esse app permite gravar rastros do dispositivo sem precisar conectá-lo à fonte de energia ou ao adb.

Usar o Memory Profiler do Android Studio

Use o Memory Profiler do Android Studio para inspecionar a pressão de memória que pode ser causada por vazamentos de memória ou padrões de uso inadequados. Ele mostra as alocações de objetos em tempo real.

Você pode corrigir problemas de memória no seu app seguindo as informações do Memory Profiler para rastrear o motivo e a frequência das GCs.

Para criar o perfil da memória do app, siga estas etapas:

  1. Detectar problemas de memória.

    Grave uma sessão de criação de perfil de memória da jornada do usuário em que você quer se concentrar. Procure uma contagem crescente de objetos, conforme mostrado na Figura 7, que acaba levando a GCs, conforme mostrado na Figura 8.

    alt_text Figura 7. Aumentando a contagem de objetos.

    alt_text Figura 8. Coletas de lixo.

    Depois de identificar a jornada do usuário que está aumentando a pressão sobre a memória, analise as causas raiz da pressão.

  2. Diagnostique pontos de acesso de pressão sobre a memória.

    Selecione um intervalo na linha do tempo para visualizar Alocações e Tamanho superficial, conforme mostrado na Figura 9.

    alt_text Figura 9. Valores de Allocations e Shallow Size.

    Há várias maneiras de ordenar esses dados. Confira abaixo alguns exemplos de como cada visualização pode ajudar a analisar problemas.

    • Arrange by class: útil quando você quer encontrar classes que geram objetos que, de outra forma, são armazenados em cache ou reutilizados de um pool de memória.

      Por exemplo, se um app criar 2.000 objetos de classe chamados "Vertex" a cada segundo, ele vai aumentar a contagem de Allocations em 2.000 a cada segundo, e essa informação vai aparecer na classificação por classe. Se você quiser reutilizar esses objetos para evitar a geração de lixo, implemente um pool de memória.

    • Arrange by callstack: útil quando você quer descobrir onde há um hot path em que a memória está sendo alocada, como dentro de um loop ou de uma função específica que realiza muito trabalho de alocação.

    • Shallow size: rastreia somente a memória do próprio objeto. Ele é útil para rastrear classes simples compostas principalmente apenas de valores primitivos.

    • RetainedSize: mostra a memória total devido ao objeto e as referências que fazem referência apenas ao objeto. É útil para rastrear a pressão da memória devido a objetos complexos. Para conseguir esse valor, faça um despejo de memória completo, conforme mostrado na Figura 10, e Retained Tamanho como uma coluna, conforme mostrado na Figura 11.

      alt_text Figura 10. Despejo de memória cheio.

      Coluna "Tamanho retido".
      Figura 11. Coluna "Retenção mantida".
  3. Medir o impacto de uma otimização.

    As GCs são mais evidentes e mais fáceis de medir o impacto das otimizações de memória. Quando uma otimização reduz a pressão da memória, há menos GCs.

    Para medir o impacto da otimização, meça o tempo entre as GCs na linha do tempo do criador de perfil. É possível perceber que o intervalo entre as GCs demora mais.

    Os impactos finais das melhorias de memória são os seguintes:

    • Os desligamentos por falta de memória provavelmente serão reduzidos se o app não atingir constantemente a pressão de memória.
    • Ter menos GCs melhora as métricas de instabilidade, especialmente na P99. Isso ocorre porque as GCs causam contenção de CPU, o que pode adiar tarefas de renderização durante a GC.