O jitter é o comportamento aleatório do sistema que impede a execução de um trabalho perceptível. Esta página descreve como identificar e resolver problemas de instabilidade relacionados a jitter.
Atraso do programador de linhas de execução do app
O atraso do programador é o sintoma mais óbvio de jitter: um processo que precisa ser executado é executado, mas não é executado por um período significativo. A importância do atraso varia de acordo com o contexto. Exemplo:
- Uma linha de execução auxiliar aleatória em um app provavelmente pode ser atrasada por muitos milissegundos sem problemas.
- A linha de execução da interface do app pode tolerar 1 a 2ms de jitter.
- As kthreads do driver que são executadas como SCHED_FIFO podem causar problemas se forem executadas por 500us antes da execução.
Os tempos de execução podem ser identificados no systrace pela barra azul que precede um
segmento em execução de uma linha de execução. Um tempo de execução também pode ser determinado pelo
tempo entre o evento sched_wakeup
de uma linha de execução e o
evento sched_switch
que sinaliza o início da execução da linha de execução.
Linhas de execução muito longas
Linhas de execução da interface do app que podem ser executadas por muito tempo podem causar problemas. As linhas de execução de nível inferior com tempos de execução longos geralmente têm causas diferentes, mas a tentativa de reduzir o tempo de execução da linha de execução da interface para zero pode exigir a correção de alguns dos mesmos problemas que fazem com que as linhas de execução de nível inferior tenham tempos de execução longos. Para evitar atrasos:
- Use cpusets conforme descrito em Limitação térmica.
- Aumente o valor de CONFIG_HZ.
- Historicamente, o valor foi definido como 100 em plataformas arm e arm64. No entanto, isso é um acidente da história e não é um valor bom para usar em dispositivos interativos. CONFIG_HZ=100 significa que um jiffy tem 10 ms, o que significa que o balanceamento de carga entre CPUs pode levar 20 ms (dois jiffys) para acontecer. Isso pode contribuir significativamente para o jank em um sistema carregado.
- Dispositivos recentes (Nexus 5X, Nexus 6P, Pixel e Pixel XL) foram enviados com CONFIG_HZ=300. Isso deve ter um custo de energia desprezível, melhorando significativamente os tempos de execução. Se você notar aumentos significativos no consumo de energia ou problemas de desempenho após mudar o CONFIG_HZ, é provável que um dos drivers esteja usando um timer baseado em jiffies brutos em vez de milissegundos e convertendo para jiffies. Isso geralmente é fácil de corrigir. Consulte o patch que corrigiu problemas de timer kgsl no Nexus 5X e 6P ao converter para CONFIG_HZ=300.
- Por fim, testamos o CONFIG_HZ=1000 no Nexus/Pixel e descobrimos que ele oferece uma redução perceptível no desempenho e no consumo de energia devido à redução do overhead do RCU.
Com essas duas mudanças, um dispositivo vai ficar muito melhor para o tempo de execução da linha de execução da interface sob carga.
Use sys.use_fifo_ui
Para reduzir o tempo de execução da linha de execução da interface, defina a
propriedade sys.use_fifo_ui
como 1.
Aviso: não use essa opção em
configurações de CPU heterogêneas, a menos que você tenha um programador de RT ciente da capacidade.
No momento, NENHUM AGENDADOR DE RT ENVIADO ESTÁ
ATENTO À CAPACIDADE. Estamos trabalhando em uma para EAS, mas ela ainda não está
disponível. O programador RT padrão é baseado apenas nas prioridades RT e se
uma CPU já tem uma linha de execução RT de prioridade igual ou maior.
Como resultado, o programador RT padrão vai mover a
linha de execução de interface relativamente longa de um núcleo grande de alta frequência para um núcleo
pequeno na frequência mínima se uma kthread FIFO de prioridade mais alta for acionada
no mesmo núcleo grande. Isso vai causar regressões de desempenho
significativas. Como essa opção ainda não foi usada em um dispositivo Android
de envio, entre em contato com a equipe de performance do Android para
ajudar a validar.
Quando sys.use_fifo_ui
está ativado, o ActivityManager rastreia a linha de execução
da interface e a RenderThread (as duas linhas de execução mais importantes da interface) do app
principal e as torna SCHED_FIFO em vez de SCHED_OTHER. Isso
elimina efetivamente o jitter da interface e das linhas de renderização. Os rastros que
coletamos com essa opção ativada mostram tempos de execução na ordem de
microssegundos em vez de milissegundos.
No entanto, como o balanceador de carga do RT não tinha capacidade, houve uma redução de 30% no desempenho de inicialização do app porque a linha de execução da interface responsável por iniciar o app seria movida 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 capacidade, observamos performance equivalente em operações em massa e uma redução de 10 a 15% nos tempos de frame de 95º e 99º percentil em muitos dos nossos comparativos de mercado da interface.
Interromper o tráfego
Como as plataformas ARM enviam interrupções para a CPU 0 por padrão, recomendamos o uso de um balanceador de IRQ (irqbalance ou msm_irqbalance em plataformas Qualcomm).
Durante o desenvolvimento do Pixel, identificamos um problema que poderia ser atribuído diretamente
à CPU 0 com interrupções. Por exemplo, se a linha de execução mdss_fb0
fosse programada na CPU 0, haveria uma probabilidade muito maior de instabilidade
devido a uma interrupção acionada pela tela quase imediatamente
antes da saída de dados. O mdss_fb0
estaria no meio do próprio trabalho
com um prazo muito apertado e perderia algum tempo para o gerenciador de interrupção
do MDSS. Inicialmente, tentamos corrigir isso definindo a afinidade
de CPU da linha de execução mdss_fb0 para as CPUs 1 a 3 para evitar a contenção com a
interrupção, mas depois percebemos que ainda não havíamos ativado o msm_irqbalance. Com
o msm_irqbalance ativado, o jank foi notavelmente melhorado, mesmo quando o mdss_fb0 e
a interrupção do MDSS estavam na mesma CPU devido à redução da contenção de outras
interrupções.
Isso pode ser identificado no systrace analisando a seção de programação e a seção de interrupção. A seção de programação mostra o que foi programado, mas uma região sobreposta na seção de interrupção significa que uma interrupção está sendo executada durante esse tempo em vez do processo normalmente programado. Se você notar períodos significativos de tempo durante uma interrupção, suas opções incluem:
- Acelere o gerenciador de interrupção.
- Evite que a interrupção aconteça.
- Mude a frequência da interrupção para que ela fique fora de fase com outro trabalho regular que possa estar interferindo (se for uma interrupção regular).
- Defina a afinidade da CPU da interrupção diretamente e evite que ela seja equilibrada.
- Defina a afinidade de CPU da linha de execução em que a interrupção está interferindo para evitar a interrupção.
- Confie no balanceador de interrupção para mover a interrupção para uma CPU com menos carga.
Geralmente, não é recomendável definir a afinidade de CPU, mas isso pode ser útil para 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 acionam interrupções em que o sistema está mais restrito do que o normal (como RV), a afinidade explícita de CPU pode ser uma boa solução.
Softirqs longos
Enquanto um softirq está em execução, ele desativa a preempção. Os softirqs também podem ser acionados em muitos lugares no kernel e podem ser executados em um processo do usuário. Se houver atividade suficiente de softirq, os processos do usuário vão parar de executar softirqs, e o ksoftirqd será ativado para executar softirqs e ser equilibrado de carga. Isso geralmente não é um problema. No entanto, um único softirq muito longo pode causar estragos no sistema.
Os softirqs ficam visíveis na seção irq de um rastreamento, então é fácil identificar se o problema pode ser reproduzido durante o rastreamento. Como uma softirq pode ser executada em um processo do usuário, uma softirq incorreta também pode se manifestar como um tempo de execução extra em um processo do usuário sem um motivo óbvio. Se isso acontecer, verifique a seção de irq para saber se os softirqs são os culpados.
Drivers que deixam a preempção ou IRQs desativados por muito tempo
A desativação da interrupção ou das interrupções por muito tempo (dezenas de milissegundos) resulta em instabilidade. Normalmente, o jank se manifesta como uma linha de execução que pode ser executada, mas não está em execução em uma CPU específica, mesmo que a linha de execução possa ser executada com uma prioridade mais alta (ou SCHED_FIFO) do que a outra linha de execução.
Confira algumas diretrizes:
- Se a linha de execução for SCHED_FIFO e a linha de execução for SCHED_OTHER, a linha de execução terá preempção ou interrupções desativadas.
- Se a linha de execução tiver uma prioridade significativamente maior (100) do que a linha de execução (120), a linha de execução provavelmente terá a preempção ou as interrupções desativadas se a linha de execução não for executada em dois jiffies.
- Se a linha de execução e a linha em execução tiverem a mesma prioridade, a linha em execução provavelmente terá preempção ou interrupções desativadas se a linha de execução não for executada em 20 ms.
Executar um manipulador de interrupção impede que você atenda outras interrupções, o que também desativa a preempção.
Outra opção para identificar regiões com problemas é com o rastreador preemptirqsoff (consulte Como usar o ftrace dinâmico). Esse rastreador pode fornecer insights muito mais detalhados sobre a causa raiz de uma região ininterruptível (como nomes de função), mas requer um trabalho mais invasivo para ser ativado. Embora isso possa ter um impacto maior na performance, vale a pena tentar.
Uso incorreto de filas de trabalhos
Os manipuladores de interrupção geralmente precisam fazer trabalhos que podem ser executados fora de um contexto de interrupção, permitindo que o trabalho seja distribuído para diferentes linhas de execução no kernel. Um desenvolvedor de drivers pode notar que o kernel tem uma funcionalidade de tarefa assíncrona em todo o sistema chamada workqueues e pode usá-la para trabalhos relacionados a interrupções.
No entanto, as filas de trabalhos quase sempre são a resposta errada para esse problema, porque elas são sempre SCHED_OTHER. Muitas interrupções de hardware estão no caminho crítico de performance e precisam ser executadas imediatamente. As filas de trabalhos não têm garantia de quando serão executadas. Sempre que encontramos um workqueue no caminho crítico de performance, ele era uma fonte de instabilidade esporádica, independente do dispositivo. No Pixel, com um processador principal, observamos que uma única ThreadPool poderia ser atrasada em até 7 ms se o dispositivo estivesse em carga, dependendo do comportamento do programador e de outras coisas em execução no sistema.
Em vez de um workqueue, os drivers que precisam processar trabalhos semelhantes a interrupções dentro de uma linha de execução separada precisam criar a própria kthread SCHED_FIFO. Para receber ajuda ao fazer isso com as funções kthread_work, consulte este patch.
Contenção de bloqueio do framework
A contenção de bloqueio do framework pode ser uma fonte de instabilidade ou outros problemas de desempenho. Geralmente, é causado pelo bloqueio do ActivityManagerService, mas também pode ser encontrado em outros bloqueios. Por exemplo, o bloqueio do PowerManagerService pode afetar a tela na performance. Se você estiver vendo isso no seu dispositivo, não há uma correção adequada, porque ela só pode ser feita com melhorias na arquitetura do framework. No entanto, se você estiver modificando o código executado no system_server, é essencial evitar manter bloqueios por um longo período, especialmente o bloqueio do ActivityManagerService.
Contenção de bloqueio do Binder
Historicamente, o Binder tinha uma única trava global. Se a linha de execução que executa uma transação de vinculação foi interrompida enquanto segurava o bloqueio, nenhuma outra linha de execução poderá realizar uma transação de vinculação até que a linha de execução original libere o bloqueio. Isso é ruim. A contenção do binder pode bloquear tudo no sistema, incluindo o envio de atualizações da interface para a tela (as linhas de execução da interface se comunicam com o SurfaceFlinger por meio do binder).
O Android 6.0 incluiu vários patches para melhorar esse comportamento desativando a preempção enquanto mantinha a trava de vinculação. Isso era seguro apenas porque a trava do binder precisava ser mantida por alguns microssegundos de execução real. Isso melhorou significativamente o desempenho em situações sem contenção e evitou a contenção impedindo a maioria das mudanças de agendador enquanto a trava de vinculação era mantida. No entanto, a preempção não pode ser desativada durante todo o tempo de execução de retenção da trava do vinculamento, o que significa que a preempção foi ativada para funções que podem entrar em suspensão (como copy_from_user), o que pode causar a mesma preempção que o caso original. Quando enviamos os patches, eles disseram que essa foi a pior ideia da história. Concordamos com eles, mas também não podíamos argumentar com a eficácia dos patches para evitar o problema.
Contenção de fd em um processo
Isso é raro. O problema provavelmente não é causado por isso.
No entanto, se você tiver várias linhas de execução em um processo gravando o mesmo fd, será possível notar a contenção nesse fd. No entanto, o único momento em que isso ocorreu durante a inicialização do Pixel foi durante um teste em que as linhas de execução de baixa prioridade tentaram ocupar todo o tempo da CPU enquanto uma única linha de execução de alta prioridade estava em execução no mesmo processo. Todas as linhas de execução estavam gravando no fd do marcador de rastreamento, e a linha de execução de alta prioridade poderia ser bloqueada no fd do marcador de rastreamento se uma linha de execução de baixa prioridade estivesse segurando a trava do fd e fosse interrompida. Quando o rastreamento foi desativado nas linhas de execução de baixa prioridade, não houve problemas de desempenho.
Não foi possível reproduzir isso em nenhuma outra situação, mas vale a pena destacar como uma possível causa de problemas de desempenho durante o rastreamento.
Transições de inatividade da CPU desnecessárias
Ao lidar com IPC, especialmente pipelines de vários processos, é comum encontrar variações no seguinte comportamento de execução:
- A linha de execução A é executada na CPU 1.
- A linha de execução A ativa a linha de execução B.
- A linha de execução B começa a ser executada na CPU 2.
- A linha de execução A entra imediatamente no modo de suspensão para ser ativada pela linha de execução B quando a linha de execução B terminar o trabalho atual.
Uma fonte comum de overhead é entre as etapas 2 e 3. Se a CPU 2 estiver ociosa, ela precisa ser trazida de volta para um estado ativo antes que a linha de execução B possa ser executada. Dependendo do SOC e da profundidade do modo inativo, isso pode levar dezenas de microssegundos antes que a linha de execução B comece a ser executada. 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 reduzido significativamente por transições de inatividade da CPU. O lugar mais comum para o Android encontrar esse problema é em transações de vinculação, e muitos serviços que usam a vinculação acabam parecendo com a situação descrita acima.
Primeiro, use a função wake_up_interruptible_sync()
nos
drivers do kernel e ofereça suporte a ela em qualquer programador personalizado. Trate isso como um
requisito, não uma dica. O Binder usa isso hoje, e isso ajuda muito com
transações síncronas do Binder, evitando transições de inatividade da CPU desnecessárias.
Em segundo lugar, verifique se os tempos de transição de cpuidle são realistas e se o governador cpuidle os leva em consideração corretamente. Se o SOC estiver alternando entre o estado de inatividade mais profundo e o mais profundo, você não vai economizar energia com o estado de inatividade mais profundo.
Geração de registros
A geração de registros não é sem custo financeiro para ciclos de CPU ou memória. Portanto, não envie spam para o buffer de registro. Os custos de registro são ciclados no app (diretamente) e no daemon de registro. Remova todos os registros de depuração antes de enviar o dispositivo.
Problemas de E/S
As operações de E/S são fontes comuns de instabilidade. Se uma linha de execução acessar um arquivo mapeado em memória e a página não estiver no cache de página, ela falhará e lerá a página do disco. Isso bloqueia a linha de execução (geralmente por mais de 10 ms) e, se acontecer no caminho crítico da renderização da interface, 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 o framework bloqueie alguns arquivos no cache da página. Isso remove a memória para
uso por qualquer outro processo, mas se houver alguns arquivos conhecidos de antemão
para uso regular, pode ser eficaz bloquear esses arquivos.
Em dispositivos Pixel e Nexus 6P com o 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
- Criptografia. Outra possível causa de problemas de E/S. A criptografia inline
oferece a melhor performance em comparação com a criptografia baseada em CPU
ou o uso de um bloco de hardware acessível por DMA. Mais importante,
a criptografia inline reduz o jitter associado à E/S, especialmente quando
comparada com a criptografia baseada em CPU. Como as buscas no cache da página geralmente estão no
caminho crítico da renderização da interface, a criptografia baseada em CPU introduz uma carga
adicional de CPU no caminho crítico, o que aumenta a instabilidade em relação à busca de E/S.
Os mecanismos de criptografia de hardware baseados em DMA têm um problema semelhante, já que o kernel precisa gastar ciclos gerenciando esse trabalho, mesmo que outro trabalho crítico esteja disponível para execução. Recomendamos que qualquer fornecedor de SOC que esteja criando um novo hardware inclua suporte à criptografia inline.
Agrupamento agressivo de tarefas pequenas
Alguns programadores oferecem suporte para agrupar 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 capacidade e o consumo de energia, pode ser catastrófico para a latência. Há várias linhas de execução de curta duração no caminho crítico da renderização da interface que podem ser consideradas pequenas. Se essas linhas forem atrasadas à medida que são migradas lentamente para outras CPUs, isso vai causar instabilidade. Recomendamos usar o empacotamento de tarefas pequenas de forma muito conservadora.
Uso excessivo do cache de página
Um dispositivo sem memória livre suficiente pode ficar extremamente lento de repente ao executar uma operação de longa duração, como abrir um novo app. Um rastro do app pode revelar que ele está consistentemente bloqueado na E/S durante uma execução específica, mesmo quando não está bloqueado na E/S. Isso geralmente é um sinal de thrashing do cache de página, especialmente em dispositivos com menos memória.
Uma maneira de identificar isso é fazer um systrace usando a tag pagecache e
alimentar esse rastreamento para o script em
system/extras/pagecache/pagecache.py
. O pagecache.py converte
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ê definitivamente está atingindo o cache de página.
Isso significa que o conjunto de trabalho necessário para sua carga de trabalho (normalmente um único app e o system_server) é maior do que a quantidade de memória disponível para o cache de página no dispositivo. Como resultado, uma parte da carga de trabalho recebe os dados necessários no cache de página, outra parte que será usada em breve será excluída e terá que ser buscada novamente, causando o problema até que a carga seja concluída. Essa é a causa fundamental de problemas de desempenho quando não há memória suficiente disponível em um dispositivo.
Não há uma maneira infalível de corrigir o uso excessivo do cache de página, mas há 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 apps e para o cache de página.
- Faça uma auditoria nos recursos exclusivos do seu dispositivo para garantir que você não esteja removendo memória do SO desnecessariamente. Vimos situações em que as exclusões usadas para depuração foram deixadas acidentalmente nas configurações do kernel de envio, consumindo dezenas de megabytes de memória. Isso pode fazer a diferença entre acionar o cache de página e não, especialmente em dispositivos com menos memória.
- Se você notar que o cache de página está sendo usado em system_server em arquivos críticos, considere fixá-los. Isso vai aumentar a pressão de memória em outros lugares, mas pode modificar o comportamento o suficiente para evitar a fragmentação.
- Reajuste o lowmemorykiller para tentar manter mais memória livre. Os limites do lowmemorykiller são baseados na memória livre absoluta e no cache de página. Portanto, aumentar o limite em que os processos em um determinado nível de oom_adj são encerrados pode resultar em um comportamento melhor à custa de um aumento na morte de apps em segundo plano.
- Tente usar o ZRAM. Usamos a ZRAM no Pixel, mesmo que ele tenha 4 GB, porque ela pode ajudar com páginas sujas usadas com pouca frequência.