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 instabilidade.
Atraso do agendador de thread 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 significativo de tempo. O significado 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 interface do usuário de um aplicativo pode tolerar 1-2ms de jitter.
- Drivers kthreads executados como SCHED_FIFO podem causar problemas se forem executáveis por 500us antes de serem executados.
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 encadeamento e o evento sched_switch
que sinaliza o início da execução do encadeamento.
Tópicos que são muito longos
Os threads da interface do usuário do aplicativo que podem ser executados por muito tempo podem causar problemas. Os threads de nível inferior com tempos de execução longos geralmente têm causas diferentes, mas tentar empurrar o tempo de execução do thread da interface do usuário para zero pode exigir a correção de alguns dos mesmos problemas que fazem com que os threads de nível inferior tenham tempos de execução longos. Para mitigar atrasos:
- Use cpusets conforme descrito em Limitação térmica .
- Aumente o valor CONFIG_HZ.
- Historicamente, o valor foi definido como 100 nas plataformas arm e arm64. No entanto, isso é um acidente da história e não é um bom valor para usar 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. Isso deve ter um custo de energia insignificante, melhorando 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 de seus drivers esteja usando um cronômetro baseado em jiffies brutos em vez de milissegundos e convertendo em jiffies. Isso geralmente é uma correção fácil (veja o patch que corrigiu os problemas do temporizador kgsl no Nexus 5X e 6P ao converter para CONFIG_HZ=300).
- Por fim, testamos CONFIG_HZ=1000 no Nexus/Pixel e descobrimos que oferece um desempenho notável e redução de energia devido à redução da sobrecarga do RCU.
Com essas duas alterações sozinhas, um dispositivo deve ter uma aparência muito melhor para o tempo de execução do thread da interface do usuário sob carga.
Usando sys.use_fifo_ui
Você pode tentar direcionar o tempo executável do thread da interface do usuário para zero 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 de RT com reconhecimento de capacidade. E, neste momento, NENHUM AGENDADOR DE RT ATUALMENTE ESTÁ CONHECENDO A CAPACIDADE . Estamos trabalhando em um para EAS, mas ainda não está disponível. O escalonador de RT padrão é baseado puramente em prioridades de RT e se uma CPU já possui um encadeamento de RT de prioridade igual ou superior.
Como resultado, o agendador de RT padrão moverá alegremente seu thread de interface do usuário de execução relativamente longa de um núcleo grande de alta frequência para um núcleo pequeno com frequência mínima se um kthread FIFO de prioridade mais alta acordar no mesmo núcleo grande. Isso introduzirá regressões de desempenho significativas . Como esta opção ainda não foi usada em um dispositivo Android de envio, 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, o ActivityManager rastreia o thread da interface do usuário e o RenderThread (os dois threads mais críticos para a interface do usuário) do aplicativo principal e torna esses threads SCHED_FIFO em vez de SCHED_OTHER. Isso elimina efetivamente o jitter da interface do usuário e dos RenderThreads; os rastreamentos que reunimos com essa 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 estava ciente da capacidade, houve uma redução de 30% no desempenho de inicialização do aplicativo porque o thread de interface do usuário responsável pela inicialização do aplicativo seria movido de um núcleo Kryo ouro de 2,1 GHz para um núcleo Kryo prata de 1,5 GHz . Com um balanceador de carga RT com reconhecimento de capacidade, vemos um desempenho equivalente em operações em massa e uma redução de 10 a 15% nos tempos de quadro do percentil 95 e 99 em muitos de nossos benchmarks de interface do usuário.
Interromper o tráfego
Como as plataformas ARM entregam interrupções à CPU 0 apenas por padrão, recomendamos o uso de um balanceador de IRQ (irqbalance ou msm_irqbalance em plataformas Qualcomm).
Durante o desenvolvimento do Pixel, vimos instabilidades que podem ser atribuídas diretamente à sobrecarga da CPU 0 com interrupções. Por exemplo, se o encadeamento mdss_fb0
foi agendado na CPU 0, havia uma probabilidade muito maior de jank devido a uma interrupção que é acionada pela exibição quase imediatamente antes do scanout. mdss_fb0
estaria no meio de seu próprio trabalho com um prazo muito curto e perderia algum tempo para o manipulador de interrupção MDSS. Inicialmente, tentamos corrigir isso definindo a afinidade de CPU do thread mdss_fb0 para CPUs 1-3 para evitar contenção com a interrupção, mas percebemos que ainda não havíamos ativado msm_irqbalance. Com msm_irqbalance habilitado, o jank foi visivelmente melhorado mesmo quando mdss_fb0 e a interrupção MDSS estavam na mesma CPU devido à contenção reduzida de outras interrupções.
Isso pode ser identificado no systrace observando a seção sched, bem como 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ê vir pedaços significativos de tempo durante uma interrupção, suas opções incluem:
- Torne o manipulador de interrupção mais rápido.
- Impedir que a interrupção aconteça em primeiro lugar.
- Altere a frequência da interrupção para estar fora de fase com outro trabalho regular que possa estar interferindo (se for uma interrupção regular).
- Defina a afinidade de CPU da interrupção diretamente e evite que ela seja balanceada.
- Defina a afinidade de CPU do encadeamento 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 menos carregada.
Definir a afinidade da CPU geralmente não é recomendado, mas 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 aciona certas interrupções em que o sistema é mais restrito do 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 pode ser acionado em muitos lugares dentro do kernel e pode ser executado dentro de um processo de usuário. Se houver atividade de softirq suficiente, os processos do usuário pararão de executar softirqs e ksoftirqd acordará para executar softirqs e ter balanceamento de carga. Normalmente, isso é bom. 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ê ver isso, verifique a seção irq para ver se os softirqs são os culpados.
Drivers deixando preempção ou IRQs desabilitados por muito tempo
Desabilitar a preempção ou interrupções por muito tempo (dezenas de milissegundos) resulta em instabilidade. Normalmente, o jank se manifesta como um encadeamento se tornando executável, mas não em execução em uma CPU específica, mesmo que o encadeamento executável tenha prioridade significativamente mais alta (ou SCHED_FIFO) do que o outro encadeamento.
Algumas orientações:
- 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 em 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 em 20 ms.
Tenha em mente que a execução de um manipulador de interrupção impede que você atenda a outras interrupções, o que também desabilita a preempção.
Outra opção para identificar regiões problemáticas é com o rastreador preemptirqsoff (consulte Using dynamic ftrace ). Esse rastreador pode fornecer uma visão muito maior da causa raiz de uma região ininterrupta (como nomes de funções), mas requer um trabalho mais invasivo para habilitar. Embora possa ter mais impacto no desempenho, definitivamente vale a pena tentar.
Uso incorreto de filas de trabalho
Os manipuladores de interrupção geralmente precisam fazer um trabalho que possa 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 notar que o kernel tem uma funcionalidade de tarefa assíncrona em todo o sistema muito conveniente 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 esse 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. Toda vez que vimos uma fila de trabalho no caminho crítico do desempenho, ela foi uma fonte de instabilidade esporádica, 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 com as 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 correção, porque só pode ser aprimorado por meio de melhorias de arquitetura no framework. No entanto, se você estiver modificando o código que é executado dentro do system_server, é essencial evitar manter 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 impedido enquanto mantém o bloqueio, nenhum outro encadeamento pode executar uma transação de fichário até que o encadeamento original tenha liberado o bloqueio. Isto é mau; A contenção do binder pode bloquear tudo no sistema, incluindo o envio de atualizações da interface do usuário para a tela (os threads da interface do usuário se comunicam com o SurfaceFlinger via binder).
O Android 6.0 incluiu vários patches para melhorar esse comportamento desativando a preempção enquanto mantinha o bloqueio do binder. Isso era seguro apenas porque o bloqueio do fichário deveria ser mantido por alguns microssegundos de 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 trocas de agendador enquanto o bloqueio do binder estava mantido. No entanto, a preempção não pôde ser desabilitada durante todo o tempo de execução de retenção do bloqueio de 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 upstream, eles prontamente nos disseram que essa era a pior ideia da história. (Concordamos com eles, mas também não podíamos discutir a eficácia dos patches na prevenção de instabilidade.)
fd contenção dentro de um processo
Isso é raro. Seu jank provavelmente não é causado 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 exibiçã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 sendo executado dentro do mesmo processo. Todos os encadeamentos estavam gravando no marcador de rastreamento fd e o encadeamento de alta prioridade poderia ser bloqueado no marcador de rastreamento fd se um encadeamento de baixa prioridade estivesse segurando o bloqueio fd e fosse então preemptivo. Quando o rastreamento foi desabilitado dos 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 ociosas de CPU desnecessárias
Ao lidar com IPC, especialmente pipelines de vários processos, é comum ver variações no seguinte comportamento de tempo de execução:
- O thread A é executado na CPU 1.
- A thread A desperta a thread B.
- O thread B começa a ser executado na CPU 2.
- A thread A imediatamente entra em suspensão, para ser despertada pela thread B quando a 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 a um estado ativo antes que a thread B possa ser executada. 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 pode ser significativamente reduzido por transições de inatividade da CPU. O lugar mais comum para o Android atingir isso é em torno de transações de binder, e muitos serviços que usam binder 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 a partir de qualquer agendador personalizado. Trate isso como um requisito, não uma dica. O Binder usa isso hoje e ajuda muito com transações de binder síncronas, evitando transições desnecessárias de CPU ociosa.
Em segundo lugar, certifique-se de que os tempos de transição da cpuidle sejam realistas e que o controlador da cpuidle esteja levando isso em consideração corretamente. Se o seu SOC estiver entrando e saindo do seu estado ocioso mais profundo, você não economizará energia indo para o estado ocioso mais profundo.
Exploração madeireira
O log não é gratuito para ciclos de CPU ou memória, portanto, não faça spam no buffer de log. Ciclos de custos de log em seu aplicativo (diretamente) e no daemon de log. Remova todos os logs 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ágina, ele falhará e lerá a página do disco. Isso bloqueia o encadeamento (geralmente por mais de 10 ms) e, se ocorrer no caminho crítico da renderização da interface do usuário, 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:
- Serviço de Pinner . Adicionado no Android 7.0, 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 bloqueá-los.
Nos 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
- Criptografia . Outra possível causa de problemas de E/S. Achamos que a criptografia em linha oferece o melhor desempenho quando comparada à criptografia baseada em CPU ou usando 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 de página geralmente estão no caminho crítico da renderização da interface do usuário, a criptografia baseada em CPU introduz carga de CPU adicional no caminho crítico, o que adiciona mais jitter 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. É altamente recomendável que qualquer fornecedor de SOC crie um novo hardware para incluir suporte para criptografia em linha.
Embalagem agressiva para pequenas tarefas
Alguns agendadores oferecem suporte para compactar 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 taxa de transferência e consumo de energia, pode ser catastrófico para a latência. Existem vários threads de execução curta no caminho crítico da renderização da interface do usuário que podem ser considerados pequenos; se esses encadeamentos estiverem atrasados à medida que são migrados lentamente para outras CPUs, isso causará instabilidade. Recomendamos usar o empacotamento de pequenas tarefas de forma muito conservadora.
Thrashing de cache de página
Um dispositivo sem memória livre suficiente pode ficar extremamente lento de repente durante a execução de 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 geralmente não é bloqueado na E/S. Isso geralmente é um sinal de perda de cache de página, especialmente em dispositivos com menos memória.
Uma maneira de identificar isso é usar 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 de página em estatísticas agregadas por arquivo. Se você achar 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.
O que isso significa é que o conjunto de trabalho exigido por 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ágina em seu dispositivo. Como resultado, à medida que uma parte da carga de trabalho obtém os dados de que precisa 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é o carregamento Completou. Essa é a causa fundamental dos 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 thrashing do cache de 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 os aplicativos e o cache da página.
- Faça uma auditoria dos carveouts que você tem para o seu dispositivo para garantir que você não esteja removendo memória do sistema operacional desnecessariamente. Vimos situações em que carveouts usados para depuração foram deixados acidentalmente nas configurações do kernel de envio, consumindo dezenas de megabytes de memória. Isso pode fazer a diferença entre atingir o cache de página ou não, especialmente em dispositivos com menos memória.
- Se você estiver vendo o cache de página thrashing no system_server em arquivos críticos, considere fixar esses arquivos. Isso aumentará a pressão da memória em outros lugares, mas pode modificar o comportamento o suficiente para evitar o thrashing.
- 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 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.