要想做出一款能流畅运行在各色设备和平台上的优秀游戏可能并没有那么简单。这也是为什么我们持续完善工具、帮助开发者优化整个创作流程,这其中也包括了最近改进过的Frame Timing Manager。Unity 2022.1的更新为Frame Timing Manager功能带来了更强的平台支持,让你能够收集比以往更多的数据。更多详情请在本文中了解。
Frame Timing Manager能做些什么?
Frame Timing Manager是一种能检测每一帧数据活动的功能,比如总的CPU和GPU帧时间。与更为通用的Unity Profiler和Profiler API相比,Frame Timing Manager是一个更为专门的工具,会消耗更少的性能。它只搜集最为重要的帧数据,因此所有信息都经过了精心筛选。
Frame Timing Manager的一个主要作用是更深入地调查性能瓶颈。让你能够找出制约了应用性能的因素:它究竟依赖着CPU上的主线程或渲染线程,还是依赖于GPU?根据分析结果,你就能采取进一步的行动来提高性能。
你可以用动态分辨率功能来解决GPU上遇到的瓶颈。或者,你也能通过提高或降低渲染分辨率,动态地控制GPU的工作负荷。
在开发过程中,你甚至可以在应用的HUD上显示帧时间,直接创建一个实时的简略型迷你分析器。这样,你就可以随时监测帧的活动。
最后,你可以使用Frame Timing Manager来生成发布版的性能报告。你可以让应用将收集到的信息发送到服务器上,包括各平台上的性能统计等,以便更好地进行整体决策。
Frame Timing Manager API可以测量哪些指标?
Frame Timing Manager API的FrameTiming架构可提供一组实用的CPU和GPU帧测量指标。他们包括:
cpuMainThreadPresentWaitTime是“[wait]”字段出现(调用)的次数总和,包括等待Present()完成和达成目标fps所须的调用。GPU的工作时间统计起来会比较困难,因为GPU通常会在“场景渲染”期间开始工作,在上一帧与下一帧的某个固定的时间点上结束。
如何开始
首先,我们需要知道Frame Timing Manager在开发版中是始终启用的。如果你只打算在开发中使用它,就不必采取额外的步骤,可以直接使用工具的C# API及其测量器。
而如果你想在发布版中使用该功能,就必须激活它。这里有很多种方法。一种比较直接的方法是勾选Project Player设置中的选框。这时,你可以用C# API来读取数据。不过,这种方法的效率最低。如果直接在设置中启用该功能,它会始终保持启用状态,无论你需不需要。
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
}
}
}
或者,你也可以使用Profiler Recorder API读取Frame Timing Manager的数值。Profiler Recorder API的优点在于,Frame Timing Manager只有在测量器被添加记录器时才会开始测量,让你能动态地开关功能、控制性能开销。
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
}
}
Frame Timing Manager提供的数据可用于检测性能瓶颈。在最简单的应用中,你可以将CPU主线程、CPU渲染线程、Present Wait和GPU时间进行比较,找出哪一块最有可能限制了帧率。例如:
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;
}
}
Frame Timing Manager可用作一个简单的屏显分析器,方便评估应用的健康状况。下方是一种最基本的应用形式:
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);
}
}
支持平台与限制
Frame Timing Manager支持所有Unity支持的平台,但有以下例外:
Frame Timing Manager的应用有几个重要的细节:
高级主题
高级用户可使用Frame Timing Manager的时间戳用于帧时间轴的可视化或计算其他指标的变化值。
可用的时间戳有:
告诉我们你的想法
我们希望这些改进能帮助你准确测量和了解应用的性能。这些更新目前都可在Unity 2022.1中找到。
若你想了解性能分析工具的未来计划,可以在此处查看我们的产品路线图。同时,你也能在论坛上随时与我们取得联系。我们期待着从大家的反馈中学习如何进一步改善Unity的性能特色与工具。