Teste de desempenho

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 não estão disponíveis até que um sistema seja integrado. Usar os testes de desempenho fornecidos facilita o teste durante o desenvolvimento, detecta problemas graves mais cedo e melhora a experiência do usuário.

Os testes de desempenho incluem as quatro categorias a seguir:

  • Capacidade de processamento do binder (disponível em system/libhwbinder/vts/performance/Benchmark_binder.cpp)
  • Latência de vinculação (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)
  • Latência do hwbinder (disponível em system/libhwbinder/vts/performance/Latency.cpp)

Sobre o binder e o hwbinder

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

Proporção fichário hwbinder
Objetivo Fornecer um esquema de IPC de uso geral para o framework Comunicar-se com hardware
Propriedade Otimizado para uso do framework do Android Latência mínima de overhead
Mudar a política de programação para primeiro/segundo plano Sim Não
Como passar argumentos Usa a serialização com suporte do objeto Parcel Usa buffers de dispersão e evita a sobrecarga para copiar dados necessários para a serialização de pacotes.
Herança de prioridade Não Sim

Processos de binder e hwbinder

Um visualizador de systrace mostra as transações da seguinte maneira:

Figura 1. Visualização do Systrace dos processos de vinculação.

No exemplo acima:

  • Os quatro (4) processos schd-dbg são processos de cliente.
  • Os quatro (4) processos de vinculação são processos do servidor (o nome começa com Binder e termina com um número de sequência).
  • Um processo do cliente é sempre associado a um processo do servidor, que é dedicado ao cliente.
  • Todos os pares de processos cliente-servidor são programados de forma independente pelo kernel simultaneamente.

Na CPU 1, o kernel do SO 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, processar a solicitação e alternar o contexto de volta após a conclusão da solicitação.

Capacidade de processamento x latência

Em uma transação perfeita, em que o processo do cliente e do servidor muda sem problemas, os testes de throughput e latência não produzem mensagens substancialmente diferentes. No entanto, quando o kernel do SO está processando uma solicitação de interrupção (IRQ, na sigla em inglês) de hardware, aguardando bloqueios ou simplesmente escolhendo não processar 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 na latência.

O teste de throughput gera um grande número de transações com diferentes tamanhos de payload, fornecendo uma boa estimativa para o tempo de transação normal (nos melhores cenários) e a capacidade máxima que o vinculamento pode alcançar.

Por outro lado, o teste de latência não realiza ações no payload para minimizar o tempo de transação normal. Podemos usar o tempo de transação para estimar o overhead do vinculador, criar estatísticas para o pior caso e calcular a proporção de transações cuja latência atende a um prazo especificado.

Processar inversões de prioridade

Uma inversão de prioridade ocorre quando uma linha de execução com prioridade mais alta está esperando por uma linha de execução com prioridade mais baixa. Os aplicativos em tempo real (RT, na sigla em inglês) têm um problema de inversão de prioridade:

Figura 3. Inversão de prioridade em aplicativos em tempo real.

Ao usar a programação do Linux Completely Fair Scheduler (CFS), uma linha de execução sempre tem a chance de ser executada, mesmo quando outras linhas de execução têm uma prioridade mais alta. Como resultado, os aplicativos com programação de CFS processam a inversão de prioridade como comportamento esperado e não como um problema. Nos casos em que o framework do Android precisa de programação de RT para garantir o privilégio de linhas de execução de alta prioridade, a inversão de prioridade precisa ser resolvida.

Exemplo de inversão de prioridade durante uma transação de vinculação (a linha de execução de RT é logicamente bloqueada por outras linhas de execução de CFS ao aguardar que uma linha de execução de vinculação seja servida):

Figura 4. Inversão de prioridade, linhas de execução bloqueadas em tempo real.

Para evitar bloqueios, use a herança de prioridade para elevar temporariamente a linha de execução do Binder para uma linha de execução RT quando ela atender uma solicitação de um cliente RT. Lembre-se de que a programação de RT tem recursos limitados e deve ser usada com cuidado. Em um sistema com n CPUs, o número máximo de linhas de execução RT atuais também é n. Outras linhas de execução RT podem precisar esperar (e, portanto, perder os prazos) se todas as CPUs estiverem ocupadas por outras linhas de execução RT.

Para resolver todas as possíveis inversões de prioridade, use a herança de prioridade para o binder e o hwbinder. No entanto, como o binder é amplamente usado em todo o sistema, ativar a herança de prioridade para transações de binder pode spammar o sistema com mais linhas de execução em tempo real do que ele pode atender.

Executar testes de capacidade

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

  • O teste de throughput do binder está em system/libhwbinder/vts/performance/Benchmark_binder.cpp.
  • O teste de throughput do hwbinder está em system/libhwbinder/vts/performance/Benchmark.cpp.

Resultados dos testes

Exemplo de resultados do teste de throughput para transações que usam diferentes tamanhos de payload:

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
  • Tempo indica o atraso de ida e volta medido em tempo real.
  • CPU indica o tempo acumulado quando as CPUs são programadas para o teste.
  • Iterações indica o número de vezes que a função de teste foi executada.

Por exemplo, para um payload de 8 bytes:

BM_sendVec_binderize/8         69974 ns      32700 ns      21296

… a capacidade máxima de processamento que o vinculador pode alcançar é calculada como:

Capacidade MÁXIMA com payload de 8 bytes = (8 * 21296)/69974 ~= 2.423 b/ns ~= 2.268 Gb/s

Opções de teste

Para receber 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"
    },
   .
}

Executar testes de latência

O teste de latência mede o tempo que o cliente leva para começar a inicializar a transação, mudar para o processo do servidor para processamento e receber o resultado. O teste também procura comportamentos conhecidos de programadores incorretos que podem afetar negativamente a latência da transação, como um programador que não oferece suporte à herança de prioridade ou à flag 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.

Resultados 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 minutos.
-v Receba uma saída detalhada (depuração).
-trace Interromper o rastreamento quando um prazo for atingido.

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

Especificar iterações

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

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"
}

Os resultados do 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 é de 2.500us (2,5 ms). A maioria das transações precisa atender a 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 em que 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 autor da chamada de prioridade normal. Duas transações miss o prazo, fazendo com que a proporção de atendimento (meetR) seja 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 do que o other_ms, com valores avg e wst mais baixos e um meetR mais alto. A diferença pode ser ainda mais significativa com a carga em segundo plano.

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

Especificar valores de par

Cada processo do cliente é associado a um processo do servidor dedicado ao cliente, e cada par pode ser programado de forma independente para qualquer CPU. No entanto, a migração da CPU não deve acontecer durante uma transação, desde que a flag SYNC seja honor.

Verifique se o sistema não está sobrecarregado. Embora a latência alta em um sistema sobrecarregado seja esperada, os resultados do 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.

Especificar valores de prazo

Após testes extensivos de cenários de usuário (execução do teste de latência em um produto qualificado), determinamos que 2,5 ms é o prazo a ser atendido. Para novos aplicativos com requisitos mais altos (como 1.000 fotos/segundo), esse valor de prazo vai mudar.

Especificar saída detalhada

O uso da opção -v exibe uma 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
  • A linha de execução de serviço é criada com uma prioridade SCHED_OTHER e executada em CPU:1 com pid 8674.
  • A primeira transação é iniciada por um fifo-caller. Para atender a essa transação, o hwbinder faz upgrade da prioridade do servidor (pid: 8674 tid: 8676) para 99 e também a marca com uma classe de programação temporária (impresso como ???). O programador coloca o processo do servidor em CPU:0 para execução e o sincroniza com a mesma CPU com o cliente.
  • O autor da chamada da segunda transação tem uma prioridade SCHED_OTHER. O servidor é rebaixado e atende o autor da chamada com prioridade SCHED_OTHER.

Usar o rastreamento para depuração

É possível 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 uma latência inadequada é 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 componentes a seguir podem afetar a latência:

  • Modo de build do Android. O modo Eng geralmente é mais lento que o modo userdebug.
  • Framework. Como o serviço de framework usa ioctl para configurar o vinculador?
  • Driver de vinculação. O driver oferece suporte a bloqueio de granularidade fina? Ele contém todos os patches de mudança de desempenho?
  • Versão do kernel. Quanto melhor for a capacidade do kernel em tempo real, 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?
  • Programador do kernel. O kernel tem um programador com economia de energia (EAS) ou um programador de processamento heterogêneo (HMP, na sigla em inglês)? Algum driver do kernel (cpu-freq, cpu-idle, cpu-hotplug etc.) afeta o programador?