Identificando Jank relacionado ao Jitter

Jitter é o comportamento aleatório do sistema que impede a execução de trabalho perceptível. Esta página descreve como identificar e resolver problemas de instabilidade relacionados ao jitter.

Atraso no agendador de threads do aplicativo

O atraso do agendador é o sintoma mais óbvio de jitter: um processo que deveria ser executado torna-se executável, mas não é executado por um período de tempo significativo. A importância do atraso varia de acordo com o contexto. Por exemplo:

  • Um thread auxiliar aleatório em um aplicativo provavelmente pode ser atrasado por muitos milissegundos sem problemas.
  • O thread de UI de um aplicativo pode tolerar de 1 a 2 ms de instabilidade.
  • Os kthreads do driver executados como SCHED_FIFO podem causar problemas se puderem ser executados por 500us antes da execução.

Os tempos executáveis ​​podem ser identificados no systrace pela barra azul que precede um segmento em execução de um thread. Um tempo executável também pode ser determinado pelo período de tempo entre o evento sched_wakeup de um thread e o evento sched_switch que sinaliza o início da execução do thread.

Tópicos que demoram muito

Threads de interface do usuário do aplicativo que podem ser executados por muito tempo podem causar problemas. Threads de nível inferior com tempos de execução longos geralmente têm causas diferentes, mas tentar levar o tempo de execução do thread de UI para zero pode exigir a correção de alguns dos mesmos problemas que fazem com que threads de nível inferior tenham tempos de execução longos. Para mitigar atrasos:

  1. Use cpusets conforme descrito em Aceleração térmica .
  2. Aumente o valor CONFIG_HZ.
    • Historicamente, o valor foi definido como 100 nas plataformas arm e arm64. No entanto, isso é um acidente histórico e não é um bom valor para uso em dispositivos interativos. CONFIG_HZ=100 significa que um instante tem 10ms de duração, o que significa que o balanceamento de carga entre CPUs pode levar 20ms (dois instantes) para acontecer. Isso pode contribuir significativamente para a instabilidade em um sistema carregado.
    • Dispositivos recentes (Nexus 5X, Nexus 6P, Pixel e Pixel XL) foram enviados com CONFIG_HZ=300. Isto deve ter um custo de energia insignificante, ao mesmo tempo que melhora significativamente os tempos de execução. Se você observar aumentos significativos no consumo de energia ou problemas de desempenho após alterar CONFIG_HZ, é provável que um dos seus drivers esteja usando um temporizador baseado em instantes brutos em vez de milissegundos e convertendo para instantes. Geralmente, essa é uma solução fácil (consulte o patch que corrigiu problemas de temporizador kgsl no Nexus 5X e 6P ao converter para CONFIG_HZ = 300).
    • Por fim, experimentamos CONFIG_HZ=1000 no Nexus/Pixel e descobrimos que ele oferece desempenho notável e redução de energia devido à diminuição da sobrecarga do RCU.

Somente com essas duas alterações, um dispositivo deve parecer muito melhor para o tempo de execução do thread da UI sob carga.

Usando sys.use_fifo_ui

Você pode tentar zerar o tempo executável do thread da UI definindo a propriedade sys.use_fifo_ui como 1.

Aviso : não use esta opção em configurações de CPU heterogêneas, a menos que você tenha um agendador RT com reconhecimento de capacidade. E, neste momento, NENHUM PROGRAMADOR DE RT DE ENVIO ATUAL ESTÁ CONSCIENTE DA CAPACIDADE . Estamos trabalhando em um para EAS, mas ainda não está disponível. O agendador RT padrão é baseado puramente nas prioridades RT e se uma CPU já possui um thread RT de prioridade igual ou superior.

Como resultado, o agendador RT padrão moverá alegremente seu thread de UI de execução relativamente longa de um grande núcleo de alta frequência para um pequeno núcleo com frequência mínima se um FIFO kthread de prioridade mais alta acordar no mesmo grande núcleo. Isto irá introduzir regressões de desempenho significativas . Como esta opção ainda não foi usada em um dispositivo Android enviado, se você quiser usá-la, entre em contato com a equipe de desempenho do Android para ajudá-lo a validá-la.

Quando sys.use_fifo_ui está habilitado, ActivityManager rastreia o thread de UI e RenderThread (os dois threads mais críticos para UI) do aplicativo principal e torna esses threads SCHED_FIFO em vez de SCHED_OTHER. Isso elimina efetivamente o jitter da UI e do RenderThreads; os rastreamentos que reunimos com esta opção habilitada mostram tempos de execução na ordem de microssegundos em vez de milissegundos.

No entanto, como o balanceador de carga RT não tinha reconhecimento de capacidade, houve uma redução de 30% no desempenho de inicialização do aplicativo porque o thread de UI responsável pela inicialização do aplicativo seria movido de um núcleo Kryo dourado de 2,1 GHz para um núcleo Kryo prateado de 1,5 GHz. . Com um balanceador de carga RT com reconhecimento de capacidade, vemos desempenho equivalente em operações em massa e uma redução de 10 a 15% nos tempos de quadro dos percentis 95 e 99 em muitos de nossos benchmarks de UI.

Interromper o tráfego

Como as plataformas ARM fornecem interrupções apenas para a CPU 0 por padrão, recomendamos o uso de um balanceador IRQ (irqbalance ou msm_irqbalance nas plataformas Qualcomm).

Durante o desenvolvimento do Pixel, vimos erros que poderiam ser atribuídos diretamente à sobrecarga da CPU 0 com interrupções. Por exemplo, se o encadeamento mdss_fb0 foi planejado na CPU 0, havia uma probabilidade muito maior de instabilidade devido a uma interrupção que é acionada pela exibição quase imediatamente antes da varredura. mdss_fb0 estaria no meio de seu próprio trabalho com um prazo muito apertado e então perderia algum tempo para o manipulador de interrupção do MDSS. Inicialmente, tentamos corrigir isso definindo a afinidade da CPU do thread mdss_fb0 para as CPUs 1-3 para evitar contenção com a interrupção, mas então percebemos que ainda não havíamos habilitado o msm_irqbalance. Com msm_irqbalance ativado, o jank foi visivelmente melhorado mesmo quando mdss_fb0 e a interrupção MDSS estavam na mesma CPU devido à redução da contenção de outras interrupções.

Isso pode ser identificado no systrace observando a seção sched e também a seção irq. A seção sched mostra o que foi agendado, mas uma região sobreposta na seção irq significa que uma interrupção está sendo executada durante esse período, em vez do processo normalmente agendado. Se você observar períodos significativos de tempo gastos durante uma interrupção, suas opções incluem:

  • Torne o manipulador de interrupções mais rápido.
  • Evite que a interrupção aconteça em primeiro lugar.
  • Altere a frequência da interrupção para ficar fora de fase com outro trabalho regular no qual ela possa estar interferindo (se for uma interrupção regular).
  • Defina a afinidade da CPU da interrupção diretamente e evite que ela seja balanceada.
  • Defina a afinidade da CPU do thread no qual a interrupção está interferindo para evitar a interrupção.
  • Confie no balanceador de interrupção para mover a interrupção para uma CPU menos carregada.

Definir a afinidade da CPU geralmente não é recomendado, mas pode ser útil em casos específicos. Em geral, é muito difícil prever o estado do sistema para as interrupções mais comuns, mas se você tiver um conjunto muito específico de condições que aciona certas interrupções onde o sistema está mais restrito que o normal (como VR), a afinidade explícita da CPU pode ser uma boa solução.

Softirqs longos

Enquanto um softirq está em execução, ele desativa a preempção. softirqs também podem ser acionados em vários locais do kernel e podem ser executados dentro de um processo do usuário. Se houver atividade softirq suficiente, os processos do usuário pararão de executar softirqs e o ksoftirqd será ativado para executar softirqs e ter carga balanceada. Normalmente, isso está bem. No entanto, um único softirq muito longo pode causar estragos no sistema.


softirqs são visíveis na seção IRQ de um rastreamento, portanto, são fáceis de detectar se o problema puder ser reproduzido durante o rastreamento. Como um softirq pode ser executado dentro de um processo de usuário, um softirq ruim também pode se manifestar como tempo de execução extra dentro de um processo de usuário sem motivo óbvio. Se você vir isso, verifique a seção irq para ver se os softirqs são os culpados.

Drivers deixando a preempção ou IRQs desativados por muito tempo

Desativar a preempção ou interrupções por muito tempo (dezenas de milissegundos) resulta em instabilidade. Normalmente, o jank se manifesta como um thread que se torna executável, mas não está em execução em uma CPU específica, mesmo que o thread executável tenha prioridade significativamente mais alta (ou SCHED_FIFO) do que o outro thread.

Algumas diretrizes:

  • Se o thread executável for SCHED_FIFO e o thread em execução for SCHED_OTHER, o thread em execução terá preempção ou interrupções desabilitadas.
  • Se o thread executável tiver prioridade significativamente mais alta (100) do que o thread em execução (120), o thread em execução provavelmente terá preempção ou interrupções desabilitadas se o thread executável não for executado dentro de dois instantes.
  • Se o thread executável e o thread em execução tiverem a mesma prioridade, o thread em execução provavelmente terá preempção ou interrupções desabilitadas se o thread executável não for executado dentro de 20 ms.

Lembre-se de que executar um manipulador de interrupções impede que você atenda a outras interrupções, o que também desativa a preempção.


Outra opção para identificar regiões ofensivas é com o rastreador preemptirqsoff (consulte Usando ftrace dinâmico ). Este rastreador pode fornecer uma visão muito maior sobre a causa raiz de uma região ininterrupta (como nomes de funções), mas requer um trabalho mais invasivo para ser habilitado. Embora possa ter um impacto maior no desempenho, definitivamente vale a pena tentar.

Uso incorreto de filas de trabalho

Os manipuladores de interrupção geralmente precisam realizar um trabalho que pode ser executado fora de um contexto de interrupção, permitindo que o trabalho seja distribuído para diferentes threads no kernel. Um desenvolvedor de driver pode perceber que o kernel possui uma funcionalidade de tarefa assíncrona muito conveniente para todo o sistema, chamada filas de trabalho , e pode usá-la para trabalhos relacionados a interrupções.

No entanto, as filas de trabalho são quase sempre a resposta errada para este problema porque são sempre SCHED_OTHER. Muitas interrupções de hardware estão no caminho crítico de desempenho e devem ser executadas imediatamente. As filas de trabalho não têm garantias sobre quando serão executadas. Cada vez que vimos uma fila de trabalho no caminho crítico de desempenho, ela foi uma fonte de erros esporádicos, independentemente do dispositivo. No Pixel, com um processador principal, vimos que uma única fila de trabalho poderia ser atrasada em até 7 ms se o dispositivo estivesse sob carga, dependendo do comportamento do agendador e de outras coisas em execução no sistema.

Em vez de uma fila de trabalho, os drivers que precisam lidar com trabalho semelhante a interrupção dentro de um thread separado devem criar seu próprio kthread SCHED_FIFO. Para obter ajuda para fazer isso com funções kthread_work, consulte este patch .

Contenção de bloqueio de estrutura

A contenção de bloqueio de estrutura pode ser uma fonte de instabilidade ou outros problemas de desempenho. Geralmente é causado pelo bloqueio ActivityManagerService, mas também pode ser visto em outros bloqueios. Por exemplo, o bloqueio PowerManagerService pode afetar o desempenho da tela. Se você está vendo isso no seu dispositivo, não há uma boa solução porque só pode ser melhorado por meio de melhorias arquitetônicas na estrutura. No entanto, se você estiver modificando o código executado dentro do system_server, é fundamental evitar reter bloqueios por muito tempo, especialmente o bloqueio ActivityManagerService.

Contenção de bloqueio de fichário

Historicamente, o fichário teve um único bloqueio global. Se o encadeamento que executa uma transação de fichário foi interrompido enquanto mantinha o bloqueio, nenhum outro encadeamento poderá executar uma transação de fichário até que o encadeamento original tenha liberado o bloqueio. Isto é mau; a contenção do fichário pode bloquear tudo no sistema, incluindo o envio de atualizações da interface do usuário para o display (threads da interface do usuário se comunicam com o SurfaceFlinger por meio do fichário).

O Android 6.0 incluiu vários patches para melhorar esse comportamento, desativando a preempção enquanto mantém o bloqueio do fichário. Isso era seguro apenas porque o bloqueio do fichário deveria ser mantido por alguns microssegundos do tempo de execução real. Isso melhorou drasticamente o desempenho em situações não contestadas e evitou a contenção, impedindo a maioria das alternâncias do agendador enquanto o bloqueio do fichário era mantido. No entanto, a preempção não pôde ser desabilitada durante todo o tempo de execução de retenção do bloqueio do fichário, o que significa que a preempção foi habilitada para funções que poderiam dormir (como copy_from_user), o que poderia causar a mesma preempção que o caso original. Quando enviamos os patches para o upstream, eles prontamente nos disseram que essa era a pior ideia da história. (Concordamos com eles, mas também não podíamos contestar a eficácia dos patches na prevenção de erros.)

contenção fd dentro de um processo

Isso é raro. Sua instabilidade provavelmente não é causada por isso.

Dito isto, se você tiver vários threads dentro de um processo escrevendo o mesmo fd, é possível ver contenção neste fd, no entanto, a única vez que vimos isso durante a inicialização do Pixel foi durante um teste em que threads de baixa prioridade tentaram ocupar toda a CPU tempo enquanto um único thread de alta prioridade estava em execução no mesmo processo. Todos os threads estavam gravando no marcador de rastreamento fd e o thread de alta prioridade poderia ser bloqueado no marcador de rastreamento fd se um thread de baixa prioridade estivesse segurando o bloqueio fd e fosse então interrompido. Quando o rastreamento foi desabilitado nos threads de baixa prioridade, não houve problema de desempenho.

Não conseguimos reproduzir isso em nenhuma outra situação, mas vale a pena apontar como uma possível causa de problemas de desempenho durante o rastreamento.

Transições desnecessárias de CPU ociosa

Ao lidar com IPC, especialmente pipelines de vários processos, é comum ver variações no seguinte comportamento de tempo de execução:

  1. O thread A é executado na CPU 1.
  2. O thread A acorda o thread B.
  3. Thread B começa a rodar na CPU 2.
  4. O thread A adormece imediatamente, para ser despertado pelo thread B quando o thread B terminar seu trabalho atual.

Uma fonte comum de sobrecarga está entre as etapas 2 e 3. Se a CPU 2 estiver ociosa, ela deverá ser trazida de volta para um estado ativo antes que o thread B possa ser executado. Dependendo do SOC e da profundidade da inatividade, isso pode levar dezenas de microssegundos antes que o thread B comece a ser executado. Se o tempo de execução real de cada lado do IPC estiver próximo o suficiente da sobrecarga, o desempenho geral desse pipeline poderá ser significativamente reduzido pelas transições de inatividade da CPU. O lugar mais comum para o Android atingir isso é em torno de transações de fichário, e muitos serviços que usam fichário acabam se parecendo com a situação descrita acima.

Primeiro, use a função wake_up_interruptible_sync() em seus drivers de kernel e suporte isso em qualquer agendador personalizado. Trate isso como um requisito, não como uma dica. O Binder usa isso hoje e ajuda muito nas transações síncronas do binder, evitando transições desnecessárias de CPU ociosa.

Em segundo lugar, certifique-se de que os tempos de transição da CPU sejam realistas e que o governador da CPU os esteja levando em consideração corretamente. Se o seu SOC estiver entrando e saindo do estado ocioso mais profundo, você não economizará energia indo para o estado ocioso mais profundo.

Exploração madeireira

O registro não é gratuito para ciclos de CPU ou memória, portanto, não envie spam para o buffer de registro. Registrar ciclos de custos em seu aplicativo (diretamente) e no daemon de log. Remova todos os registros de depuração antes de enviar seu dispositivo.

Problemas de E/S

As operações de E/S são fontes comuns de jitter. Se um thread acessar um arquivo mapeado na memória e a página não estiver no cache de páginas, ele falhará e lerá a página do disco. Isso bloqueia o thread (geralmente por mais de 10 ms) e, se acontecer no caminho crítico da renderização da IU, pode resultar em instabilidade. Há muitas causas de operações de E/S para discutir aqui, mas verifique os seguintes locais ao tentar melhorar o comportamento de E/S:

  • PinnerService . Adicionado no Android 7.0, o PinnerService permite que a estrutura bloqueie alguns arquivos no cache da página. Isso remove a memória para uso por qualquer outro processo, mas se houver alguns arquivos que são conhecidos a priori por serem usados ​​regularmente, pode ser eficaz bloquear esses arquivos.

    Em dispositivos Pixel e Nexus 6P com Android 7.0, bloqueamos quatro arquivos:
    • /system/framework/arm64/boot-framework.oat
    • /system/framework/oat/arm64/services.odex
    • /system/framework/arm64/boot.oat
    • /system/framework/arm64/boot-core-libart.oat
    Esses arquivos são constantemente usados ​​pela maioria dos aplicativos e pelo system_server, portanto, não devem ser paginados. Em particular, descobrimos que se algum deles for paginado, ele será paginado novamente e causará instabilidade ao mudar de um aplicativo pesado.
  • Criptografia . Outra possível causa de problemas de E/S. Descobrimos que a criptografia inline oferece o melhor desempenho quando comparada à criptografia baseada em CPU ou ao uso de um bloco de hardware acessível via DMA. Mais importante ainda, a criptografia em linha reduz o jitter associado à E/S, especialmente quando comparada à criptografia baseada em CPU. Como as buscas no cache da página geralmente estão no caminho crítico da renderização da UI, a criptografia baseada em CPU introduz carga adicional de CPU no caminho crítico, o que adiciona mais instabilidade do que apenas a busca de E/S.

    Os mecanismos de criptografia de hardware baseados em DMA têm um problema semelhante, pois o kernel precisa gastar ciclos gerenciando esse trabalho, mesmo que outro trabalho crítico esteja disponível para execução. Recomendamos fortemente que qualquer fornecedor de SOC que construa novo hardware inclua suporte para criptografia em linha.

Embalagem agressiva de pequenas tarefas

Alguns agendadores oferecem suporte para empacotar pequenas tarefas em núcleos de CPU únicos para tentar reduzir o consumo de energia, mantendo mais CPUs ociosas por mais tempo. Embora isso funcione bem para a taxa de transferência e o consumo de energia, pode ser catastrófico para a latência. Existem vários threads de curta duração no caminho crítico da renderização da UI que podem ser considerados pequenos; se esses threads forem atrasados ​​​​à medida que são migrados lentamente para outras CPUs, isso causará instabilidade. Recomendamos o uso de empacotamento para pequenas tarefas de maneira muito conservadora.

Destruição do cache da página

Um dispositivo sem memória livre suficiente pode ficar extremamente lento repentinamente ao executar uma operação de longa duração, como abrir um novo aplicativo. Um rastreamento do aplicativo pode revelar que ele está consistentemente bloqueado na E/S durante uma execução específica, mesmo quando muitas vezes não está bloqueado na E/S. Isso geralmente é um sinal de sobrecarga no cache da página, especialmente em dispositivos com menos memória.

Uma maneira de identificar isso é pegar um systrace usando a tag pagecache e alimentar esse rastreamento para o script em system/extras/pagecache/pagecache.py . pagecache.py traduz solicitações individuais para mapear arquivos no cache da página em estatísticas agregadas por arquivo. Se você descobrir que mais bytes de um arquivo foram lidos do que o tamanho total desse arquivo no disco, você está definitivamente sofrendo uma sobrecarga no cache da página.

O que isso significa é que o conjunto de trabalho exigido pela sua carga de trabalho (normalmente um único aplicativo mais system_server) é maior que a quantidade de memória disponível para o cache de páginas no seu dispositivo. Como resultado, à medida que uma parte da carga de trabalho obtém os dados necessários no cache da página, outra parte que será usada em um futuro próximo será despejada e terá que ser buscada novamente, fazendo com que o problema ocorra novamente até que a carga Completou. Esta é a causa fundamental dos problemas de desempenho quando não há memória suficiente disponível em um dispositivo.

Não existe uma maneira infalível de corrigir o desgaste do cache da página, mas existem algumas maneiras de tentar melhorar isso em um determinado dispositivo.

  • Use menos memória em processos persistentes. Quanto menos memória for usada por processos persistentes, mais memória estará disponível para aplicativos e cache de páginas.
  • Audite as exceções que você possui para o seu dispositivo para garantir que você não está removendo memória desnecessariamente do sistema operacional. Vimos situações em que carveouts usados ​​para depuração foram deixados acidentalmente nas configurações do kernel, consumindo dezenas de megabytes de memória. Isso pode fazer a diferença entre atingir ou não o esgotamento do cache da página, especialmente em dispositivos com menos memória.
  • Se você estiver vendo o cache de páginas sobrecarregando system_server em arquivos críticos, considere fixar esses arquivos. Isso aumentará a pressão da memória em outros lugares, mas poderá modificar o comportamento o suficiente para evitar problemas.
  • Ajuste novamente o lowmemorykiller para tentar manter mais memória livre. Os limites do lowmemorykiller são baseados na memória livre absoluta e no cache da página, portanto, aumentar o limite no qual os processos em um determinado nível oom_adj são eliminados pode resultar em um melhor comportamento às custas do aumento da morte do aplicativo em segundo plano.
  • Tente usar ZRAM. Usamos ZRAM no Pixel, embora o Pixel tenha 4 GB, porque pode ajudar com páginas sujas raramente usadas.