Performance Hint API

Veröffentlicht:

Android 12 (API-Level 31) – Performance Hint API

Android 13 (API‑Level 33) – Performance Hint Manager in der NDK API

(Vorabversion) Android 15 (DP1) – reportActualWorkDuration()

Mit CPU-Leistungshinweisen kann ein Spiel das dynamische CPU-Leistungsverhalten beeinflussen, um es besser an seine Anforderungen anzupassen. Auf den meisten Geräten passt Android die CPU-Taktfrequenz und den Kerntyp für eine Arbeitslast dynamisch an die vorherigen Anforderungen an. Wenn für eine Arbeitslast mehr CPU-Ressourcen benötigt werden, wird die Taktfrequenz erhöht und die Arbeitslast wird schließlich auf einen größeren Kern verschoben. Wenn für die Arbeitslast weniger Ressourcen benötigt werden, reduziert Android die Ressourcenzuweisung. Mit ADPF kann die Anwendung oder das Spiel ein zusätzliches Signal zu seiner Leistung und seinen Fristen senden. So kann das System aggressiver hochgefahren werden (was die Leistung verbessert) und die Taktraten schnell gesenkt werden, wenn die Arbeitslast abgeschlossen ist (was den Stromverbrauch senkt).

Taktrate

Wenn Android-Geräte die CPU-Taktfrequenz dynamisch anpassen, kann sich die Frequenz auf die Leistung Ihres Codes auswirken. Es ist wichtig, Code zu entwickeln, der auf dynamische Taktfrequenzen reagiert, um die Leistung zu maximieren, einen sicheren thermischen Zustand aufrechtzuerhalten und Energie effizient zu nutzen. Sie können CPU-Frequenzen nicht direkt in Ihrem App-Code zuweisen. Daher versuchen Apps häufig, mit höheren CPU-Taktraten zu laufen, indem sie eine Busy-Loop in einem Hintergrundthread ausführen, damit die Arbeitslast anspruchsvoller erscheint. Das ist nicht empfehlenswert, da es Strom verschwendet und die thermische Belastung des Geräts erhöht, wenn die App die zusätzlichen Ressourcen nicht tatsächlich nutzt. Die CPU PerformanceHint API wurde entwickelt, um dieses Problem zu beheben. Wenn Sie dem System die tatsächliche und die angestrebte Arbeitsdauer mitteilen, kann Android sich einen Überblick über den CPU-Bedarf der App verschaffen und Ressourcen effizient zuweisen. So wird eine optimale Leistung bei effizientem Stromverbrauch erzielt.

Haupttypen

Die CPU-Kern-Typen, auf denen Ihr Spiel ausgeführt wird, sind ein weiterer wichtiger Leistungsfaktor. Auf Android-Geräten wird der einem Thread zugewiesene CPU-Kern häufig dynamisch auf Grundlage des aktuellen Arbeitslastverhaltens geändert. Die Zuweisung von CPU-Kernen ist bei SoCs mit mehreren Kerntypen noch komplexer. Auf einigen dieser Geräte können die größeren Kerne nur kurz verwendet werden, ohne dass es zu einer thermisch nicht nachhaltigen Situation kommt.

Ihr Spiel sollte aus folgenden Gründen nicht versuchen, die CPU-Kernaffinität festzulegen:

  • Der beste Kerntyp für eine Arbeitslast variiert je nach Gerätemodell.
  • Die Nachhaltigkeit der Ausführung größerer Kerne variiert je nach SoC und den verschiedenen Kühllösungen, die von den einzelnen Gerätemodellen bereitgestellt werden.
  • Die Umweltauswirkungen auf den thermischen Zustand können die Auswahl des Kerns weiter erschweren. Beispielsweise können das Wetter oder eine Smartphone-Hülle den thermischen Zustand eines Geräts verändern.
  • Die Auswahl der Kerne kann nicht an neue Geräte mit zusätzlicher Leistung und zusätzlichen thermischen Funktionen angepasst werden. Daher wird die Prozessoraffinität eines Spiels oft ignoriert.

Beispiel für das Standardverhalten des Linux-Schedulers

Verhalten des Linux-Schedulers
Abbildung 1: Es kann etwa 200 ms dauern, bis die CPU-Frequenz durch den Governor erhöht oder verringert wird. ADPF arbeitet mit dem DVFS-System (Dynamic Voltage and Frequency Scaling) zusammen, um die beste Leistung pro Watt zu erzielen.

Die PerformanceHint API abstrahiert mehr als nur DVFS-Latenzen.

ADPF abstrahiert mehr als DVFS-Latenzen
Abbildung 2. ADPF weiß, wie es in Ihrem Namen die beste Entscheidung trifft.
  • Wenn die Aufgaben auf einer bestimmten CPU ausgeführt werden müssen, kann die PerformanceHint API diese Entscheidung für Sie treffen.
  • Daher müssen Sie keine Affinität verwenden.
  • Geräte haben unterschiedliche Topologien. Die Leistungs- und thermischen Eigenschaften sind zu unterschiedlich, um App-Entwicklern zur Verfügung gestellt zu werden.
  • Sie können keine Annahmen über das zugrunde liegende System treffen, auf dem Sie arbeiten.

Lösung

ADPF bietet die PerformanceHintManager-Klasse, damit Spiele Leistungshinweise für die CPU-Taktfrequenz und den Core-Typ an Android senden können. Das Betriebssystem kann dann entscheiden, wie die Hinweise basierend auf dem SoC und der thermischen Lösung des Geräts am besten verwendet werden. Wenn Ihre App diese API zusammen mit der Überwachung des thermischen Zustands verwendet, kann sie dem Betriebssystem fundiertere Hinweise geben, anstatt Busy-Loops und andere Programmiertechniken zu verwenden, die zu einer Drosselung führen können.

So werden Leistungshinweise in einem Spiel verwendet:

  1. Hinweissitzungen für wichtige Threads erstellen, die sich ähnlich verhalten. Beispiel:
    • Rendering-Thread und seine Abhängigkeiten erhalten eine Sitzung
      1. In Cocos erhalten der Haupt-Engine-Thread und der Render-Thread eine Sitzung.
      2. Binden Sie in Unity das Adaptive Performance Android Provider-Plug-in ein.
      3. In Unreal das Unreal Adaptive Performance-Plug-in einbinden und Skalierbarkeitsoptionen verwenden, um mehrere Qualitätsstufen zu unterstützen
    • IO-Threads erhalten eine weitere Sitzung
    • Audio-Threads erhalten eine dritte Sitzung
  2. Das Spiel sollte dies frühzeitig tun, mindestens 2 ms und vorzugsweise mehr als 4 ms, bevor für eine Sitzung mehr Systemressourcen benötigt werden.
  3. Pro Hinweis-Sitzung wird die Dauer vorhergesagt, die für die Ausführung der jeweiligen Sitzung erforderlich ist. Die typische Dauer entspricht einem Frame-Intervall. Die App kann jedoch ein kürzeres Intervall verwenden, wenn die Arbeitslast zwischen den Frames nicht wesentlich variiert.

So setzen Sie die Theorie in die Praxis um:

PerformanceHintManager initialisieren und createHintSession aufrufen

Rufen Sie den Manager über den Systemdienst ab und erstellen Sie eine Hinweis-Sitzung für Ihren Thread oder Ihre Threadgruppe, die an derselben Arbeitslast arbeitet.

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

Bei Bedarf Threads festlegen

Veröffentlicht:

Android 11 (API-Level 34)

Verwenden Sie die Funktion setThreads des PerformanceHintManager.Session, wenn Sie später weitere Threads hinzufügen müssen. Wenn Sie beispielsweise Ihren Physik-Thread später erstellen und ihn der Sitzung hinzufügen müssen, können Sie diese setThreads API verwenden.

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

Wenn Sie auf niedrigere API-Levels abzielen, müssen Sie die Sitzung beenden und jedes Mal eine neue Sitzung erstellen, wenn Sie die Thread-IDs ändern müssen.

Tatsächliche Arbeitsdauer melden

Die tatsächliche Dauer, die zum Erledigen der Aufgabe benötigt wird, wird in Nanosekunden erfasst und nach Abschluss der Aufgabe in jedem Zyklus an das System gemeldet. Wenn Sie dies beispielsweise für Ihre Rendering-Threads verwenden, rufen Sie diese Funktion bei jedem Frame auf.

Verwenden Sie Folgendes, um die tatsächliche Zeit zuverlässig abzurufen:

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

Beispiel:

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

Zielarbeitszeit bei Bedarf aktualisieren

Wenn sich die angestrebte Arbeitsdauer ändert, z. B. wenn der Spieler eine andere Ziel-FPS auswählt, rufen Sie die Methode updateTargetWorkDuration auf, um das System darüber zu informieren, damit das Betriebssystem die Ressourcen entsprechend dem neuen Ziel anpassen kann. Sie müssen sie nicht für jeden Frame aufrufen, sondern nur, wenn sich die Zieldauer ändert.

C++

APerformanceHint_updateTargetWorkDuration(hint_session, target_duration);

Java

hintSession.updateTargetWorkDuration(targetDuration);