Desenvolvimento

9 mai, 2018

Apresentando o Nanoscope: uma ferramenta de rastreamento de método extremamente precisa para Android

Publicidade

Em outubro do ano passado, a equipe de Mobile Engineering da Uber deu início a um esforço para melhorar o desempenho dos aplicativos. Até agora obtivemos grandes avanços com acelerações de mais de 50% em algumas de nossas principais transições. No início, aprendemos que certas classes de problemas de desempenho são triviais para a causa raiz. É fácil identificar I/O na thread principal, por exemplo. Nesses casos, encontramos ferramentas convencionais de perfil de desempenho do Android suficientes para depuração.

Investigações mais complexas, no entanto, foram por vezes inconclusivas devido a dados incompletos ou imprecisos. Algumas perguntas que inicialmente nos esforçamos para responder, incluíam: “Por que certas animações demoram a começar?” e “Por que a inflação do TextView é lenta em alguns casos?”. Depois de termos limitações no gerenciador de perfil de CPU do Android Studio, construímos o Nanoscope, uma ferramenta interna para nos fornecer o melhor rastreamento de método.

Desde a implementação do nosso protótipo inicial, usamos e fazemos iterações no rastreador interno e agora podemos depurar com confiança esses problemas difíceis de desempenho. Entre outras descobertas, descobrimos que a criação de camadas de hardware de animação é mais cara do que o esperado, e que o autodimensionamento de texto do TextView é muito mais lento se você não usar a granularidade.

Nosso rastreador de métodos internos continua a nos fornecer uma visão sem precedentes sobre o desempenho de nossos aplicativos. Por isso, decidimos compartilhar essa ferramenta com o restante da comunidade Android. E no final de abril lançamos o Nanoscope: uma ferramenta de rastreamento de métodos extremamente precisa para Android.

1. Use o comando nanoscope para iniciar o rastreamento. 2. Explore o flamegraph no visualizador do Nanoscope.

Motivação

Entendemos o valor de alavancar as ferramentas existentes e acreditamos que novas ferramentas merecem justificativa completa, por isso, antes de nos aprofundarmos no funcionamento do Nanoscope, veremos as ferramentas de desempenho do Android Studio e onde elas ficaram aquém do esperado.

Método de rastreamento do Android Studio

Como o Nanoscope, o Android Studio fornece funcionalidade de rastreamento de método. O principal bloqueio para nós foi a sobrecarga de desempenho significativa, introduzida pela instrumentação de rastreio de método do Android Studio.

Esquerda: rastreamento do Nanoscpe. Direita: rastreamento do método do Android Studio.
Tempo do clique ao começo da animação.

Algumas de nossas principais transições tiveram várias ordens de magnitude mais lentas com o rastreamento de método do Android Studio ativado. Qualquer rastreamento de método retardará o desempenho do tempo de execução em uma determinada quantia devido à lógica extra de log, porém, nesse nível de distorção, os perfis de desempenho resultantes não eram mais uma representação precisa do uso normal do aplicativo e não eram úteis para nossas investigações de desempenho.

Amostragem do método Android Studio

Além do rastreamento de método, o Android Studio oferece a amostragem de métodos como uma alternativa que promete um impacto significativamente reduzido no desempenho do tempo de execução. Testamos esse recurso, e é possível fazer uma amostragem com pouca sobrecarga configurando a frequência de amostragem, mas ela possui desvantagens. Em frequências mais baixas, menos medições são feitas e, portanto, a sobrecarga total é reduzida ao custo da precisão, conforme ilustrado na Figura 1:

Figura 1: Amostragem de baixa frequência

O aplicativo funciona sem problemas nesse caso, mas o rastreamento não possui muitos detalhes importantes. Frequências mais altas produzem um rastreamento mais completo, mas requerem mais medições, aumentando o impacto no desempenho, mostrado na Figura 2:

Figura 2: Amostragem de alta frequência

Com um loop de feedback de dados de produção de cerca de três semanas, é importante que tenhamos um entendimento preciso do perfil de desempenho de nosso código para não descobrirmos que três semanas depois de corrigir um problema, ele na verdade não foi resolvido ou as coisas pioraram. Descobrimos que, em qualquer frequência, a amostragem do método do Android Studio não tinha os detalhes nem a precisão que precisávamos. Nesse ponto, começamos a nos perguntar se era possível ter os dois.

Design do Nanoscope

Antes de construir um protótipo, a primeira decisão a tomar foi se nossa ferramenta seria rastreada ou baseada em amostras. Para evitar qualquer preocupação com dados incompletos, chegamos a uma implementação baseada em rastreio. Também teorizamos que uma ferramenta de amostragem ideal seria menos eficiente do que uma ferramenta de rastreamento ideal, pois cada amostra precisa percorrer toda a pilha, enquanto uma medição de rastreio simplesmente registra o identificador do método atual.

A solução que chegamos foi uma ferramenta baseada em rastreios, extremamente baixa, que poderia nos fornecer os detalhes e a precisão necessários para depurar com confiança nossos problemas de desempenho. Os primeiros resultados de um protótipo foram promissores e nos encorajaram a continuar o trabalho sobre o que eventualmente se tornaria o Nanoscope.

O nível de desempenho que atingimos com o Nanoscope depende da integração profunda com o sistema operacional. Para conseguir isso, implementamos o Nanoscope como um fork do Android Open Source Project (AOSP). Essa estratégia também serve como a maior barreira à entrada do Nanoscope para os usuários, pois exige um dispositivo que executa o sistema operacional personalizado. Porém, com controle total sobre o sistema operacional, nossa estratégia é bastante simples:

1. Alocar um array para manter nossos dados de rastreamento.
2. Na entrada do método:

  • Escrever o timestamp e o ponteiro de método para indicar um push para a pilha de chamadas

3. Na saída do método:

  • Gravar o timestamp e um ponteiro nulo para indicar um pop da pilha de chamadas

Interpretador

Nossa primeira tarefa foi instrumentar o interpretador. Felizmente, todos os métodos executados pelo interpretador fluem através de um único método:

static inline JValue Execute(Thread* self, ...) {
  …
  self->TraceStart(method);
  … // Method execution logic
  self->TraceEnd(method);
  …
}

Adicionamos os métodos TraceStart e TraceEnd para realizar o levantamento mínimo do registro de nossos dados de rastreamento:

void Thread::TraceStart(ArtMethod* method) {
  if (LIKELY(tlsPtr_.trace_data_ptr != nullptr)) {
    *tlsPtr_.trace_data_ptr++ = reinterpret_cast<int64_t>(method);
    *tlsPtr_.trace_data_ptr++ = generic_timer_count();
  }
}

void Thread::TraceEnd(ArtMethod* method) {
  if (LIKELY(tlsPtr_.trace_data_ptr != nullptr)) {
    *tlsPtr_.trace_data_ptr++ = 0;
    *tlsPtr_.trace_data_ptr++ = generic_timer_count();
  }
}

Primeiro, determinamos se o rastreamento está ativado para a thread, verificando se o array de dados de rastreamento existe. Em seguida, escrevemos um identificador de método (ou um nullptr para um pop), seguido pelo timestamp atual, que é recuperado diretamente de um registrador de timer para um desempenho ideal.

Compilador

Nem todos os métodos Java são executados pelo interpretador. Alguns métodos são compilados JIT ou AOT nas instruções da máquina e executados diretamente. Nesses casos, poderíamos gerar uma chamada para nossos métodos TraceStart/TraceEnd, mas evitamos um salto ao inserir as instruções de montagem equivalentes no início e no final de cada método compilado. Abaixo está a montagem de 64 bits que geramos para a entrada do método:

; TR = thread register
; MR = method register
; data_ptr = temp register
; timestamp = temp register

VisitTraceStart:
    LDR data_ptr, [TR, data_ptr_offset]  ; data_ptr = thread.data_ptr
    CBZ data_ptr, done                   ; if (data_ptr != null)
    MRS timestamp, SYS_CNTVCT_EL0        ;     timestamp = <Generic Timer count>
    STR MR, [data_ptr], +8               ;     *data_ptr++ = method
    STR timestamp, [data_ptr], +8        ;     *data_ptr++ = timestamp
    STR data_ptr, [TR, data_ptr_offset]  ;     *thread.data_ptr = data_ptr
  done:

Nós geramos instruções semelhantes para saídas de métodos e também incluímos suporte para o compilador de 32 bits.

Resultados

Ficamos obcecados em minimizar a lógica executada por método e estamos muito orgulhosos dos resultados. Durante o rastreamento, o Nanoscope introduz apenas 20 nanossegundos de sobrecarga por método e menos de 10% de sobrecarga total em nossa sequência de inicialização. Além das descobertas mencionadas anteriormente, aqui estão alguns exemplos de problemas de desempenho que agora entendemos em profundidade, graças ao Nanoscope:

  • Os WebViews demoram para inicializar apenas pela primeira vez devido à inicialização do Chromium.
  • Grande parte do tempo gasto na inicialização do Google Maps é devido ao carregamento de classe.
  • MenuView realiza religação/layout/inflação em cada evento de clique (issue do GitHub).

A precisão do Nanoscope também tornou mais fácil categorizar o comportamento do desempenho localmente, em vez de confiar nas médias das medições de produção. Agora podemos responder rapidamente às seguintes perguntas, por exemplo:

  • Qual porcentagem da transição é gasta em operações relacionadas ao View?
  • Qual porcentagem da transição é atribuída às nossas bibliotecas de plataforma?
  • Qual porcentagem da transição é gasta dentro do RxJava?

Desde que começamos a usar o Nanoscope, dados distorcidos ou ausentes não são mais um obstáculo para a depuração de problemas de desempenho do Android na Uber.

Próximos passos

Se você quiser saber mais sobre a arquitetura, confira o wiki e, se for interessante para melhorar o desempenho do seu aplicativo, considere dar uma chance ao Nanoscope.

Há muitos outros problemas interessantes para resolver na Uber e estamos contratando!

***

Este artigo é do Uber Engineering. Ele foi escrito por Leland Takamine e Brian Attwell. A tradução foi feita pela Redação iMasters com autorização. Você pode conferir o original em: https://eng.uber.com/nanoscope/