Erkennen von Leistungsengpässen mit dem Unity Frame Timing Manager

ANTON KRUGLYAKOV / UNITYSenior Software Engineer
Jun 16, 2022|14 Min.
Erkennen von Leistungsengpässen mit dem Unity Frame Timing Manager
Diese Website wurde aus praktischen Gründen für Sie maschinell übersetzt. Die Richtigkeit und Zuverlässigkeit des übersetzten Inhalts kann von uns nicht gewährleistet werden. Sollten Sie Zweifel an der Richtigkeit des übersetzten Inhalts haben, schauen Sie sich bitte die offizielle englische Version der Website an.

Es kann eine Herausforderung sein, ein herausragendes Erlebnis zu schaffen, das auf einer Vielzahl von Geräten und Plattformen reibungslos funktioniert. Deshalb entwickeln wir unsere Tools, wie z.B. unseren verbesserten Frame Timing Manager, ständig weiter, um eine Optimierung auf breiter Front zu erreichen. Lesen Sie weiter, um zu erfahren, wie das Unity 2022.1-Update die Plattformunterstützung für diese Funktion verbessert und es Ihnen ermöglicht, mehr Daten zu sammeln als bisher möglich war.

Was kann der Frame Timing Manager für Sie tun?

Der Frame Timing Manager ist eine Funktion, die Zeitmessungen auf Frame-Ebene liefert, wie z.B. CPU- und GPU-Gesamtzeiten. Im Vergleich zum allgemeinen Unity Profiler und der Profiler API ist der Frame Timing Manager für eine sehr spezifische Aufgabe konzipiert und hat daher einen viel geringeren Leistungs-Overhead. Die Menge der gesammelten Informationen ist sorgfältig begrenzt, da sie nur die wichtigsten Rahmenstatistiken hervorheben.

Ein Hauptgrund für den Einsatz des Frame Timing Manager ist die genauere Untersuchung von Leistungsengpässen. So können Sie feststellen, was die Leistung Ihrer Anwendung beeinträchtigt: Ist sie an den Haupt- oder Render-Thread auf der CPU gebunden oder an die GPU? Auf der Grundlage Ihrer Analyse können Sie weitere Maßnahmen zur Verbesserung der Leistung ergreifen.

Die dynamische Auflösungsfunktion unterstützt die Behebung von erkannten Engpässen auf der GPU-Seite. Sie können dann die Rendering-Auflösung erhöhen oder verringern, um den Arbeitsaufwand für den Grafikprozessor dynamisch zu steuern.

Während der Entwicklung können Sie das Timing sogar in einem Anwendungs-HUD visualisieren, so dass Sie einen High-Level-Mini-Profiler in Echtzeit direkt in Ihre Anwendung einbauen können. Auf diese Weise haben Sie es immer griffbereit.

Und schließlich können Sie den Frame Timing Manager für Leistungsberichte im Release-Modus verwenden. Auf der Grundlage der gesammelten Informationen können Sie Statistiken über die Leistung Ihrer Anwendung auf verschiedenen Plattformen an Ihre Server senden, um eine bessere Gesamtentscheidung zu treffen.

Welche Messungen bietet die Frame Timing Manager API?

Die Frame Timing Manager API bietet eine Reihe von nützlichen CPU- und GPU-Messungen pro Frame als FrameTiming struct. Hier ist eine Liste von ihnen:

  • cpuFrameTime bezieht sich auf die gesamte CPU-Frame-Zeit. Sie wird berechnet als die Zeit zwischen dem Start des Frames und dem nächsten Frame auf dem Hauptthread.
  • cpuMainThreadFrameTime ist die Arbeitszeit des Hauptthreads oder die Gesamtzeit zwischen dem Start des Frames und dem Abschluss der Arbeit des Hauptthreads.
  • cpuRenderThreadFrameTime bezieht sich auf die Arbeitszeit des Render-Threads oder die Gesamtzeit zwischen der ersten an den Render-Thread übermittelten Arbeitsanforderung und dem Zeitpunkt, an dem die Funktion Present() aufgerufen wird.
  • cpuMainThreadPresentWaitTime ist die Dauer, die die CPU während des Frames auf den Abschluss von Present() wartet.
  • gpuFrameTime ist die Arbeitszeit des Grafikprozessors oder die Gesamtzeit zwischen der an den Grafikprozessor übertragenen Arbeit und dem Signal, das anzeigt, dass der Grafikprozessor die Arbeit beendet hat. Siehe die entsprechenden Einschränkungen im Abschnitt "Unterstützte Plattformen und Einschränkungen" weiter unten.

Beachten Sie, dass die cpuMainThreadPresentWaitTime die Summe der angezeigten "[wait]"-Blöcke ist und die Wartezeiten für Present() und target fps enthält. Es ist schwieriger, die GPU-Arbeitszeit darzustellen, da sie irgendwo in der Mitte des "Scene Rendering" beginnt und am Synchronisationspunkt des nächsten Frames mit dem vorherigen Frame endet.

Wie man anfängt

Zunächst einmal ist zu erwähnen, dass der Frame Timing Manager in den Entwicklungs-Builds immer aktiv ist. Wenn Sie es nur in der Entwicklung verwenden möchten, müssen Sie keine zusätzlichen Schritte ausführen - Sie können einfach die Frame Timing Manager C# API oder die entsprechenden Zähler verwenden.

Bei Release-Builds müssen Sie die Funktion explizit aktivieren, bevor Sie sie nutzen können. Es gibt mehrere Möglichkeiten, dies zu tun. Ein einfacher Ansatz besteht darin, ein Kontrollkästchen in den Project Player-Einstellungen zu aktivieren. In diesem Fall können Sie Daten über die C# API lesen. Leider ist dies jedoch die am wenigsten effiziente Methode. Wenn Sie die Funktion in den Einstellungen aktivieren, bleibt sie aktiv, unabhängig davon, ob Sie sie zu einem bestimmten Zeitpunkt benötigen oder nicht.


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

Alternativ können Sie die Werte des Frame Timing Managers auch über die Profiler Recorder API lesen. Der Vorteil der Profiler Recorder API besteht darin, dass die Messungen des Frame Timing Managers nur dann vorgenommen werden, wenn Sie einen Recorder an den Zähler anschließen, so dass Sie eine dynamische Kontrolle über die Funktion und ihren Overhead haben.

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
Erkennung von Engpässen

Die vom Frame Timing Manager bereitgestellten Daten können zur Erkennung von Engpässen verwendet werden. In der einfachsten Variante können Sie die Haupt-Thread-CPU, Render-Thread-CPU, Present Wait und GPU-Zeit vergleichen, um festzustellen, welches die größte und wahrscheinlichste Ursache für die Einschränkung der Bildrate ist. Zum Beispiel:

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

Der Frame Timing Manager kann als einfacher Profiler auf dem Bildschirm verwendet werden, mit dem Sie den Zustand Ihrer Anwendung beurteilen können. Die einfachste Form könnte folgendermaßen aussehen:

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
Unterstützte Plattformen und Einschränkungen

Der Frame Timing Manager unterstützt alle Plattformen, die von Unity unterstützt werden, mit den folgenden Ausnahmen:

  • Auf Linux-Plattformen, wenn die OpenGL API verwendet wird, wird keine GPU-Zeit angegeben.
  • Auf der WebGL-Plattform wird keine GPU-Zeit angegeben.
  • Unter iOS und macOS, wenn die Metal API verwendet wird, wurde berichtet, dass die GPU-Zeit bei starker GPU-Belastung potenziell höher sein kann als die gesamte Frame-Zeit.

Die wichtigsten Besonderheiten der Implementierung des Frame Timing Managers sind:

Der Frame Timing Manager erzeugt Ergebnisse mit einer festen Verzögerung von vier Frames. Das bedeutet, dass Sie Ergebnisse für ein Bild erhalten, das vier Bilder zurückliegt (nicht für das aktuelle Bild). Der Frame Timing Manager liefert Zeitmessungen, die für denselben Frame sowohl für die CPU als auch für die GPU synchronisiert sind. Aufgrund von Plattform- und Hardwarebeschränkungen sind die Ergebnisse des GPU-Timings auf den meisten Plattformen nicht sofort verfügbar.

Der Frame Timing Manager garantiert nicht, dass die GPU für alle Frames verfügbar ist. Es kann sein, dass der Grafikprozessor die Ergebnisse nicht rechtzeitig oder überhaupt nicht zurückgibt. In diesen Fällen wird die GPU Frame Time mit Null angegeben.

Auf Plattformen, die kein GPU-Timestamping erlauben, berechnet Unity den Wert für die Frame Complete Time, anstatt ihn zu messen. Genauer gesagt, berechnet Unity die Frame Complete Time als First Submit Timestamp + GPU-Zeit. Wenn die GPU keine GPU-Zeit liefert, wird die Frame Complete Time automatisch mit dem Present Timestamp gleichgesetzt.

Auf GPUs, die eine kachelbasierte Architektur mit verzögertem Rendering verwenden, wie z.B. mobile Plattformen, sind die Ergebnisse weniger präzise, da die GPU-Ausführung verzögert erfolgt und die Ausführung der Rendering-Phasen möglicherweise separat erfolgt. Der Frame Timing Manager kann nur die Gesamtdauer messen.

Fortgeschrittene Themen

Für fortgeschrittene Benutzer bietet der Frame Timing Manager Zeitstempelinformationen, die für die Visualisierung der Timeline von Frames oder die Berechnung von Deltas mit anderen Markern verwendet werden können.

Die angegebenen Zeitstempel sind:

  • frameStartTimestamp: Die CPU-Taktzeit beim ersten Start des Frames
  • firstSubmitTimestamp: Die CPU-Uhrzeit, zu der die anfängliche Arbeit während des Frames an die GPU übermittelt wird (plattform- und API-abhängig); verschiedene Plattformen übermitteln zu unterschiedlichen Zeiten.
  • cpuTimePresentCalled: Die CPU-Uhrzeit zu dem Zeitpunkt, an dem Present() für den Frame aufgerufen wird. Dies ist der Zeitpunkt, an dem Unity die Übermittlung von Objekten für das Rendering abschließt und der GPU mitteilt, dass das Bild dem Benutzer präsentiert werden kann.
  • cpuTimeFrameComplete: Die CPU-Taktzeit zu dem Zeitpunkt, an dem die GPU das Rendern des Bildes beendet. Auf den meisten Plattformen wird dieser Wert berechnet und entspricht dem Zeitstempel der ersten Übermittlung + Frame GPU-Zeit.

Lassen Sie uns wissen, was Sie denken

Wir hoffen, dass Ihnen diese Verbesserungen dabei helfen werden, die einzigartige Leistung Ihrer Anwendung zu messen und zu verstehen. Diese Vorteile liegen jetzt mit Unity 2022.1 in Ihren Händen.

Wenn Sie sich fragen, wie es mit unseren Profiling-Tools weitergeht, sehen Sie sich hier unsere Roadmap an. Ansonsten wenden Sie sich bitte an das Team in unserem Forum. Wir würden uns freuen, Ihre Meinung zu hören und zu sehen, wie wir die Leistungsmerkmale und Werkzeuge von Unity weiter verbessern können.