Detección de cuellos de botella en el rendimiento con Unity Frame Timing Manager

Crear una experiencia destacada que funcione sin problemas en diversos dispositivos y plataformas puede ser todo un reto. Por eso seguimos perfeccionando nuestras herramientas, como nuestro gestor de sincronización de cuadros mejorado, para optimizar en todos los ámbitos. Siga leyendo para descubrir cómo la actualización de Unity 2022.1 proporciona una mayor compatibilidad de la plataforma con esta función, lo que le permitirá recopilar más datos de los que antes era posible.
El gestor de tiempo de fotogramas es una función que proporciona mediciones de tiempo a nivel de fotograma, como los tiempos totales de CPU y GPU por fotograma. En comparación con el Profiler de Unity y la API del Profiler de uso general, el Gestor de tiempo de fotogramas está diseñado para una tarea muy específica y, por tanto, tiene una sobrecarga de rendimiento mucho menor. La cantidad de información recopilada se limita cuidadosamente, ya que sólo destaca las estadísticas más importantes del marco.
Una de las principales razones para aprovechar el gestor de temporización de tramas es investigar los cuellos de botella de rendimiento con mayor detalle. Esto le permite determinar qué es lo que frena el rendimiento de su aplicación: ¿Está ligado al hilo principal o al hilo de renderizado en la CPU, o está ligado a la GPU? Basándose en su análisis, puede tomar nuevas medidas para mejorar el rendimiento.
La función de resolución dinámica permite solucionar los cuellos de botella detectados en la GPU. A continuación, puede aumentar o reducir la resolución de renderizado para controlar dinámicamente la cantidad de trabajo de la GPU.
Durante el desarrollo, puede incluso visualizar la temporización en un HUD de la aplicación, lo que le permite disponer de un miniprofiler de alto nivel en tiempo real integrado directamente en su aplicación. De este modo, siempre estará disponible para su uso.
Por último, puede utilizar el gestor de temporización de tramas para obtener informes sobre el rendimiento del modo de liberación. Basándose en la información recopilada, puede enviar estadísticas a sus servidores sobre el rendimiento de su aplicación en diferentes plataformas para una mejor toma de decisiones global.
La API Frame Timing Manager proporciona un conjunto de mediciones útiles de la CPU y la GPU por fotograma como la estructura FrameTiming. Aquí tiene una lista de ellos:
- cpuFrameTime se refiere al tiempo total de fotogramas de la CPU. Se calcula como el tiempo transcurrido entre el inicio de la trama y la siguiente trama en el hilo principal.
- cpuMainThreadFrameTime es el tiempo de trabajo del hilo principal, o la cantidad total de tiempo entre el inicio del fotograma y que el hilo principal termine su trabajo.
- cpuRenderThreadFrameTime se refiere al tiempo de trabajo del hilo de renderizado, o la cantidad total de tiempo entre la primera solicitud de trabajo enviada al hilo de renderizado y el momento en que se llama a la función Present().
- cpuMainThreadPresentWaitTime es la duración que pasa la CPU esperando a que se complete Present() durante el fotograma.
- gpuFrameTime es el tiempo de trabajo de la GPU, o la cantidad total de tiempo entre el trabajo enviado a la GPU y la señal que indica que la GPU ha terminado el trabajo. Consulte las limitaciones pertinentes en la sección "Plataformas compatibles y limitaciones" más abajo.
Tenga en cuenta que el cpuMainThreadPresentWaitTime es la suma de los bloques "[wait]" mostrados, e incluye las esperas para Present() y el objetivo de FPS. Es más difícil mostrar el tiempo de trabajo de la GPU, ya que comienza en algún punto intermedio del "renderizado de la escena" y termina en el punto de sincronización del siguiente fotograma con el fotograma anterior.
En primer lugar, cabe señalar que el gestor de tiempo de fotogramas siempre está activo en las compilaciones de desarrollo. Si planea utilizarlo sólo en el desarrollo, no necesita completar ningún paso adicional - puede simplemente utilizar la API C# de Frame Timing Manager o sus contadores.
En el caso de las versiones de lanzamiento, deberá activar explícitamente la función antes de poder utilizarla. Hay múltiples formas de hacerlo. Un método sencillo consiste en marcar una casilla de verificación en los ajustes del reproductor de proyectos. En este caso, puede leer los datos utilizando la API de C#. Pero, por desgracia, es el método menos eficaz. Si activa la función en los ajustes, permanecerá activa tanto si la necesita como si no en un momento determinado.
using Unity.Profiling;
using UnityEngine;
public class ExampleScript : MonoBehaviour
{
FrameTiming[] m_FrameTimings = new FrameTiming[10];
void Update()
{
// Instruct FrameTimingManager to collect and cache information
FrameTimingManager.CaptureFrameTimings();
// Read cached information about N last frames (10 in this example)
// The returned value tells how many samples is actually returned
var ret = FrameTimingManager.GetLatestTimings((uint)m_FrameTimings.Length, m_FrameTimings);
if (ret > 0)
{
// Your code logic here
}
}
}
csAlternativamente, puede leer los valores del Frame Timing Manager utilizando la API del Profiler Recorder. La ventaja de la API Profiler Recorder es que las mediciones del Frame Timing Manager sólo se realizan cuando se conecta un grabador al contador, lo que le proporciona un control dinámico sobre la función y su sobrecarga.
using Unity.Profiling;
using UnityEngine;
public class ExampleScript : MonoBehaviour
{
ProfilerRecorder mainThreadTimeRecorder;
void OnEnable()
{
// Create ProfilerRecorder and attach it to a counter
mainThreadTimeRecorder = ProfilerRecorder.StartNew(ProfilerCategory.Internal, "CPU Main Thread Frame Time");
}
void OnDisable()
{
// Recorders must be explicitly disposed after use
mainThreadTimeRecorder.Dispose();
}
void Update()
{
var frameTime = mainThreadTimeRecorder.LastValue;
// Your code logic here
}
}
csLos datos proporcionados por el gestor de temporización de tramas pueden utilizarse para la detección de cuellos de botella. En la variante más sencilla, puede comparar la CPU del hilo principal, la CPU del hilo de renderizado, la espera presente y el tiempo de la GPU para determinar cuál es la causa mayor y más probable de la limitación de la velocidad de fotogramas. Por ejemplo:
using Unity.Profiling;
using UnityEngine;
public class ExampleScript : MonoBehaviour
{
internal enum PerformanceBottleneck
{
Indeterminate, // Cannot be determined
PresentLimited, // Limited by presentation (vsync or framerate cap)
CPU, // Limited by CPU (main and/or render thread)
GPU, // Limited by GPU
Balanced, // Limited by both CPU and GPU, i.e. well balanced
}
FrameTiming[] m_FrameTimings = new FrameTiming[1];
void Update()
{
FrameTimingManager.CaptureFrameTimings();
var ret = FrameTimingManager.GetLatestTimings((uint)m_FrameTimings.Length, m_FrameTimings);
if (ret > 0)
{
var bottleneck = DetermineBottleneck(m_FrameTimings[0]);
// Your code logic here
}
}
static PerformanceBottleneck DetermineBottleneck(FrameTimeSample s)
{
const float kNearFullFrameTimeThresholdPercent = 0.2f;
const float kNonZeroPresentWaitTimeMs = 0.5f;
// If we're on platform which doesn't support GPU time
if (s.GPUFrameTime == 0)
return PerformanceBottleneck.Indeterminate;
float fullFrameTimeWithMargin = (1f - kNearFullFrameTimeThresholdPercent) * s.FullFrameTime;
// GPU time is close to frame time, CPU times are not
if (s.GPUFrameTime > fullFrameTimeWithMargin &&
s.MainThreadCPUFrameTime < fullFrameTimeWithMargin &&
s.RenderThreadCPUFrameTime < fullFrameTimeWithMargin)
return PerformanceBottleneck.GPU;
// One of the CPU times is close to frame time, GPU is not
if (s.GPUFrameTime < fullFrameTimeWithMargin &&
(s.MainThreadCPUFrameTime > fullFrameTimeWithMargin ||
s.RenderThreadCPUFrameTime > fullFrameTimeWithMargin))
return PerformanceBottleneck.CPU;
// Main thread waited due to Vsync or target frame rate
if (s.MainThreadCPUPresentWaitTime > kNonZeroPresentWaitTimeMs)
{
// None of the times are close to frame time
if (s.GPUFrameTime < fullFrameTimeWithMargin &&
s.MainThreadCPUFrameTime < fullFrameTimeWithMargin &&
s.RenderThreadCPUFrameTime < fullFrameTimeWithMargin)
return PerformanceBottleneck.PresentLimited;
}
return PerformanceBottleneck.Balanced;
}
}
csEl gestor de tiempo de fotogramas puede utilizarse como un sencillo perfilador en pantalla, útil para evaluar la salud de la aplicación. Su forma más básica podría ser la siguiente:
using System;
using UnityEngine;
using Unity.Profiling;
public class FrameTimingsHUDDisplay : MonoBehaviour
{
GUIStyle m_Style;
readonly FrameTiming[] m_FrameTimings = new FrameTiming[1];
void Awake()
{
m_Style = new GUIStyle();
m_Style.fontSize = 15;
m_Style.normal.textColor = Color.white;
}
void OnGUI()
{
CaptureTimings();
var reportMsg =
$"\nCPU: {m_FrameTimings[0].cpuFrameTime :00.00}" +
$"\nMain Thread: {m_FrameTimings[0].cpuMainThreadFrameTime:00.00}" +
$"\nRender Thread: {m_FrameTimings[0].cpuRenderThreadFrameTime:00.00}" +
$"\nGPU: {m_FrameTimings[0].gpuFrameTime:00.00}";
var oldColor = GUI.color;
GUI.color = new Color(1, 1, 1, 1);
float w = 300, h = 210;
GUILayout.BeginArea(new Rect(32, 50, w, h), "Frame Stats", GUI.skin.window);
GUILayout.Label(reportMsg, m_Style);
GUILayout.EndArea();
GUI.color = oldColor;
}
private void CaptureTimings()
{
FrameTimingManager.CaptureFrameTimings();
FrameTimingManager.GetLatestTimings(m_FrameTimings.Length, m_FrameTimings);
}
}
csEl Frame Timing Manager es compatible con todas las plataformas que son compatibles con Unity, con las siguientes excepciones:
- En las plataformas Linux, cuando se utiliza la API OpenGL, no se proporciona tiempo de GPU.
- En la plataforma WebGL, no se proporciona tiempo de GPU.
- En iOS y macOS, cuando se utiliza la API Metal, se ha informado de que el tiempo de la GPU podría ser potencialmente superior al tiempo total de fotogramas bajo una carga elevada de la GPU.
Los detalles importantes de la implementación del gestor de temporización de tramas son:
El gestor de tiempo de fotogramas produce resultados con un retraso fijo de cuatro fotogramas. Esto significa que obtendrá resultados para un fotograma que esté cuatro fotogramas por detrás (no para el fotograma actual). El gestor de temporización de fotogramas proporciona mediciones de tiempo sincronizadas para el mismo fotograma tanto para la CPU como para la GPU. Debido a las limitaciones de la plataforma y el hardware, los resultados de sincronización de la GPU no están disponibles de inmediato en la mayoría de las plataformas.
Frame Timing Manager no garantiza que la GPU esté disponible para todos los fotogramas. La GPU podría no devolver los resultados a tiempo, o no devolver ningún resultado en absoluto. En estos casos, el Tiempo de Cuadro de la GPU se reporta como cero.
En plataformas que no permiten el timestamping de la GPU, Unity computa el valor del Tiempo de Completar Fotograma en lugar de medirlo. Más específicamente, Unity calcula el Tiempo de Completar Fotograma como la Marca de Tiempo del Primer Envío + el tiempo de la GPU. Si la GPU no puede proporcionar la hora de la GPU, la Hora de finalización del fotograma se establece automáticamente como igual a la Hora actual.
En las GPU que utilizan una arquitectura de renderizado diferido basada en mosaicos, como las plataformas móviles, los resultados son menos precisos porque la ejecución de la GPU es diferida y la ejecución de las fases de renderizado puede realizarse por separado. El gestor de tiempo de fotogramas sólo puede medir la duración total.
Para los usuarios avanzados, el gestor de temporización de fotogramas proporciona información de temporización que puede utilizarse para la visualización de la línea de tiempo de los fotogramas o para calcular deltas con otros marcadores.
Las marcas de tiempo proporcionadas son:
- frameStartTimestamp: La hora del reloj de la CPU cuando se inicia la trama por primera vez
- firstSubmitTimestamp: La hora del reloj de la CPU en la que se envía el trabajo inicial a la GPU durante el fotograma (depende de la plataforma y de la API); las distintas plataformas lo envían en momentos diferentes.
- cpuTimePresentCalled: El tiempo de reloj de la CPU en el momento en que se llama a Present() para el fotograma. Es el momento en que Unity termina de enviar los objetos para su renderizado e informa a la GPU de que el fotograma puede presentarse al usuario.
- cpuTimeFrameComplete: El tiempo de reloj de la CPU en el momento en que la GPU termina de renderizar el fotograma. En la mayoría de las plataformas, este valor se calcula y es igual a la marca de tiempo del primer envío + el tiempo de la GPU de fotogramas.
Esperamos que estas mejoras le ayuden a medir y comprender la historia única del rendimiento de su aplicación. Estas ventajas están ahora en sus manos con Unity 2022.1.
Si se pregunta qué es lo próximo para nuestras herramientas de creación de perfiles, consulte nuestra hoja de ruta aquí. De lo contrario, no dude en ponerse en contacto con el equipo en nuestro foro. Nos encantaría conocer su opinión y ver cómo podemos seguir mejorando las funciones de rendimiento y las herramientas de Unity.
