API de Performance Hint

Lanzamiento:

Android 12 (nivel de API 31): API de Performance Hint

Android 13 (nivel de API 33): Administrador de sugerencias de rendimiento en la API del NDK

(Versión preliminar) Android 15 (DP1) - reportActualWorkDuration()

Con las sugerencias de rendimiento de CPU, un juego puede influir en el comportamiento dinámico del rendimiento de la CPU para satisfacer mejor sus necesidades. En la mayoría de los dispositivos, Android ajusta de forma dinámica la velocidad del reloj de la CPU y el tipo de núcleo para una carga de trabajo según las demandas anteriores. Si una carga de trabajo usa más recursos de CPU, la velocidad del reloj aumenta y la carga de trabajo se traslada a un núcleo más grande. Si la carga de trabajo usa menos recursos, Android reduce la asignación de recursos. Con ADPF, la aplicación o el juego puede enviar un indicador adicional sobre su rendimiento y los plazos. Esto ayuda a que el sistema se incremente de forma más agresiva (mejorando el rendimiento) y bajar los relojes rápidamente cuando se completa la carga de trabajo (ahorro de uso de energía).

Velocidad de reloj

Cuando los dispositivos Android ajustan dinámicamente la velocidad del reloj de la CPU, la frecuencia puede cambiar el rendimiento de tu código. Diseñar un código que aborde las velocidades de reloj dinámicas es importante para maximizar el rendimiento, mantener un estado térmico seguro y usar la energía de manera eficiente. No puedes asignar frecuencias de CPU de forma directa en el código de la app. En consecuencia, una forma común para que las apps intenten ejecutarse a velocidades de reloj de CPU más altas es ejecutar un bucle ocupado en un subproceso en segundo plano para que la carga de trabajo parezca más exigente. Esta es una práctica no recomendada, ya que desperdicia energía y aumenta la carga térmica del dispositivo cuando la app no está usando los recursos adicionales. La API de PerformanceHint de la CPU está diseñada para solucionar este problema. Si permites que el sistema conozca la duración real y la duración objetivo del trabajo, Android podrá obtener una descripción general de las necesidades de CPU de la app y asignar recursos de manera eficiente. Esto generará un rendimiento óptimo con un nivel de consumo de energía eficiente.

Tipos principales

Los tipos de núcleo de CPU en los que se ejecuta el juego son otro factor de rendimiento importante. Los dispositivos Android suelen cambiar el núcleo de CPU asignado a un subproceso de forma dinámica según el comportamiento reciente de la carga de trabajo. La asignación de núcleo de CPU es aún más compleja en SoCs con varios tipos de núcleo. En algunos de estos dispositivos, los núcleos más grandes solo se pueden usar brevemente sin pasar a un estado térmico insostenible.

El juego no debería intentar establecer la afinidad de núcleo de CPU por los siguientes motivos:

  • El mejor tipo de núcleo para una carga de trabajo varía según el modelo de dispositivo.
  • La sostenibilidad de ejecutar núcleos más grandes varía según el SoC y las diversas soluciones térmicas proporcionadas por cada modelo de dispositivo.
  • El impacto ambiental en el estado térmico puede complicar aún más la elección principal. Por ejemplo, el clima o una funda de teléfono pueden cambiar el estado térmico de un dispositivo.
  • La selección de núcleo no admite dispositivos nuevos con rendimiento y capacidades térmicas adicionales. Como resultado, los dispositivos a menudo ignoran la afinidad de procesador de un juego.

Ejemplo de comportamiento predeterminado del programador de Linux

Comportamiento de Linux Scheduler
Figura 1: El controlador puede tardar ~200 ms en aumentar o disminuir la frecuencia de CPU. ADPF funciona con el sistema de escalamiento dinámico de voltaje y frecuencia (DVFS) para proporcionar el mejor rendimiento por vatio.

La API de PerformanceHint abstrae más que las latencias de DVFS.

ADPF abstrae más que las latencias de DVFS.
Figura 2: El ADPF sabe cómo tomar la mejor decisión por ti
  • Si las tareas deben ejecutarse en una CPU específica, la API de PerformanceHint sabe cómo tomar esa decisión por ti.
  • Por lo tanto, no necesitas usar la afinidad.
  • Los dispositivos tienen varias topologías. Las características térmicas y de potencia son demasiado variadas para exponerlas al desarrollador de apps.
  • No puedes hacer suposiciones sobre el sistema subyacente en el que te ejecutas.

Solución

ADPF proporciona la clase PerformanceHintManager para que los juegos puedan enviar sugerencias de rendimiento a Android sobre la velocidad de reloj de la CPU y el tipo de núcleo. Luego, el SO puede decidir la mejor manera de usar las sugerencias basadas en el SoC y la solución térmica del dispositivo. Si la app usa esta API junto con la supervisión de estado térmico, se puede proporcionar sugerencias más fundamentadas al SO en lugar de usar bucles ocupados y otras técnicas de codificación que puedan causar limitaciones.

Así es como un juego usa sugerencias de rendimiento:

  1. Crea sesiones de sugerencias para subprocesos clave que se comporten de manera similar. Por ejemplo:
    • El subproceso de renderización y sus dependencias obtienen una sesión.
      1. En Cocos, el subproceso del motor principal y el subproceso de renderización obtienen una sesión.
      2. En Unity, integra el complemento de Adaptive Performance Android Provider en Unity.
      3. En Unreal, integra el complemento de rendimiento adaptable de Unreal y usa las opciones de escalabilidad para admitir varios niveles de calidad.
    • Los subprocesos de IO obtienen otra sesión
    • Los subprocesos de audio obtienen una tercera sesión.
  2. El juego debe hacer esto con tiempo, al menos 2 ms y preferentemente más de 4 ms antes de que una sesión necesite aumentar los recursos del sistema.
  3. En cada sesión de sugerencias, se predice la duración necesaria para que cada sesión se ejecute. La duración típica es equivalente a un intervalo de fotogramas, pero la app puede usar un intervalo más corto si la carga de trabajo no varía significativamente entre los fotogramas.

A continuación, te mostramos cómo poner la teoría en práctica:

Inicializa PerformanceHintManager y createHintSession

Obtén el administrador mediante el servicio del sistema y crea una sesión de sugerencias para tu subproceso o grupo de subprocesos que trabajen en la misma carga de trabajo.

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

Configura subprocesos si es necesario

Lanzamiento:

Android 11 (nivel de API 34)

Usa la función setThreads de PerformanceHintManager.Session cuando tengas otros subprocesos que debas agregar más adelante. Por ejemplo, si creas tu subproceso de física más adelante y necesitas agregarlo a la sesión, puedes usar esta API de 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);

Si te orientas a niveles de API inferiores, deberás destruir la sesión y volver a crear una nueva cada vez que necesites cambiar los ID del subproceso.

Informar la duración real del trabajo

Realiza un seguimiento de la duración real necesaria para completar el trabajo en nanosegundos y, luego, informa al sistema cuando se complete el trabajo en cada ciclo. Por ejemplo, si esto es para tus subprocesos de renderización, llámalo en cada fotograma.

Para obtener la hora real de manera confiable, usa lo siguiente:

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 ejemplo:

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

Actualiza la duración del trabajo objetivo cuando sea necesario

Cuando cambie la duración objetivo del trabajo, por ejemplo, si el jugador elige un FPS objetivo diferente, llama al método updateTargetWorkDuration para informarle al sistema que pueda ajustar los recursos de acuerdo con el objetivo nuevo. No es necesario que la llames en cada fotograma y solo debes hacerlo cuando cambia la duración objetivo.

C++

APerformanceHint_updateTargetWorkDuration(hint_session, target_duration);

Java

hintSession.updateTargetWorkDuration(targetDuration);