API Performance Hint

Lançamento:

Android 12 (API de nível 31): API Performance Hint

Android 13 (API de nível 33): Gerenciador de dicas de desempenho na API NDK

(Prévia) Android 15 (DP1): reportActualWorkDuration()

Com as dicas de desempenho da CPU, um jogo pode influenciar o comportamento dinâmico do desempenho da CPU para atender melhor às próprias necessidades. Na maioria dos dispositivos, o Android ajusta dinamicamente a velocidade do clock e o tipo de núcleo da CPU para uma carga de trabalho com base nas demandas anteriores. Se uma carga de trabalho usar mais recursos da CPU, a velocidade do clock aumentará e a carga de trabalho será movida para um núcleo maior. Se a carga de trabalho usar menos recursos, o Android diminuirá a alocação de recursos. Com o ADPF, o app ou jogo pode enviar um sinal adicional sobre o desempenho e os prazos. Isso ajuda o sistema a acelerar o processo de forma mais agressiva (melhorando o desempenho) e a reduzir os relógios rapidamente quando a carga de trabalho estiver concluída (economizando energia).

Velocidade do relógio

Quando os dispositivos Android ajustam dinamicamente a velocidade do clock da CPU, a frequência pode mudar o desempenho do código. Projetar um código que aborde velocidades dinâmicas do clock é importante para maximizar o desempenho, manter um estado térmico seguro e usar energia de maneira eficiente. Não é possível atribuir frequências de CPU diretamente no código do app. Como resultado, uma maneira comum de os apps tentarem ser executados em velocidades mais altas do clock da CPU é executar um loop ocupado em uma linha de execução em segundo plano para que a carga de trabalho pareça mais exigente. Essa é uma prática não recomendada, porque desperdiça energia e aumenta a carga térmica no dispositivo quando o app não está realmente usando os outros recursos. A API CPU PerformanceHint foi projetada para resolver esse problema. Ao permitir que o sistema saiba a duração real do trabalho e a duração pretendida, o Android poderá ter uma visão geral das necessidades de CPU do app e alocar recursos com eficiência. Isso vai gerar um desempenho ideal no nível de consumo eficiente de energia.

Tipos de núcleo

Os tipos de núcleo da CPU em que seu jogo é executado são outro fator de performance importante. Os dispositivos Android geralmente mudam o núcleo da CPU atribuído a uma linha de execução de forma dinâmica com base no comportamento recente da carga de trabalho. A atribuição de núcleo da CPU é ainda mais complexa em SoCs com vários tipos de núcleo. Em alguns desses dispositivos, os núcleos maiores só podem ser usados de forma breve, porque atingem rapidamente um estado térmico insustentável.

Seu jogo não deve tentar definir a afinidade de núcleo da CPU pelos seguintes motivos:

  • O melhor tipo de núcleo para a carga de trabalho varia de acordo com o modelo do dispositivo.
  • A sustentabilidade da execução de núcleos maiores varia de acordo com o SoC e as diversas soluções térmicas fornecidas por cada modelo de dispositivo.
  • O impacto ambiental no estado térmico pode complicar ainda mais a escolha do núcleo. Por exemplo, o clima ou uma capa de smartphone pode mudar o estado térmico de um dispositivo.
  • A seleção do núcleo não acomoda novos dispositivos com recursos térmicos e de performance extras. Como resultado, os dispositivos geralmente ignoram a afinidade entre o processador e um jogo.

Exemplo de comportamento padrão do programador do Linux

Comportamento do programador do Linux
Figura 1. O Governor pode levar cerca de 200 ms para aumentar ou diminuir a frequência da CPU. O ADPF funciona com o sistema de tensão dinâmica e escala de frequência (DVFS, na sigla em inglês) para oferecer o melhor desempenho por watt.

A API PerformanceHint abstrai mais que as latências do DVFS

A ADPF abstrai mais do que as latências do DVFS
Figura 2. A ADPF sabe como tomar a melhor decisão por você.
  • Se as tarefas precisarem ser executadas em uma CPU específica, a API PerformanceHint saberá como tomar essa decisão por você.
  • Portanto, não é necessário usar afinidade.
  • Os dispositivos vêm com várias topologias. As características de energia e térmica variam muito para serem expostas ao desenvolvedor de apps.
  • Você não pode fazer suposições sobre o sistema subjacente em que está sendo executado.

Solução

O ADPF fornece a classe PerformanceHintManager para que os jogos possam enviar dicas de desempenho ao Android para a velocidade do clock e o tipo de núcleo da CPU. Então, o SO pode decidir a melhor forma de usar as dicas com base no SoC e na solução térmica do dispositivo. Se o app usa essa API com o monitoramento de estado térmico, ele pode fornecer dicas mais fundamentadas para o SO, em vez de usar loops ocupados e outras técnicas de programação que podem causar limitações.

Veja como um jogo usa as dicas de performance:

  1. Crie sessões de dicas para as linhas de execução principais que se comportam de maneira semelhante. Por exemplo:
    • A linha de execução de renderização e as dependências dela recebem uma sessão.
      1. No Cocos, as linhas de execução do mecanismo principal e de renderização têm uma sessão (link em inglês)
      2. No Unity, integre o plug-in do provedor de desempenho adaptável do Android (link em inglês).
      3. No Unreal, integre o plug-in Unreal Adaptive Performance e use as opções de escalonamento para oferecer suporte a vários níveis de qualidade
    • As linhas de execução de E/S recebem outra sessão
    • As linhas de execução de áudio recebem uma terceira sessão
  2. O jogo precisa fazer isso pelo menos 2ms ou, de preferência, mais de 4ms antes de uma sessão exigir mais recursos do sistema.
  3. Estima a duração necessária para cada sessão de dica. A duração típica é equivalente a um intervalo de frames, mas o app poderá usar um intervalo mais curto se a carga de trabalho não variar significativamente entre os frames.

Veja como colocar a teoria em prática:

Inicializar PerformanceHintManager e createHintSession

Fazer com que o gerenciador use o serviço do sistema e criar uma sessão de dicas para a linha de execução ou o grupo que trabalha na mesma carga de trabalho.

C++

int32_t tids[1];
tids[0] = gettid();
int64_t target_fps_nanos = getFpsNanos();
APerformanceHintManager* hint_manager = APerformanceHint_getManager();
APerformanceHintSession* hint_session =
  APerformanceHint_createSession(hint_manager, tids, 1, target_fps_nanos);

Java

int[] tids = {
  android.os.Process.myTid()
};
long targetFpsNanos = getFpsNanos();
PerformanceHintManager performanceHintManager =
  (PerformanceHintManager) this.getSystemService(Context.PERFORMANCE_HINT_SERVICE);
PerformanceHintManager.Session hintSession =
  performanceHintManager.createHintSession(tids, targetFpsNanos);

Definir conversas, se necessário

Lançamento:

Android 11 (API de nível 34)

Use a função setThreads do PerformanceHintManager.Session quando tiver outras linhas de execução que precisem ser adicionadas mais tarde. Por exemplo, se você criar a linha de execução de física mais tarde e precisar adicioná-la à sessão, use essa API setThreads.

C++

auto tids = thread_ids.data();
std::size_t size = thread_ids_.size();
APerformanceHint_setThreads(hint_session, tids, size);

Java

int[] tids = new int[3];

// add all your thread IDs. Remember to use android.os.Process.myTid() as that
// is the linux native thread-id.
// Thread.currentThread().getId() will not work because it is jvm's thread-id.
hintSession.setThreads(tids);

Se você estiver segmentando níveis de API mais baixos, vai precisar destruir a sessão e recriar uma nova sempre que precisar mudar os IDs das linhas de execução.

Duração real do trabalho do relatório

Acompanhe a duração real necessária para concluir o trabalho em nanossegundos e informe-a ao sistema após a conclusão do trabalho em cada ciclo. Por exemplo, se for para as linhas de execução de renderização, chame esse método em todos os frames.

Para obter o tempo real de maneira confiável, use:

C++

clock_gettime(CLOCK_MONOTONIC, &clock); // if you prefer "C" way from <time.h>
// or
std::chrono::high_resolution_clock::now(); // if you prefer "C++" way from <chrono>

Java

System.nanoTime();

Por exemplo:

C++

// All timings should be from `std::chrono::steady_clock` or `clock_gettime(CLOCK_MONOTONIC, ...)`
auto start_time = std::chrono::high_resolution_clock::now();

// do work

auto end_time = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::nanoseconds>(end_time - start_time).count();
int64_t actual_duration = static_cast<int64_t>(duration);

APerformanceHint_reportActualWorkDuration(hint_session, actual_duration);

Java

long startTime = System.nanoTime();

// do work

long endTime = System.nanoTime();
long duration = endTime - startTime;

hintSession.reportActualWorkDuration(duration);

Atualize a duração desejada para o trabalho quando necessário

Sempre que a duração de trabalho desejada mudar, por exemplo, se o jogador escolher um QPS diferente, chame o método updateTargetWorkDuration para informar o sistema e, assim, ajustar os recursos de acordo com o novo objetivo. Você não precisa fazer a chamada em todos os frames, basta chamá-la quando a duração desejada mudar.

C++

APerformanceHint_updateTargetWorkDuration(hint_session, target_duration);

Java

hintSession.updateTargetWorkDuration(targetDuration);