Interfejs Performance Hint API

Data premiery:

Android 12 (poziom 31 interfejsu API) – interfejs Performance Hint API

Android 13 (poziom 33 interfejsu API) – Performance Hint Manager w interfejsie NDK API

(Wersja zapoznawcza) Android 15 (DP1) – reportActualWorkDuration()

Dzięki wskazówkom dotyczącym wydajności procesora gra może wpływać na dynamiczną wydajność procesora, aby lepiej dopasować ją do swoich potrzeb. Na większości urządzeń Android dynamicznie dostosowuje szybkość zegara procesora i typ rdzenia do zadań na podstawie poprzednich wymagań. Jeśli zadanie wykorzystuje więcej zasobów procesora, zwiększa się szybkość zegara i ostatecznie zadanie jest przenoszone na większy rdzeń. Jeśli zadanie wykorzystuje mniej zasobów, Android zmniejsza ich przydział. Dzięki ADPF aplikacja lub gra może wysyłać dodatkowy sygnał dotyczący jej wydajności i terminów. Dzięki temu system może szybciej zwiększać wydajność i szybciej obniżać taktowanie po zakończeniu pracy (co pozwala oszczędzać energię).

Taktowanie

Gdy urządzenia z Androidem dynamicznie dostosowują częstotliwość zegara procesora, może to wpłynąć na wydajność kodu. Projektowanie kodu, który uwzględnia dynamiczne prędkości zegara, jest ważne dla maksymalizacji wydajności, utrzymania bezpiecznego stanu termicznego i efektywnego wykorzystania energii. Nie możesz bezpośrednio przypisywać częstotliwości procesora w kodzie aplikacji. Dlatego aplikacje często próbują działać z wyższą częstotliwością zegara procesora, uruchamiając pętlę w wątku w tle, aby obciążenie wydawało się większe. Jest to zła praktyka, ponieważ marnuje energię i zwiększa obciążenie termiczne urządzenia, gdy aplikacja nie korzysta z dodatkowych zasobów. Interfejs API PerformanceHint CPU został zaprojektowany, aby rozwiązać ten problem. Gdy poinformujesz system o rzeczywistym i docelowym czasie trwania zadania, Android będzie mógł uzyskać przegląd potrzeb aplikacji w zakresie procesora i efektywnie przydzielać zasoby. Zapewni to optymalną wydajność przy niskim zużyciu energii.

Typy rdzeni

Rodzaje rdzeni procesora, na których działa gra, to kolejny ważny czynnik wpływający na wydajność. Urządzenia z Androidem często dynamicznie zmieniają rdzeń procesora przypisany do wątku na podstawie niedawnego zachowania zadania. Przypisywanie rdzeni procesora jest jeszcze bardziej złożone w przypadku układów SoC z wieloma typami rdzeni. Na niektórych z tych urządzeń większe rdzenie mogą być używane tylko przez krótki czas, zanim osiągną stan, w którym temperatura będzie zbyt wysoka.

Gra nie powinna próbować ustawiać powiązania rdzeni procesora z tych powodów:

  • Najlepszy typ rdzenia dla danego zadania zależy od modelu urządzenia.
  • Zrównoważone działanie większych rdzeni zależy od układu SoC i różnych rozwiązań termicznych stosowanych w poszczególnych modelach urządzeń.
  • Wpływ środowiska na stan termiczny może dodatkowo skomplikować wybór rdzenia. Na przykład pogoda lub etui na telefon mogą zmienić stan termiczny urządzenia.
  • Podstawowy wybór nie obejmuje nowych urządzeń o większej wydajności i lepszych możliwościach termicznych. W rezultacie urządzenia często ignorują powiązanie procesora z grą.

Przykład domyślnego działania harmonogramu systemu Linux

Działanie harmonogramu Linuksa
Rysunek 1. Zmiana częstotliwości procesora przez regulator może potrwać około 200 ms. ADPF współpracuje z systemem Dynamic Voltage and Frequency Scaling (DVFS), aby zapewnić najlepszą wydajność na wat

Interfejs PerformanceHint API obejmuje więcej niż tylko opóźnienia DVFS

ADPF Abstracts more than DVFS Latencies
Rysunek 2. ADPF wie, jak podjąć najlepszą decyzję w Twoim imieniu
  • Jeśli zadania muszą być wykonywane na określonym procesorze, interfejs PerformanceHint API wie, jak podjąć tę decyzję w Twoim imieniu.
  • Dlatego nie musisz używać podobieństwa.
  • Urządzenia mają różne topologie. Charakterystyka zasilania i termiczna jest zbyt zróżnicowana, aby można było ją udostępnić deweloperowi aplikacji.
  • Nie możesz zakładać niczego na temat systemu, w którym działasz.

Rozwiązanie

ADPF udostępnia klasę PerformanceHintManager, dzięki której gry mogą wysyłać do Androida wskazówki dotyczące wydajności w zakresie szybkości zegara procesora i typu rdzenia. System operacyjny może wtedy zdecydować, jak najlepiej wykorzystać wskazówki na podstawie układu SoC i rozwiązania termicznego urządzenia. Jeśli Twoja aplikacja korzysta z tego interfejsu API wraz z monitorowaniem stanu termicznego, może przekazywać do systemu operacyjnego bardziej precyzyjne wskazówki zamiast używać pętli zajętości i innych technik kodowania, które mogą powodować ograniczanie przepustowości.

Oto jak gra korzysta ze wskazówek dotyczących wydajności:

  1. Twórz sesje podpowiedzi dla kluczowych wątków, które zachowują się podobnie. Na przykład:
    • Wątek renderowania i jego zależności otrzymują 1 sesję.
      1. W Cocos główny wątek silnika i wątek renderowania otrzymują jedną sesję.
      2. W Unity zintegruj wtyczkę Adaptive Performance Android Provider.
      3. W Unreal zintegruj wtyczkę Unreal Adaptive Performance i użyj opcji skalowalności, aby obsługiwać wiele poziomów jakości.
    • Wątki wejścia/wyjścia otrzymują kolejną sesję
    • Wątki audio otrzymują trzecią sesję
  2. Gra powinna to zrobić odpowiednio wcześnie, co najmniej 2 ms, a najlepiej ponad 4 ms przed tym, jak sesja będzie wymagać większych zasobów systemowych.
  3. W każdej sesji podpowiedzi przewiduj czas trwania każdej sesji. Typowy czas trwania jest równy interwałowi klatek, ale aplikacja może używać krótszego interwału, jeśli obciążenie nie różni się znacząco w przypadku poszczególnych klatek.

Aby przełożyć teorię na praktykę:

Inicjowanie interfejsu PerformanceHintManager i tworzenie sesji podpowiedzi

Pobierz menedżera za pomocą usługi systemowej i utwórz sesję podpowiedzi dla wątku lub grupy wątków pracujących nad tym samym zadaniem.

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

W razie potrzeby ustaw wątki.

Data premiery:

Android 11 (poziom 34 interfejsu API)

Użyj funkcji setThreadsPerformanceHintManager.Session, gdy masz inne wątki, które trzeba będzie dodać później. Jeśli na przykład utworzysz wątek fizyki później i musisz dodać go do sesji, możesz użyć tego setThreadsinterfejsu API.

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

Jeśli kierujesz reklamy na niższe poziomy interfejsu API, musisz niszczyć sesję i tworzyć nową za każdym razem, gdy chcesz zmienić identyfikatory wątków.

Raportowanie rzeczywistego czasu pracy

Śledź rzeczywisty czas potrzebny na wykonanie pracy w nanosekundach i zgłaszaj go do systemu po zakończeniu pracy w każdym cyklu. Jeśli na przykład dotyczy to wątków renderowania, wywołuj tę funkcję w każdej klatce.

Aby uzyskać wiarygodny czas rzeczywisty, użyj:

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

Na przykład:

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

W razie potrzeby zaktualizuj docelowy czas pracy

Za każdym razem, gdy zmieni się docelowy czas pracy, np. gdy gracz wybierze inny docelowy FPS, wywołaj metodę updateTargetWorkDuration, aby poinformować o tym system. Dzięki temu system operacyjny będzie mógł dostosować zasoby do nowego celu. Nie musisz wywoływać tej funkcji w każdej klatce. Wystarczy, że zrobisz to, gdy zmieni się docelowy czas trwania.

C++

APerformanceHint_updateTargetWorkDuration(hint_session, target_duration);

Java

hintSession.updateTargetWorkDuration(targetDuration);