Unity 검색

Unity Frame Timing Manager로 성능 병목 현상 감지

2022년 6월 16일 엔진 & 플랫폼 | 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는 총 프레임 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는 유용한 프레임당 CPU 및 GPU 측정치를 FrameTiming 구조체로 제공합니다. 이러한 구조체의 목록은 아래와 같습니다.

  • cpuFrameTime: 총 CPU 프레임 시간을 지칭하며, 메인 스레드에서 프레임 시작과 다음 프레임 간의 시간으로 계산됩니다.
  • cpuMainThreadFrameTime: 메인 스레드의 작업 시간, 즉 프레임 시작부터 메인 스레드가 잡을 완료하기까지의 총 시간입니다.
  • cpuRenderThreadFrameTime: 렌더 스레드의 작업 시간, 즉 렌더 스레드에 제출된 첫 번째 작업 요청과 Present() 함수가 호출된 시점 간의 총 시간입니다.
  • cpuMainThreadPresentWaitTime: 프레임이 진행되는 동안 Present()가 완료되기까지 CPU가 대기한 시간입니다.
  • gpuFrameTime: GPU의 작업 시간, 즉 GPU에 제출된 작업과 GPU가 잡을 완료했음을 나타내는 시그널 간의 총 시간입니다. 아래 '지원되는 플랫폼 및 제한 사항' 섹션에서 관련 제한 사항을 참조하세요.

cpuMainThreadPresentWaitTime은 표시된 '[wait]' 블록의 합이며, Present() 및 타겟 FPS 대기를 포함합니다. GPU 작업 시간의 경우 'Scene rendering' 과정의 중간 어디쯤에서 시작하여 다음 프레임이 이전 프레임과 동기화되는 지점에서 끝나기 때문에 표시하기가 어렵습니다.

시작하는 방법

첫 번째로, 개발 빌드에서 Frame Timing Manager는 항상 활성화 상태라는 점을 알아두는 것이 좋습니다. 개발 과정에서만 사용하려는 경우에는 추가 단계를 완료하지 않아도 되고, 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 대기, 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 로드가 높을 때 GPU 시간이 총 프레임 시간보다 높을 수 있다고 보고되었습니다.

Frame Timing Manager의 구현 관련 중요 세부 사항은 다음과 같습니다.

  1. Frame Timing Manager는 네 프레임이 고정적으로 지연되는 결과를 도출합니다. 즉, 현재 프레임이 아닌 네 프레임 뒤에 있는 프레임에 대해 결과를 얻습니다. Frame Timing Manager는 CPU와 GPU 양쪽의 동일한 프레임에 대해 동기화된 시간 측정치를 제공합니다. 플랫폼 및 하드웨어 제한 사항으로 인해 대부분의 플랫폼에서 GPU 타이밍 결과를 바로 얻을 수는 없습니다.
  2. Frame Timing Manager는 모든 프레임에 GPU를 사용할 수 있다고 보장하지 않습니다. GPU가 결과를 제때 반환하지 못하거나 어떤 결과도 반환하지 못할 수 있습니다. 이러한 경우 GPU 프레임 시간은 0으로 보고됩니다.
  3. GPU 타임스탬핑이 허용되지 않는 플랫폼에서 Unity는 프레임 완료 시간 값을 측정하는 대신 계산합니다. 구체적으로 설명하자면, 첫 제출 타임스탬프에 GPU 시간을 더하여 프레임 완료 시간을 계산합니다. GPU가 GPU 시간을 제공하지 못하는 경우에는 프레임 완료 시간이 현재 타임스탬프와 동일하도록 자동 설정됩니다.
  4. 모바일 플랫폼과 같이 타일 기반의 디퍼드 렌더링 아키텍처를 사용하는 GPU에서는 GPU 실행이 연기되어 여러 렌더링 단계가 별도로 실행될 수 있기 때문에 결과의 정확도가 떨어집니다. Frame Timing Manager는 전체 기간만 측정할 수 있습니다.

심화 주제

고급 수준의 사용자라면 Frame Timing Manager를 통해 프레임 타임라인 시각화 또는 다른 마커를 활용한 델타 계산에 사용할 수 있는 타임스탬프 정보를 제공받을 수 있습니다.

다음과 같은 타임스탬프가 제공됩니다.

  • frameStartTimestamp: 프레임이 처음으로 시작될 때의 CPU 클럭 시간입니다.
  • firstSubmitTimestamp: 프레임(플랫폼 및 API 종속)이 진행되는 과정에서 최초 작업이 GPU에 제출될 때의 CPU 클럭 시간으로, 여러 플랫폼에서 서로 다른 시간에 제출합니다.
  • cpuTimePresentCalled: 프레임에 대해 Present()가 호출되는 시점의 CPU 클럭 시간입니다. Unity에서 렌더링용 오브젝트 제출을 완료하고 사용자에게 프레임을 제시할 수 있음을 GPU에 알리는 시점입니다.
  • cpuTimeFrameComplete: GPU가 프레임 렌더링을 완료하는 시점의 CPU 클럭 시간입니다. 대부분의 플랫폼에서 이 값은 계산되며, 첫 제출 타임스탬프와 프레임 GPU 시간의 합과 동일합니다.

의견을 들려 주세요

이러한 개선 사항을 통해 애플리케이션의 고유한 성능을 측정하고 관련 내용을 파악하실 수 있기를 바랍니다. 이제 Unity 2022.1을 통해 이러한 장점을 활용하실 수 있습니다.

Unity 프로파일링 툴의 다음 목표가 궁금하시다면 여기에서 유니티의 로드맵을 확인하세요. 또는 유니티 포럼에서 유니티 팀에 문의해 주세요. 유니티는 여러분의 의견에 귀를 기울이고 Unity의 성능 기능과 툴을 더욱 개선할 방안을 모색합니다.

2022년 6월 16일 엔진 & 플랫폼 | 14 분 소요

Is this article helpful for you?

Thank you for your feedback!