API Performance Hint

Data di uscita:

Android 12 (livello API 31) - API Performance Hint

Android 13 (livello API 33) - Performance Hint Manager nell'API NDK

(Anteprima) Android 15 (DP1) - reportActualWorkDuration()

Con i suggerimenti sulle prestazioni della CPU, un gioco può influenzare il comportamento dinamico delle prestazioni della CPU per soddisfare meglio le sue esigenze. Nella maggior parte dei dispositivi, Android regola dinamicamente la velocità di clock della CPU e il tipo di core per un workload in base alle richieste precedenti. Se un workload utilizza più risorse CPU, la velocità di clock viene aumentata e il workload viene infine spostato su un core più grande. Se il workload utilizza meno risorse, Android riduce l'allocazione delle risorse. Con ADPF, l'applicazione o il gioco può inviare un segnale aggiuntivo sulle proprie prestazioni e scadenze. Ciò aiuta il sistema ad aumentare più rapidamente la frequenza (migliorando le prestazioni) e a ridurla velocemente quando il carico di lavoro è completato (risparmiando energia).

Velocità di clock

Quando i dispositivi Android regolano dinamicamente la velocità di clock della CPU, la frequenza può modificare le prestazioni del codice. Progettare un codice che gestisca le velocità di clock dinamiche è importante per massimizzare le prestazioni, mantenere uno stato termico sicuro e utilizzare l'energia in modo efficiente. Non puoi assegnare direttamente le frequenze della CPU nel codice dell'app. Di conseguenza, un modo comune per le app di tentare di funzionare a velocità di clock della CPU più elevate è eseguire un ciclo occupato in un thread in background in modo che il carico di lavoro sembri più impegnativo. Si tratta di una pratica errata, in quanto spreca energia e aumenta il carico termico sul dispositivo quando l'app non utilizza effettivamente le risorse aggiuntive. L'API PerformanceHint della CPU è progettata per risolvere questo problema. Se comunichi al sistema la durata effettiva del lavoro e la durata target del lavoro, Android sarà in grado di ottenere una panoramica delle esigenze di CPU dell'app e allocare le risorse in modo efficiente. In questo modo, le prestazioni saranno ottimali con un livello di consumo energetico efficiente.

Tipi di nucleo

I tipi di core della CPU su cui viene eseguito il gioco sono un altro fattore importante per le prestazioni. I dispositivi Android spesso modificano dinamicamente il core della CPU assegnato a un thread in base al recente comportamento del carico di lavoro. L'assegnazione dei core della CPU è ancora più complessa sui SoC con più tipi di core. Su alcuni di questi dispositivi, i core più grandi possono essere utilizzati solo brevemente senza entrare in uno stato termicamente insostenibile.

Il tuo gioco non deve tentare di impostare l'affinità dei core della CPU per i seguenti motivi:

  • Il tipo di core migliore per un workload varia in base al modello del dispositivo.
  • La sostenibilità dell'esecuzione di core più grandi varia in base al SoC e alle varie soluzioni termiche fornite da ogni modello di dispositivo.
  • L'impatto ambientale sullo stato termico può complicare ulteriormente la scelta del core. Ad esempio, il meteo o una cover per smartphone possono modificare lo stato termico di un dispositivo.
  • La selezione dei core non può ospitare nuovi dispositivi con prestazioni e capacità termiche aggiuntive. Di conseguenza, i dispositivi spesso ignorano l'affinità del processore di un gioco.

Esempio di comportamento predefinito dello scheduler Linux

Comportamento dello scheduler Linux
Figura 1. Il governor può impiegare circa 200 ms per aumentare o diminuire la frequenza della CPU. ADPF funziona con il sistema di scalabilità dinamica di tensione e frequenza (DVFS) per fornire le migliori prestazioni per watt

L'API PerformanceHint astrae più delle latenze DVFS

ADPF estrae più di latenze DVFS
Figura 2. ADPF sa come prendere la decisione migliore per tuo conto
  • Se le attività devono essere eseguite su una CPU specifica, l'API PerformanceHint sa come prendere questa decisione per tuo conto.
  • Pertanto, non è necessario utilizzare l'affinità.
  • I dispositivi sono dotati di varie topologie; le caratteristiche di alimentazione e termiche sono troppo varie per essere esposte allo sviluppatore di app.
  • Non puoi fare ipotesi sul sistema sottostante su cui stai lavorando.

Soluzione

ADPF fornisce la classe PerformanceHintManager, in modo che i giochi possano inviare suggerimenti sulle prestazioni ad Android per la velocità di clock della CPU e il tipo di core. Il sistema operativo può quindi decidere come utilizzare al meglio i suggerimenti in base al SoC e alla soluzione termica del dispositivo. Se la tua app utilizza questa API insieme al monitoraggio dello stato termico, può fornire suggerimenti più informati al sistema operativo anziché utilizzare loop occupati e altre tecniche di codifica che possono causare la limitazione.

Ecco come un gioco utilizza i suggerimenti sulle prestazioni:

  1. Crea sessioni di suggerimenti per i thread chiave che si comportano in modo simile. Ad esempio:
    • Il thread di rendering e le relative dipendenze ottengono una sessione
      1. In Cocos, il thread principale del motore e il thread di rendering ottengono una sessione
      2. In Unity, integra il plug-in del fornitore Android di Adaptive Performance.
      3. In Unreal, integra il plug-in Unreal Adaptive Performance e utilizza le opzioni di scalabilità per supportare più livelli di qualità.
    • I thread I/O ottengono un'altra sessione
    • I thread audio ricevono una terza sessione
  2. Il gioco deve farlo in anticipo, almeno 2 ms e preferibilmente più di 4 ms prima che una sessione richieda maggiori risorse di sistema.
  3. In ogni sessione di suggerimenti, prevedi la durata necessaria per l'esecuzione di ogni sessione. La durata tipica è equivalente a un intervallo di frame, ma l'app può utilizzare un intervallo più breve se il carico di lavoro non varia in modo significativo tra i frame.

Ecco come mettere in pratica la teoria:

Inizializza PerformanceHintManager e creaHintSession

Ottieni il gestore utilizzando il servizio di sistema e crea una sessione di suggerimenti per il thread o il gruppo di thread che lavorano sullo stesso workload.

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);

Imposta i thread, se necessario

Data di uscita:

Android 11 (livello API 34)

Utilizza la funzione setThreads di PerformanceHintManager.Session quando hai altri thread che devono essere aggiunti in un secondo momento. Ad esempio, se crei il thread di fisica in un secondo momento e devi aggiungerlo alla sessione, puoi utilizzare questa 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 scegli come target livelli API inferiori, dovrai distruggere la sessione e ricreare una nuova sessione ogni volta che devi modificare gli ID thread.

Report Actual Work Duration

Tieni traccia della durata effettiva necessaria per completare il lavoro in nanosecondi e segnalala al sistema al termine del lavoro in ogni ciclo. Ad esempio, se si tratta dei thread di rendering, chiama questo metodo su ogni frame.

Per ottenere l'ora effettiva in modo affidabile, utilizza:

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();

Ad esempio:

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);

Aggiornare la durata del lavoro target quando necessario

Ogni volta che la durata del lavoro target cambia, ad esempio se il giocatore sceglie un fps target diverso, chiama il metodo updateTargetWorkDuration per comunicare al sistema in modo che il sistema operativo possa regolare le risorse in base al nuovo target. Non devi chiamarlo a ogni frame e devi chiamarlo solo quando la durata target cambia.

C++

APerformanceHint_updateTargetWorkDuration(hint_session, target_duration);

Java

hintSession.updateTargetWorkDuration(targetDuration);