Puedes configurar el registro del sistema para capturar un perfil de CPU y subproceso de tu app durante un período breve. Luego, puedes usar el informe de resultados de un registro del sistema para mejorar el rendimiento de tu juego.
Configura un registro del sistema basado en juegos
La herramienta Systrace está disponible de dos maneras:
Systrace es una herramienta de bajo nivel que tiene las siguientes características:
- Proporciona verdades fundamentales. Systrace captura los resultados directamente del kernel, por lo que las métricas que obtiene son prácticamente idénticas a las que informarían una serie de llamadas al sistema.
- Consume pocos recursos. Systrace presenta una sobrecarga muy baja para el dispositivo (por lo general, inferior al 1%) debido a que transmite datos dentro de un búfer de la memoria.
Configuración óptima
Es importante brindarle a la herramienta un conjunto razonable de argumentos:
- Categorías: El mejor conjunto de categorías que permitirá realizar un registro del sistema basado en el juego es el siguiente: {
sched
,freq
,idle
,am
,wm
,gfx
,view
,sync
,binder_driver
,hal
,dalvik
}. Tamaño del búfer: Como regla general, un tamaño del búfer de 10 MB por núcleo de CPU permite un registro de aproximadamente 20 segundos de duración. Por ejemplo, si un dispositivo tiene dos CPUs con cuatro núcleos (8 núcleos en total), un valor adecuado para pasar al programa
systrace
sería 80,000 KB (80 MB).Si tu juego realiza una gran cantidad de cambios de contexto, aumenta el búfer a 15 MB por núcleo de CPU.
Eventos personalizados: Si defines eventos personalizados para capturarlos en tus juegos, habilita la marca
-a
, que le permite a Systrace incluir estos eventos personalizados en el informe de resultados.
Si usas el programa de línea de comandos systrace
, utiliza el siguiente comando para capturar un registro del sistema que aplique prácticas recomendadas para el conjunto de categorías, el tamaño del búfer y los eventos personalizados:
python systrace.py -a com.example.myapp -b 80000 -o my_systrace_report.html \ sched freq idle am wm gfx view sync binder_driver hal dalvik
Si usas la app del sistema de Systrace en un dispositivo, completa los siguientes pasos para capturar un registro del sistema que aplique prácticas recomendadas para el conjunto de categorías, el tamaño del búfer y los eventos personalizados:
Habilita la opción Registrar aplicaciones depurables.
Para usar esta opción, el dispositivo debe tener 256 MB o 512 MB disponibles (dependiendo de si la CPU tiene 4 u 8 núcleos), y cada porción de memoria de 64 MB debe estar disponible como fragmento contiguo.
Elige Categorías y habilita las categorías de la siguiente lista:
am
: Administrador de actividadesbinder_driver
: Controlador del kernel de Binderdalvik
: Máquina virtual Dalvikfreq
: Frecuencia de CPUgfx
: Gráficoshal
: Módulos de hardwareidle
: CPU inactivasched
: Programación de CPUsync
: Sincronizaciónview
: Sistema de vistaswm
: Administrador de ventanas
Habilita la opción Registrar seguimiento.
Carga tu juego.
Realiza las interacciones de tu juego que correspondan al rendimiento del dispositivo que quieres medir.
Después de encontrar el comportamiento no deseado en tu juego, desactiva el registro del sistema.
Obtuviste las estadísticas de rendimiento necesarias para analizar el problema en profundidad.
Para ahorrar espacio en el disco, los registros del sistema integrados en el dispositivo guardan archivos en un formato de registro comprimido (*.ctrace
). Para descomprimir el archivo cuando se genere un informe, usa el programa de línea de comandos y agrega la opción --from-file
:
python systrace.py --from-file=/data/local/traces/my_game_trace.ctrace \ -o my_systrace_report.html
Mejora áreas de rendimiento específicas
En esta sección, se destacan varias cuestiones de rendimiento comunes de los juegos para dispositivos móviles, y se describe cómo identificar y mejorar esos aspectos en tu juego.
Velocidad de carga
Los jugadores quieren sumergirse en la acción del juego lo antes posible, por lo que es importante que mejores los tiempos de carga de tu juego tanto como puedas. Por lo general, las siguientes medidas ayudan a hacerlo:
- Realiza una carga diferida. Si usas los mismos elementos en escenas o niveles consecutivos de tu juego, cárgalos solo una vez.
- Reduce el tamaño de tus elementos. De esa forma, puedes empaquetar versiones sin comprimir de estos elementos con el APK del juego.
- Usa un método de compresión que haga un uso eficiente del espacio del disco. Por ejemplo, zlib.
- Cómo usar IL2CPP en lugar de mono. (Solo se aplica si utilizas Unity). IL2CPP ofrece un mejor rendimiento de ejecución para tus secuencias de comandos C#.
- Haz que tu juego sea multisubproceso. Para obtener información detallada, consulta la sección de constancia de la velocidad de fotogramas.
Constancia de la velocidad de fotogramas
Uno de los aspectos más importantes de la experiencia de juego es lograr una velocidad de fotogramas constante. Para lograrlo, sigue las técnicas de optimización que se mencionan en esta sección.
Multisubproceso
Cuando se desarrollan juegos para varias plataformas, es normal colocar toda la actividad del juego en un solo subproceso. Si bien este método de ejecución es fácil de implementar en muchos motores de juegos, no resulta óptimo cuando se ejecutan los juegos en dispositivos Android. Como resultado, los juegos de un solo subproceso suelen cargarse lentamente y no tienen una velocidad de fotogramas constante.
El informe de Systrace incluido en la figura 1 muestra el comportamiento típico de un juego que se ejecuta en una sola CPU a la vez:
Para mejorar el rendimiento de tu juego, asegúrate de que tu juego sea multisubproceso. Por lo general, el mejor modelo es el de 2 subprocesos:
- Un subproceso de juego, que contenga los módulos principales del juego y envíe comandos de renderización
- Un subproceso de renderización, que reciba los comandos de procesamiento y los traduzca en comandos de gráfico que una GPU del dispositivo puede usar para mostrar una escena
La API de Vulkan amplía este modelo gracias a su capacidad de actualizar 2 búferes comunes en paralelo. Con esta función, puedes distribuir los subprocesos de renderización en varias CPUs, lo que mejora el tiempo de renderización de una escena.
También puedes realizar los siguientes cambios específicos del motor para mejorar el rendimiento multisubproceso:
- Si vas a desarrollar tu juego con el motor Unity, habilita las opciones Multithreaded Rendering y GPU Skinning.
- Si usas un motor de renderización personalizada, asegúrate de que la canalización de comandos de renderización y la canalización de comandos de gráficos estén alineadas correctamente; de lo contrario, podrían producirse retrasos en la visualización de las escenas del juego.
Después de aplicar los cambios, deberías ver que tu juego ocupa al menos 2 CPUs en simultáneo, como se muestra en la figura 2.
Carga de elementos de la IU
Cuando se crea un juego con muchas funciones, resulta tentador mostrarle varias opciones y acciones al jugador al mismo tiempo. Para mantener la velocidad de fotogramas constante, es importante tener en cuenta el tamaño pequeño de las pantallas de los dispositivos móviles y crear una IU lo más simple posible.
El informe de Systrace que se muestra en la figura 3 es un ejemplo de fotograma de IU que intenta renderizar demasiados elementos para la capacidad de un dispositivo móvil.
Un buen objetivo es reducir el tiempo de actualización de IU a 2 o 3 milisegundos. Para lograr una actualización tan rápida, puedes realizar optimizaciones similares a las siguientes:
- Actualiza solo los elementos en pantalla que se hayan movido.
- Limita la cantidad de texturas y capas de IU. Considera combinar llamadas de gráficos, como sombreadores y texturas, que usen el mismo material.
- Difiere las operaciones de animación de elementos a la GPU.
- Realiza un frustum y aprovechamiento de oclusión más pronunciados.
- Si es posible, realiza operaciones de dibujo con la API de Vulkan. La sobrecarga de llamada de dibujo es más baja en Vulkan.
Consumo de energía
Incluso después de realizar las optimizaciones mencionadas en la sección anterior, es posible que observes que la velocidad de fotogramas de tu juego se deteriora en el transcurso de los primeros 45 o 50 minutos de juego. Además, el dispositivo podría comenzar a recalentarse y consumir más batería con el tiempo.
En muchos casos, este aumento no deseado de temperatura y consumo de energía está relacionado con la manera en que se distribuye la carga de trabajo del juego en las CPUs del dispositivo. Para aumentar la eficiencia de consumo de energía de tu juego, aplica las prácticas recomendadas que aparecen en las siguientes secciones.
Mantén los subprocesos que consumen mucha memoria en una CPU
En muchos dispositivos móviles, las caché L1 residen en CPU específicas, y las caché L2 en un conjunto de CPU que comparten un reloj. Para maximizar los aciertos de la caché L1, te recomendamos que mantengas el subproceso principal de tu juego, junto con los subprocesos que consumen mucha memoria, ejecutándose en una sola CPU.
Difiere el trabajo de poca duración a CPU con poco consumo de energía
La mayoría de los motores de juegos, incluido Unity, saben diferir las operaciones de subprocesos de trabajo a diferentes CPU en cuanto al subproceso principal del juego. Sin embargo, el motor no conoce la arquitectura específica del dispositivo y no puede anticipar la carga de trabajo de tu juego tan bien como tú.
La mayoría de los dispositivos con sistema en chip tienen al menos 2 relojes compartidos: uno para las CPU rápidas y otro para las CPU lentas del dispositivo. Una consecuencia de esta arquitectura es que, si una CPU rápida necesita operar a máxima velocidad, todas las demás CPU operan a máxima velocidad.
El informe de ejemplo de la figura 4 muestra un juego que aprovecha las CPUs rápidas. Sin embargo, este alto nivel de actividad genera un alto consumo de energía y hace que el dispositivo recaliente con rapidez.
Para reducir el consumo general de energía, te recomendamos que le sugieras al programador que se difiera el trabajo de menor duración, como la carga de audio, la ejecución de los subprocesos de trabajo y la ejecución del coreógrafo, al conjunto de CPU lentas del dispositivo. Transfiere tanta carga como sea posible a las CPUs lentas para mantener la velocidad de fotogramas deseada.
La mayoría de los dispositivos muestran las CPUs lentas antes que las rápidas, pero no puedes asumir que el SoC de tu dispositivo las usa en ese orden. Para comprobarlo, ejecuta comandos similares a los en esta sección sobre el descubrimiento de la topología de CPU código en GitHub.
Después de saber cuáles son las CPU lentas de tu dispositivo, puedes declarar afinidades para tus subprocesos de menor duración, y el programador del dispositivo las seguirá. Para ello, agrega el siguiente código a cada subproceso:
#include <sched.h> #include <sys/types.h> #include <unistd.h> pid_t my_pid; // PID of the process containing your thread. // Assumes that cpu0, cpu1, cpu2, and cpu3 are the "slow CPUs". cpu_set_t my_cpu_set; CPU_ZERO(&my_cpu_set); CPU_SET(0, &my_cpu_set); CPU_SET(1, &my_cpu_set); CPU_SET(2, &my_cpu_set); CPU_SET(3, &my_cpu_set); sched_setaffinity(my_pid, sizeof(cpu_set_t), &my_cpu_set);
Aumento considerable de la temperatura
Cuando los dispositivos se sobrecalientan, pueden acelerar la CPU o la GPU, lo que puede afectar a las apps y los juegos de forma inesperada. Los juegos que incorporan gráficos complejos, procesamiento intensivo o actividad de red sostenida tienen más probabilidades de encontrar problemas.
Usa la API térmica para supervisar los cambios de temperatura en el dispositivo y tomar medidas para mantener un consumo de energía y una temperatura del dispositivo más bajos. Cuando el dispositivo informa sobre un aumento considerable de la temperatura, interrumpe las actividades en curso para reducir el uso de energía. Por ejemplo, reduce la velocidad de fotogramas o el teselado poligonal.
Primero, declara el objeto PowerManager
e inicialízalo en el método onCreate()
. Agrégale un objeto de escucha de estado térmico al objeto.
Kotlin
class MainActivity : AppCompatActivity() { lateinit var powerManager: PowerManager override fun onCreate(savedInstanceState: Bundle?) { powerManager = getSystemService(Context.POWER_SERVICE) as PowerManager powerManager.addThermalStatusListener(thermalListener) } }
Java
public class MainActivity extends AppCompatActivity { PowerManager powerManager; @Override protected void onCreate(Bundle savedInstanceState) { ... powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE); powerManager.addThermalStatusListener(thermalListener); } }
Define las acciones que se deben realizar cuando el objeto de escucha detecta un cambio de estado. Si tu juego usa C/C++, agrega código a los niveles del estado térmico en onThermalStatusChanged()
para llamar a tu código de juego nativo con JNI o usar la API térmica nativa.
Kotlin
val thermalListener = object : PowerManager.OnThermalStatusChangedListener() { override fun onThermalStatusChanged(status: Int) { when (status) { PowerManager.THERMAL_STATUS_NONE -> { // No thermal status, so no action necessary } PowerManager.THERMAL_STATUS_LIGHT -> { // Add code to handle light thermal increase } PowerManager.THERMAL_STATUS_MODERATE -> { // Add code to handle moderate thermal increase } PowerManager.THERMAL_STATUS_SEVERE -> { // Add code to handle severe thermal increase } PowerManager.THERMAL_STATUS_CRITICAL -> { // Add code to handle critical thermal increase } PowerManager.THERMAL_STATUS_EMERGENCY -> { // Add code to handle emergency thermal increase } PowerManager.THERMAL_STATUS_SHUTDOWN -> { // Add code to handle immediate shutdown } } } }
Java
PowerManager.OnThermalStatusChangedListener thermalListener = new PowerManager.OnThermalStatusChangedListener () { @Override public void onThermalStatusChanged(int status) { switch (status) { case PowerManager.THERMAL_STATUS_NONE: // No thermal status, so no action necessary break; case PowerManager.THERMAL_STATUS_LIGHT: // Add code to handle light thermal increase break; case PowerManager.THERMAL_STATUS_MODERATE: // Add code to handle moderate thermal increase break; case PowerManager.THERMAL_STATUS_SEVERE: // Add code to handle severe thermal increase break; case PowerManager.THERMAL_STATUS_CRITICAL: // Add code to handle critical thermal increase break; case PowerManager.THERMAL_STATUS_EMERGENCY: // Add code to handle emergency thermal increase break; case PowerManager.THERMAL_STATUS_SHUTDOWN: // Add code to handle immediate shutdown break; } } };
Latencia de la opción tocar para ver
Los juegos que renderizan fotogramas lo más rápido posible crean una situación de vinculación con la GPU, en la que el búfer de fotogramas se sobrecarga. La CPU debe esperar a la GPU, lo que provoca un notorio retraso cuando el jugador realiza una entrada y esta surte efecto en la pantalla.
Para determinar si podrías mejorar el ritmo de velocidad de fotogramas de tu juego, completa los siguientes pasos:
- Genera un informe de Systrace que incluya las categorías
gfx
yinput
. Estas categorías comprenden medidas que resultan particularmente útiles si deseas determinar la latencia de la función tocar para ver. Revisa la sección
SurfaceView
de un informe de Systrace. Un búfer sobrecargado hace que la cantidad de dibujos de búfer pendientes oscile entre 1 y 2, como se muestra en la figura 5.Figura 5: Informe de Systrace que muestra un búfer sobrecargado que suele estar demasiado lleno como para aceptar comandos de dibujo
Para mitigar esta falta de constancia en el ritmo de fotogramas, completa las acciones que se describen en las siguientes secciones:
Integra la API de ritmo de fotogramas de Android a tu juego
La API de Android Frame Pacing te ayuda a realizar intercambios de fotogramas y definir un intervalo de intercambio para que tu juego mantenga una velocidad de fotogramas más constante.
Reduce la resolución de los elementos que no son IU de tu juego
Las pantallas de los dispositivos modernos contienen muchos más píxeles de los que puede procesar un jugador, por lo que está bien reducir el muestreo de manera que una ejecución de 5 o 10 píxeles contenga un color. Debido a la estructura de la mayoría de las caché, se recomienda reducir la resolución junto con una dimensión solamente.
Sin embargo, no debes reducir la resolución de los elementos de IU de tu juego. Es importante conservar el espesor de las líneas de esos elementos para mantener un tamaño del objetivo táctil lo suficientemente grande para todos tus jugadores.
Suavidad de renderización
Cuando SurfaceFlinger se conecta a un búfer de pantalla para mostrar una escena de tu juego, la actividad de CPU aumenta temporalmente. Si estos picos de actividad de CPU no se producen de manera pareja, es posible que veas trabas en tu juego. El diagrama de la figura 6 muestra el motivo:
Si un fotograma comienza a dibujarse demasiado tarde, incluso por unos milisegundos, podría perderse la siguiente ventana de visualización. Entonces, el fotograma debe esperar a que se muestre el próximo Vsync (33 milisegundos cuando se ejecuta un juego a 30 FPS), lo que produce un notable retraso desde la perspectiva del jugador.
Para resolver esta situación, usa la API de Android Frame Pacing, que siempre presenta un nuevo fotograma en un frente de onda de Vsync.
Estado de la memoria
Cuando tu juego se ejecuta durante un período prolongado, es posible que el dispositivo tenga errores de falta de memoria.
Es esa situación, comprueba la actividad de CPU en un informe de Systrace y consulta la frecuencia con la que el sistema realiza llamadas al daemon kswapd
. Si hay muchas llamadas durante la ejecución del juego, te recomendamos que observes la manera en que tu juego administra y limpia la memoria.
Para obtener más información, consulta Cómo administrar de manera eficaz la memoria en juegos.
Estado de subprocesos
Cuando navegas por los elementos típicos de un informe de Systrace, puedes ver la cantidad de tiempo que un subproceso determinado dedica en cada posible estado del subproceso. Para ello, selecciona el subproceso en el informe, como se muestra en la figura 7:
Como se muestra en la figura 7, es posible que veas que los subprocesos de tu juego no se encuentran en estado "en ejecución" o "ejecutable" con la frecuencia que deberían. En la siguiente lista, se muestran varios motivos comunes por los cuales un subproceso determinado podría pasar en forma periódica a un estado inusual:
- Si un subproceso se encuentra inactivo durante mucho tiempo, es posible que esté experimentando una competencia de bloqueo o esperando a la actividad de GPU.
- Si un subproceso está bloqueado constantemente en E/S, quiere decir que estás leyendo demasiados datos de un disco al mismo tiempo o que el juego sufre de hiperpaginación.
Recursos adicionales
Para obtener más información sobre cómo mejorar el rendimiento de tu juego, consulta los siguientes recursos adicionales:
Videos
- Presentación de Systrace for Games realizada en Android Game Developer Summit 2018