API Performance Hint

Uscita:

Android 12 (livello API 31) - API Performance Hint

Android 13 (livello API 33) - Gestione dei suggerimenti sulle prestazioni nell'API NDK

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

Con i suggerimenti sulle prestazioni della CPU, un gioco può influenzare il comportamento dinamico della CPU per soddisfare meglio le sue esigenze. Android regola dinamicamente la velocità di clock della CPU e il tipo di core per un carico di lavoro in base alle esigenze precedenti. Se un carico di lavoro utilizza più risorse della CPU, la velocità di clock aumenta e il carico di lavoro viene infine spostato su un core più grande. Se il carico di lavoro utilizza meno risorse, Android riduce l'allocazione delle risorse. Con l'ADPF, l'applicazione o il gioco possono inviare un ulteriore indicatore sulle sue prestazioni e sulle sue scadenze. Ciò consente al sistema di crescere in modo più aggressivo (migliorando le prestazioni) e di abbassare rapidamente il numero di blocchi quando il carico di lavoro è completo (risparmiando consumo di energia).

Velocità di clock

Quando i dispositivi Android regolano in modo dinamico la velocità di clock della CPU, la frequenza può influire sulle prestazioni del tuo codice. Progettare un codice che indirizzi a 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 in cui le app tentano di essere eseguite a velocità di clock della CPU più elevate è eseguire un loop impegnativo in un thread in background in modo che il carico di lavoro sembri più impegnativo. Si tratta di una cattiva pratica perché spreca energia e aumenta il carico termico sul dispositivo quando l'app non utilizza effettivamente le risorse aggiuntive. L'API CPU PerformanceHint è progettata per risolvere questo problema. Se meglio il sistema conosce la durata effettiva del lavoro e la durata di lavoro target, Android potrà ottenere una panoramica delle esigenze di CPU dell'app e allocare le risorse in modo efficiente. Questo consente di ottimizzare le prestazioni con un consumo energetico efficiente.

Tipi di core

I tipi di core della CPU su cui viene eseguito il tuo 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 dei carichi 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 per breve tempo, senza raggiungere uno stato termicamente insostenibile.

Il tuo gioco non dovrebbe provare a impostare l'affinità del core della CPU per i seguenti motivi:

  • Il tipo di core migliore per un carico di lavoro 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. Ad esempio, le condizioni meteorologiche o la custodia del telefono possono cambiare lo stato termico di un dispositivo.
  • La selezione di base non può essere supportata su nuovi dispositivi con prestazioni e capacità termiche aggiuntive. Di conseguenza, spesso i dispositivi ignorano l'affinità del processore di un gioco.

Esempio di comportamento dello scheduler Linux predefinito

Comportamento dello scheduler Linux
Figura 1. Il regolatore può impiegare circa 200 ms per aumentare o diminuire la frequenza della CPU. L'ADPF funziona con il sistema DVFS (Dynamic Voltage and Conversion Scaling System) per offrire le migliori prestazioni per watt

L'API PerformanceHint astrae più delle latenze DVFS

L'DPF astratta più delle latenze DVFS
Figura 2. L'DPF sa come prendere la decisione migliore per tuo conto
  • Se le attività devono essere eseguite su una CPU specifica, l'API PerformanceHint sa come prenderla per tuo conto.
  • Pertanto, non hai bisogno di utilizzare l'affinità.
  • I dispositivi presentano varie topologie; le caratteristiche di potenza e termica sono troppo diverse per essere esposte agli sviluppatori di app.
  • Non puoi fare ipotesi sul sistema sottostante in esecuzione.

Soluzione

ADPF offre la classe PerformanceHintManager per consentire ai giochi di inviare suggerimenti sulle prestazioni ad Android per quanto riguarda 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ù consapevoli al sistema operativo invece di utilizzare loop impegnati e altre tecniche di codifica che possono causare la limitazione.

Ecco come un gioco utilizza i suggerimenti per le prestazioni:

  1. Crea sessioni di suggerimento per i thread chiave che si comportano in modo simile. Ad esempio:
    • Il thread di rendering e le sue dipendenze formano una sessione
      1. In Cocos, il thread del motore principale e il thread di rendering riceve una sessione.
      2. In Unity, integra il plug-in del fornitore Android Adaptive Performance
      3. In Unreal, integra il plug-in Unreal Adaptive Performance e utilizza le opzioni di scalabilità per supportare più livelli qualitativi
    • I thread IO ricevono un'altra sessione
    • I thread audio ricevono una terza sessione
  2. Il gioco dovrebbe eseguire questa operazione in anticipo, almeno 2 ms e preferibilmente più di 4 ms prima che una sessione richieda maggiori risorse di sistema.
  3. In ogni sessione del suggerimento, 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 crea HintSession

Ottieni il gestore utilizzando il servizio di sistema e crea una sessione di suggerimento per il thread o il gruppo di thread lavorando sullo stesso carico di lavoro.

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 thread se necessario

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 usare 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 eliminare la sessione e ricrearne una nuova ogni volta che devi modificare gli ID thread.

Segnala durata effettiva del lavoro

Tieni traccia della durata effettiva necessaria per completare il lavoro in nanosecondi e segnalala al sistema al termine del lavoro su ogni ciclo. Ad esempio, se questo è per i thread di rendering, chiamala per 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();

Ecco alcuni esempi:

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

Aggiorna la durata del lavoro target se necessario

Ogni volta che la durata di lavoro target cambia, ad esempio se il player sceglie un f/s 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 su ogni frame, ma devi farlo solo quando la durata target cambia.

C++

APerformanceHint_updateTargetWorkDuration(hint_session, target_duration);

Java

hintSession.updateTargetWorkDuration(targetDuration);