Détecter les goulots d'étranglement avec Unity Frame Timing Manager

ANTON KRUGLYAKOV / UNITYSenior Software Engineer
Jun 16, 2022|14 Min
Détecter les goulots d'étranglement avec Unity Frame Timing Manager
Cette page a été traduite automatiquement pour faciliter votre expérience. Nous ne pouvons pas garantir l'exactitude ou la fiabilité du contenu traduit. Si vous avez des doutes quant à la qualité de cette traduction, reportez-vous à la version anglaise de la page web.

Il n'est pas toujours facile de créer une expérience exceptionnelle qui fonctionne sans problème sur une variété d'appareils et de plateformes. C'est pourquoi nous continuons à perfectionner nos outils, tels que notre gestionnaire de synchronisation des images (Frame Timing Manager), afin d'optimiser l'ensemble du système. Lisez la suite pour découvrir comment la mise à jour Unity 2022.1 offre une meilleure prise en charge de cette fonctionnalité par la plateforme, vous permettant ainsi de collecter plus de données qu'auparavant.

Que peut faire le Frame Timing Manager pour vous ?

Le Frame Timing Manager est une fonction qui fournit des mesures de temps au niveau de la trame, comme les temps totaux de l'unité centrale et de l'unité de traitement graphique. Comparé à l'outil polyvalent Unity Profiler et à l'API Profiler, le Frame Timing Manager est conçu pour une tâche très spécifique et présente donc un surcoût de performance beaucoup plus faible. La quantité d'informations collectées est soigneusement limitée car elle ne met en évidence que les statistiques les plus importantes du cadre.

L'une des principales raisons d'utiliser le Frame Timing Manager est d'étudier plus en détail les goulets d'étranglement en matière de performances. Cela vous permet de déterminer ce qui freine la performance de votre application : Est-il lié au thread principal ou au thread de rendu sur le processeur, ou est-il lié au GPU ? Sur la base de votre analyse, vous pouvez prendre des mesures supplémentaires pour améliorer les performances.

La fonction de résolution dynamique permet de corriger les goulets d'étranglement détectés du côté du GPU. Vous pouvez ensuite augmenter ou réduire la résolution de rendu pour contrôler dynamiquement la quantité de travail sur le GPU.

Pendant le développement, vous pouvez même visualiser la synchronisation dans un HUD de l'application, ce qui vous permet d'avoir un mini-profiler de haut niveau en temps réel intégré directement dans votre application. De cette façon, il est toujours prêt à l'emploi.

Enfin, vous pouvez utiliser le Frame Timing Manager pour établir des rapports sur les performances en mode release. Sur la base des informations collectées, vous pouvez envoyer à vos serveurs des statistiques concernant les performances de votre application sur différentes plateformes pour une meilleure prise de décision globale.

Quelles sont les mesures fournies par l'API du gestionnaire de la synchronisation des trames ?

L'API Frame Timing Manager fournit un ensemble de mesures utiles du CPU et du GPU par image sous la forme de la structure FrameTiming. En voici la liste :

  • cpuFrameTime fait référence à la durée totale des images de l'unité centrale. Il est calculé comme le temps entre le début de la trame et la trame suivante sur le fil principal.
  • cpuMainThreadFrameTime est le temps de travail du thread principal, ou le temps total entre le début de la trame et la fin du travail du thread principal.
  • cpuRenderThreadFrameTime fait référence au temps de travail du fil de rendu, ou au temps total écoulé entre la première demande de travail soumise au fil de rendu et le moment où la fonction Present() est appelée.
  • cpuMainThreadPresentWaitTime est la durée pendant laquelle le CPU attend la fin de Present() pendant la trame.
  • gpuFrameTime est le temps de travail du GPU, ou le temps total entre le travail soumis au GPU et le signal indiquant que le GPU a terminé le travail. Voir les limitations correspondantes dans la section "Plateformes prises en charge et limitations" ci-dessous.

Notez que le temps d'attente du cpuMainThreadPresentWaitTime est la somme des blocs "[wait]" affichés, et inclut les temps d'attente pour Present() et target FPS. Il est plus difficile de montrer le temps de travail du GPU, car il commence quelque part au milieu du "rendu de la scène" et se termine au point de synchronisation de l'image suivante avec l'image précédente.

Comment démarrer

Tout d'abord, il convient de noter que le gestionnaire de synchronisation des images est toujours actif dans les versions de développement. Si vous prévoyez de l'utiliser uniquement dans le cadre du développement, vous n'avez pas besoin de suivre d'autres étapes. Il vous suffit d'utiliser l'API C# Frame Timing Manager ou ses compteurs.

Pour les versions préliminaires, vous devez activer explicitement la fonctionnalité avant de pouvoir l'utiliser. Il y a plusieurs façons de procéder. Une approche simple consiste à cocher une case dans les paramètres du lecteur de projet. Dans ce cas, vous pouvez lire les données à l'aide de l'API C#. Malheureusement, il s'agit de la méthode la moins efficace. Si vous activez la fonction dans les paramètres, elle restera active, que vous en ayez besoin ou non à un moment donné.


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
}
}
}
cs

Vous pouvez également lire les valeurs du Frame Timing Manager à l'aide de l'API Profiler Recorder. L'avantage de l'API Profiler Recorder est que les mesures du Frame Timing Manager ne sont prises que lorsque vous attachez un enregistreur au compteur, ce qui vous donne un contrôle dynamique sur la fonction et ses frais généraux.

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
}
}
cs
Détection des goulets d'étranglement

Les données fournies par le Frame Timing Manager peuvent être utilisées pour la détection des goulets d'étranglement. Dans la variante la plus simple, vous pouvez comparer les temps du processeur du fil principal, du processeur du fil de rendu, de l'attente actuelle et du GPU pour déterminer lequel est la cause la plus importante et la plus probable de la limitation du nombre d'images par seconde. Par exemple :

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;
}
}
cs
HUD

Le Frame Timing Manager peut être utilisé comme un simple profileur à l'écran, utile pour évaluer l'état d'une application. Sa forme la plus élémentaire pourrait ressembler à ceci :

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);
}
}
cs
Plateformes supportées et limitations

Le gestionnaire de synchronisation de trames prend en charge toutes les plateformes prises en charge par Unity, à l'exception des suivantes :

  • Sur les plates-formes Linux, lorsque l'API OpenGL est utilisée, aucun temps GPU n'est fourni.
  • Sur la plateforme WebGL, aucun temps de GPU n'est fourni.
  • Sur iOS et macOS, lorsque l'API Metal est utilisée, il a été rapporté que le temps GPU pouvait potentiellement être plus élevé que le temps de trame total en cas de forte charge GPU.

Les principales spécificités de mise en œuvre du gestionnaire de temporisation de trame sont les suivantes :

Frame Timing Manager produit des résultats avec un délai fixe de quatre images. Cela signifie que vous obtenez des résultats pour une image située quatre images plus loin (et non pour l'image actuelle). Le Frame Timing Manager fournit des mesures de temps synchronisées pour la même trame à la fois pour le CPU et le GPU. En raison des limitations de la plate-forme et du matériel, les résultats de la synchronisation du GPU ne sont pas immédiatement disponibles sur la plupart des plates-formes.

Frame Timing Manager ne garantit pas que le GPU sera disponible pour toutes les images. Le GPU peut ne pas renvoyer les résultats à temps, ou ne pas renvoyer de résultats du tout. Dans ce cas, la durée de la trame du GPU est indiquée comme nulle.

Sur les plateformes qui ne permettent pas l'horodatage par le GPU, Unity calcule la valeur du Frame Complete Time plutôt que de la mesurer. Plus précisément, Unity calcule le temps d'achèvement de la trame comme étant l'horodatage de la première soumission + le temps du GPU. Si le GPU ne fournit pas de temps GPU, le temps de fin de trame est automatiquement égal à l'horodatage actuel.

Sur les GPU qui utilisent une architecture de rendu différé basée sur les tuiles, comme les plateformes mobiles, les résultats sont moins précis parce que l'exécution du GPU est différée et que l'exécution des phases de rendu peut se faire séparément. Le Frame Timing Manager ne peut mesurer que la durée totale.

Thèmes avancés

Pour les utilisateurs avancés, le gestionnaire de timeline de trame fournit des informations d'horodatage qui peuvent être utilisées pour la visualisation de la timeline de trame ou le calcul de deltas avec d'autres marqueurs.

Les horodatages fournis sont les suivants :

  • frameStartTimestamp: Heure de l'horloge de l'unité centrale lorsque la trame démarre pour la première fois
  • firstSubmitTimestamp: Heure de l'horloge du processeur à laquelle le travail initial est soumis au GPU pendant la trame (en fonction de la plate-forme et de l'API) ; différentes plates-formes soumettent à des moments différents.
  • cpuTimePresentCalled: L'heure de l'horloge du processeur au moment où Present() est appelé pour la trame. C'est le moment où Unity finit de soumettre les objets au rendu et informe le GPU que l'image peut être présentée à l'utilisateur.
  • cpuTimeFrameComplete: Temps d'horloge du CPU au moment où le GPU termine le rendu de l'image. Sur la plupart des plateformes, cette valeur est calculée et correspond à l'horodatage de la première soumission + l'heure du GPU de la trame.

Dites-nous ce que vous en pensez

Nous espérons que ces améliorations vous aideront à mesurer et à comprendre les performances uniques de votre application. Ces avantages sont désormais entre vos mains avec Unity 2022.1.

Si vous vous demandez quelles seront les prochaines évolutions de nos outils de profilage, consultez notre feuille de route ici. Sinon, n'hésitez pas à contacter l'équipe sur notre forum. Nous aimerions connaître votre avis et voir comment nous pouvons encore améliorer les fonctionnalités et les outils de performance d'Unity.