Teste de performance

O Android 8.0 inclui testes de desempenho de binder e hwbinder para taxa de transferência e latência. Embora existam muitos cenários para detectar problemas de desempenho perceptíveis, a execução desses cenários pode ser demorada e os resultados geralmente ficam indisponíveis até que um sistema seja integrado. O uso dos testes de desempenho fornecidos facilita o teste durante o desenvolvimento, detecta problemas sérios mais cedo e melhora a experiência do usuário.

Os testes de desempenho incluem as quatro categorias a seguir:

  • taxa de transferência do binder (disponível em system/libhwbinder/vts/performance/Benchmark_binder.cpp )
  • latência do binder (disponível em frameworks/native/libs/binder/tests/schd-dbg.cpp )
  • taxa de transferência do hwbinder (disponível em system/libhwbinder/vts/performance/Benchmark.cpp )
  • hwbinder latency (disponível em system/libhwbinder/vts/performance/Latency.cpp )

Sobre o binder e o hwbinder

Binder e hwbinder são infraestruturas de comunicação entre processos (IPC) do Android que compartilham o mesmo driver Linux, mas têm as seguintes diferenças qualitativas:

Aspecto encadernador hwbinder
Propósito Fornecer um esquema IPC de propósito geral para a estrutura Comunique-se com o hardware
Propriedade Otimizado para uso do framework Android Baixa latência de sobrecarga mínima
Alterar a política de agendamento para primeiro plano/segundo plano Sim Não
Argumentos passando Usa serialização suportada pelo objeto Parcel Usa buffers de dispersão e evita a sobrecarga para copiar os dados necessários para a serialização do Parcel
Herança prioritária Não Sim

Processos binder e hwbinder

Um visualizador systrace exibe as transações da seguinte forma:

Figura 1. Visualização do Systrace dos processos do binder.

No exemplo acima:

  • Os quatro (4) processos schd-dbg são processos cliente.
  • Os quatro (4) processos de fichário são processos de servidor (o nome começa com Binder e termina com um número de sequência).
  • Um processo cliente está sempre emparelhado com um processo servidor, que é dedicado ao seu cliente.
  • Todos os pares de processos cliente-servidor são agendados independentemente pelo kernel simultaneamente.

Na CPU 1, o kernel do sistema operacional executa o cliente para emitir a solicitação. Em seguida, ele usa a mesma CPU sempre que possível para ativar um processo do servidor, manipular a solicitação e alternar o contexto de volta após a conclusão da solicitação.

Taxa de transferência x latência

Em uma transação perfeita, onde o processo cliente e servidor alternam perfeitamente, os testes de taxa de transferência e latência não produzem mensagens substancialmente diferentes. No entanto, quando o kernel do sistema operacional está processando uma solicitação de interrupção (IRQ) do hardware, aguardando bloqueios ou simplesmente optando por não manipular uma mensagem imediatamente, uma bolha de latência pode se formar.

Figura 2. Bolha de latência devido a diferenças na taxa de transferência e latência.

O teste de taxa de transferência gera um grande número de transações com diferentes tamanhos de carga útil, fornecendo uma boa estimativa para o tempo de transação regular (nos melhores cenários) e a taxa de transferência máxima que o binder pode alcançar.

Em contraste, o teste de latência não executa nenhuma ação na carga para minimizar o tempo de transação regular. Podemos usar o tempo de transação para estimar a sobrecarga do binder, fazer estatísticas para o pior caso e calcular a proporção de transações cuja latência atende a um prazo especificado.

Como lidar com inversões de prioridade

Uma inversão de prioridade ocorre quando um thread com prioridade mais alta está esperando logicamente por um thread com prioridade mais baixa. Os aplicativos de tempo real (RT) têm um problema de inversão de prioridade:

Figura 3. Inversão de prioridade em aplicações de tempo real.

Ao usar o agendamento do Linux Completely Fair Scheduler (CFS), um encadeamento sempre tem a chance de ser executado mesmo quando outros encadeamentos têm uma prioridade mais alta. Como resultado, os aplicativos com agendamento CFS tratam a inversão de prioridade como comportamento esperado e não como um problema. Nos casos em que o framework Android precisa de agendamento RT para garantir o privilégio de threads de alta prioridade, no entanto, a inversão de prioridade deve ser resolvida.

Exemplo de inversão de prioridade durante uma transação do binder (o thread RT é logicamente bloqueado por outros threads CFS ao aguardar um thread do binder atender):

Figura 4. Inversão de prioridade, encadeamentos em tempo real bloqueados.

Para evitar bloqueios, você pode usar a herança de prioridade para escalar temporariamente o encadeamento do Binder para um encadeamento RT quando ele atender a uma solicitação de um cliente RT. Tenha em mente que o agendamento RT tem recursos limitados e deve ser usado com cuidado. Em um sistema com n CPUs, o número máximo de threads RT atuais também é n ; encadeamentos RT adicionais podem precisar esperar (e, portanto, perder seus prazos) se todas as CPUs forem ocupadas por outros encadeamentos RT.

Para resolver todas as inversões de prioridade possíveis, você pode usar a herança de prioridade para binder e hwbinder. No entanto, como o binder é amplamente usado em todo o sistema, habilitar a herança de prioridade para transações do binder pode enviar spam ao sistema com mais threads RT do que ele pode atender.

Executando testes de taxa de transferência

O teste de taxa de transferência é executado em relação à taxa de transferência da transação binder/hwbinder. Em um sistema que não está sobrecarregado, bolhas de latência são raras e seu impacto pode ser eliminado desde que o número de iterações seja alto o suficiente.

  • O teste de taxa de transferência do binder está em system/libhwbinder/vts/performance/Benchmark_binder.cpp .
  • O teste de rendimento do hwbinder está em system/libhwbinder/vts/performance/Benchmark.cpp .

Resultado dos testes

Exemplo de resultados de teste de taxa de transferência para transações que usam diferentes tamanhos de carga útil:

Benchmark                      Time          CPU           Iterations
---------------------------------------------------------------------
BM_sendVec_binderize/4         70302 ns      32820 ns      21054
BM_sendVec_binderize/8         69974 ns      32700 ns      21296
BM_sendVec_binderize/16        70079 ns      32750 ns      21365
BM_sendVec_binderize/32        69907 ns      32686 ns      21310
BM_sendVec_binderize/64        70338 ns      32810 ns      21398
BM_sendVec_binderize/128       70012 ns      32768 ns      21377
BM_sendVec_binderize/256       69836 ns      32740 ns      21329
BM_sendVec_binderize/512       69986 ns      32830 ns      21296
BM_sendVec_binderize/1024      69714 ns      32757 ns      21319
BM_sendVec_binderize/2k        75002 ns      34520 ns      20305
BM_sendVec_binderize/4k        81955 ns      39116 ns      17895
BM_sendVec_binderize/8k        95316 ns      45710 ns      15350
BM_sendVec_binderize/16k      112751 ns      54417 ns      12679
BM_sendVec_binderize/32k      146642 ns      71339 ns       9901
BM_sendVec_binderize/64k      214796 ns     104665 ns       6495
  • O tempo indica o atraso de ida e volta medido em tempo real.
  • CPU indica o tempo acumulado em que as CPUs estão agendadas para o teste.
  • Iterações indica o número de vezes que a função de teste foi executada.

Por exemplo, para uma carga útil de 8 bytes:

BM_sendVec_binderize/8         69974 ns      32700 ns      21296

… o rendimento máximo que o fichário pode alcançar é calculado como:

Taxa de transferência MAX com carga útil de 8 bytes = (8 * 21296)/69974 ~= 2,423 b/ns ~= 2,268 Gb/s

Opções de teste

Para obter resultados em .json, execute o teste com o argumento --benchmark_format=json :

libhwbinder_benchmark --benchmark_format=json
{
  "context": {
    "date": "2017-05-17 08:32:47",
    "num_cpus": 4,
    "mhz_per_cpu": 19,
    "cpu_scaling_enabled": true,
    "library_build_type": "release"
  },
  "benchmarks": [
    {
      "name": "BM_sendVec_binderize/4",
      "iterations": 32342,
      "real_time": 47809,
      "cpu_time": 21906,
      "time_unit": "ns"
    },
   ….
}

Executando testes de latência

O teste de latência mede o tempo que leva para o cliente começar a inicializar a transação, alternar para o processo do servidor para manipulação e receber o resultado. O teste também procura comportamentos inválidos conhecidos do agendador que podem afetar negativamente a latência da transação, como um agendador que não dá suporte à herança de prioridade ou respeita o sinalizador de sincronização.

  • O teste de latência do binder está em frameworks/native/libs/binder/tests/schd-dbg.cpp .
  • O teste de latência do hwbinder está em system/libhwbinder/vts/performance/Latency.cpp .

Resultado dos testes

Os resultados (em .json) mostram estatísticas de latência média/melhor/pior e o número de prazos perdidos.

Opções de teste

Os testes de latência têm as seguintes opções:

Comando Descrição
-i value Especifique o número de iterações.
-pair value Especifique o número de pares de processos.
-deadline_us 2500 Especifique o prazo em nós.
-v Obtenha a saída detalhada (depuração).
-trace Interrompa o rastreamento em um acerto de prazo.

As seções a seguir detalham cada opção, descrevem o uso e fornecem resultados de exemplo.

Especificando iterações

Exemplo com um grande número de iterações e saída detalhada desabilitada:

libhwbinder_latency -i 5000 -pair 3
{
"cfg":{"pair":3,"iterations":5000,"deadline_us":2500},
"P0":{"SYNC":"GOOD","S":9352,"I":10000,"R":0.9352,
  "other_ms":{ "avg":0.2 , "wst":2.8 , "bst":0.053, "miss":2, "meetR":0.9996},
  "fifo_ms": { "avg":0.16, "wst":1.5 , "bst":0.067, "miss":0, "meetR":1}
},
"P1":{"SYNC":"GOOD","S":9334,"I":10000,"R":0.9334,
  "other_ms":{ "avg":0.19, "wst":2.9 , "bst":0.055, "miss":2, "meetR":0.9996},
  "fifo_ms": { "avg":0.16, "wst":3.1 , "bst":0.066, "miss":1, "meetR":0.9998}
},
"P2":{"SYNC":"GOOD","S":9369,"I":10000,"R":0.9369,
  "other_ms":{ "avg":0.19, "wst":4.8 , "bst":0.055, "miss":6, "meetR":0.9988},
  "fifo_ms": { "avg":0.15, "wst":1.8 , "bst":0.067, "miss":0, "meetR":1}
},
"inheritance": "PASS"
}

Esses resultados de teste mostram o seguinte:

"pair":3
Cria um par de cliente e servidor.
"iterations": 5000
Inclui 5.000 iterações.
"deadline_us":2500
O prazo é 2500us (2,5ms); espera-se que a maioria das transações atinja esse valor.
"I": 10000
Uma única iteração de teste inclui duas (2) transações:
  • Uma transação por prioridade normal ( CFS other )
  • Uma transação por prioridade em tempo real ( RT-fifo )
5.000 iterações equivalem a um total de 10.000 transações.
"S": 9352
9352 das transações são sincronizadas na mesma CPU.
"R": 0.9352
Indica a proporção na qual o cliente e o servidor são sincronizados na mesma CPU.
"other_ms":{ "avg":0.2 , "wst":2.8 , "bst":0.053, "miss":2, "meetR":0.9996}
O caso médio ( avg ), o pior ( wst ) e o melhor ( bst ) para todas as transações emitidas por um chamador de prioridade normal. Duas transações miss o prazo, tornando o índice de atendimento ( meetR ) de 0,9996.
"fifo_ms": { "avg":0.16, "wst":1.5 , "bst":0.067, "miss":0, "meetR":1}
Semelhante a other_ms , mas para transações emitidas pelo cliente com prioridade rt_fifo . É provável (mas não obrigatório) que o fifo_ms tenha um resultado melhor que other_ms , com valores de avg e wst mais baixos e um meetR mais alto (a diferença pode ser ainda mais significativa com carga em segundo plano).

Observação: a carga em segundo plano pode afetar o resultado da taxa de transferência e a tupla other_ms no teste de latência. Apenas o fifo_ms pode mostrar resultados semelhantes desde que o carregamento em segundo plano tenha uma prioridade menor que RT-fifo .

Especificando valores de par

Cada processo cliente é pareado com um processo servidor dedicado ao cliente, e cada par pode ser escalonado independentemente para qualquer CPU. No entanto, a migração da CPU não deve ocorrer durante uma transação enquanto o sinalizador SYNC for honor .

Certifique-se de que o sistema não está sobrecarregado! Embora seja esperada alta latência em um sistema sobrecarregado, os resultados de teste para um sistema sobrecarregado não fornecem informações úteis. Para testar um sistema com pressão mais alta, use -pair #cpu-1 (ou -pair #cpu com cuidado). Testar usando -pair n com n > #cpu sobrecarrega o sistema e gera informações inúteis.

Especificando valores de prazo

Após extensos testes de cenário de usuário (executando o teste de latência em um produto qualificado), determinamos que 2,5 ms é o prazo a ser cumprido. Para novos aplicativos com requisitos mais altos (como 1.000 fotos/segundo), esse valor de prazo será alterado.

Especificando a saída detalhada

O uso da opção -v exibe a saída detalhada. Exemplo:

libhwbinder_latency -i 1 -v

-------------------------------------------------- service pid: 8674 tid: 8674 cpu: 1 SCHED_OTHER 0
-------------------------------------------------- main pid: 8673 tid: 8673 cpu: 1 -------------------------------------------------- client pid: 8677 tid: 8677 cpu: 0 SCHED_OTHER 0
-------------------------------------------------- fifo-caller pid: 8677 tid: 8678 cpu: 0 SCHED_FIFO 99 -------------------------------------------------- hwbinder pid: 8674 tid: 8676 cpu: 0 ??? 99
-------------------------------------------------- other-caller pid: 8677 tid: 8677 cpu: 0 SCHED_OTHER 0 -------------------------------------------------- hwbinder pid: 8674 tid: 8676 cpu: 0 SCHED_OTHER 0
  • O encadeamento de serviço é criado com uma prioridade SCHED_OTHER e executado em CPU:1 com pid 8674 .
  • A primeira transação é então iniciada por um fifo-caller . Para atender a essa transação, o hwbinder atualiza a prioridade do servidor ( pid: 8674 tid: 8676 ) para 99 e também o marca com uma classe de agendamento transiente (impressa como ??? ). O agendador então coloca o processo do servidor em CPU:0 para ser executado e o sincroniza com a mesma CPU com seu cliente.
  • O segundo chamador de transação tem prioridade SCHED_OTHER . O servidor faz downgrade e atende o chamador com prioridade SCHED_OTHER .

Usando rastreamento para depuração

Você pode especificar a opção -trace para depurar problemas de latência. Quando usado, o teste de latência interrompe a gravação do tracelog no momento em que a latência ruim é detectada. Exemplo:

atrace --async_start -b 8000 -c sched idle workq binder_driver sync freq
libhwbinder_latency -deadline_us 50000 -trace -i 50000 -pair 3
deadline triggered: halt ∓ stop trace
log:/sys/kernel/debug/tracing/trace

Os seguintes componentes podem afetar a latência:

  • Modo de construção do Android . O modo Eng é geralmente mais lento que o modo userdebug.
  • Estrutura . Como o serviço de estrutura usa ioctl para configurar o fichário?
  • Driver de fichário . O driver suporta travamento refinado? Ele contém todos os patches de giro de desempenho?
  • Versão do kernel . Quanto melhor a capacidade de tempo real do kernel, melhores serão os resultados.
  • Configuração do kernel . A configuração do kernel contém configurações DEBUG como DEBUG_PREEMPT e DEBUG_SPIN_LOCK ?
  • Agendador de kernel . O kernel tem um agendador com reconhecimento de energia (EAS) ou agendador de multiprocessamento heterogêneo (HMP)? Algum driver do kernel (driver cpu-freq , driver cpu-idle , cpu-hotplug , etc.) afeta o agendador?