搜索 Unity

用Unity Frame Timing Manager检测性能瓶颈

2022年6月16日 类别 Engine & platform | 14 分 阅读
Abstract blue gen-art
Abstract blue gen-art
分享

Is this article helpful for you?

Thank you for your feedback!

要想做出一款能流畅运行在各色设备和平台上的优秀游戏可能并没有那么简单。这也是为什么我们持续完善工具、帮助开发者优化整个创作流程,这其中也包括了最近改进过的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帧测量指标。他们包括:

  • cpuFrameTime指的是总CPU帧时间,是主线程某帧的开始到下一帧开始的时间间隔。
  • cpuMainThreadFrameTime是主线程的工作时间,或者说是从帧开始到所有主线程任务完成的总耗时。
  • cpuRenderThreadFrameTime指的是渲染线程的工作时间,或者说是从渲染线程受到的第一个任务请求到Present()函数被调用的总时间间隔。
  • cpuMainThreadPresentWaitTime是CPU在某帧上等待Present()执行完成的时间。
  • gpuFrameTime是GPU的工作时间,或者说是从GPU收到任务到GPU发出完成信号的总时间间隔。API的相关限制可在下文的“支持平台与限制”一节中找到。

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

HUD面板

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支持的平台,但有以下例外:

  • 在Linux平台运行OpenGL API时,不支持记录GPU时间。
  • 不支持WebGL平台的GPU时间。
  • 在iOS和macOS运行的Metal API时,GPU时间在高负荷下有可能会多于总帧时间。

Frame Timing Manager的应用有几个重要的细节:

  1. Frame Timing Manager生成的报告会有固定四帧的延迟。所有统计结果都是四帧之前的结果(而非当前帧)。Frame Timing Manager支持同步记录某一帧的CPU和GPU时间。但由于平台和硬件的限制,大多数平台并不能立即提供GPU时间。
  2. 因此工具并不保证能统计所有帧的GPU时间。GPU有可能无法按时返回结果,或者根本就不返回结果。这时,GPU Frame Time将被记录为零。
  3. 在不支持生成GPU时间戳的平台上,Unity会计算完整的Frame Complete Time(帧完成时间),而不会测量GPU时间。具体来说,Unity计算的Frame Complete Time是First Submit Timestamp加GPU时间的总和。如果GPU不能提供GPU时间,则Frame Complete Time将被自动设置为Present Timestamp(当前时间戳)。
  4. 如果GPU采用了以图块为基础的延迟渲染架构,类似移动平台,则报告的结果并不会那么精确,因为GPU会延迟执行渲染,且渲染可能会单独进行。这时Frame Timing Manager就只能测量总的处理时间了。

高级主题

高级用户可使用Frame Timing Manager的时间戳用于帧时间轴的可视化或计算其他指标的变化值。

可用的时间戳有:

  • frameStartTimestamp:某帧开始渲染时的CPU时钟时间
  • firstSubmitTimestamp:某帧内渲染任务首次提交给GPU时的CPU时钟时间(视平台和API不同而不同);不同的平台会提交不同的时间。
  • cpuTimePresentCalled:某帧在调用Present()时的CPU时钟时间。Unity会在这时完成渲染对象的提交,并通知GPU这一帧已经可以展示。
  • cpuTimeFrameComplete:GPU完成渲染帧时的CPU时钟时间。在大多数平台上,这个值是First Submit Timestamp(首次提交时间戳)加Frame GPU时间得出来的。

告诉我们你的想法

我们希望这些改进能帮助你准确测量和了解应用的性能。这些更新目前都可在Unity 2022.1中找到。

若你想了解性能分析工具的未来计划,可以在此处查看我们的产品路线图。同时,你也能在论坛上随时与我们取得联系。我们期待着从大家的反馈中学习如何进一步改善Unity的性能特色与工具。

2022年6月16日 类别 Engine & platform | 14 分 阅读

Is this article helpful for you?

Thank you for your feedback!