Unity を検索

Unity フレームタイミングマネージャーによるパフォーマンスボトルネックの検出

2022年6月16日 カテゴリ: テクノロジー | 14 分 で読めます
Abstract blue gen-art
Abstract blue gen-art
取り上げているトピック
シェア

素晴らしい体験を実現しつつ、それをさまざまなデバイスやプラットフォームでスムーズに動作させるのは難しいことです。私たちが改良されたフレームタイミングマネージャーなど、全体的な最適化のためのツールに磨きをかけ続けているのは、これが理由です。この記事の続きでは、Unity 2022.1 でのアップデートにより、この機能のプラットフォームサポートが強化され、従来よりも多くのデータを収集できるようになったことをご紹介します。

フレームタイミングマネージャーで何ができるのか

フレームタイミングマネージャーは、フレーム全体の CPU 時間や GPU 時間の合計など、フレームレベルの時間計測を行う機能です。汎用の Unity プロファイラーや Profiler API と比較して、フレームタイミングマネージャーは非常に特殊なタスクのために設計されているため、パフォーマンスのオーバーヘッドが非常に低くなっています。最も重要なフレームの統計情報のみを強調するため、収集する情報量は慎重に制限されています。

フレームタイミングマネージャーを活用する主な理由の 1 つは、パフォーマンスのボトルネックをより詳細に調査することです。これによって、何がアプリケーションの性能を抑制しているのかを判断することができます。CPU のメインスレッドバウンドまたはレンダースレッドバウンドなのか、それとも GPU バウンドなのかといったことがわかります。分析結果に基づき、パフォーマンスを向上させるためのさらなるアクションを起こすことができます。

動的解像度機能は、検出されたボトルネックを GPU 側で修正することをサポートします。そして、レンダリング解像度を上げたり下げたりして、GPU の仕事量をダイナミックにコントロールすることができます。

開発中に、アプリケーション HUD でタイミングを視覚化することもでき、リアルタイムで高レベルなミニプロファイラーをアプリケーションに直接組み込むことができます。こうすることで、いつでもすぐに使えるようになるのです。

最後に、フレームタイミングマネージャーを使用して、リリースモードでのパフォーマンスレポートを作成することができます。収集した情報をもとに、さまざまなプラットフォームでのアプリケーションのパフォーマンスに関する統計情報をサーバーに送信し、全体的な意思決定を改善することができます。

Frame Timing Manager API は、どのような測定を可能にするのか

Frame Timing Manager API は、フレームごとの有用な CPU と GPU の測定値のセットを FrameTiming 構造体として提供します。その一覧をご紹介します。

  • cpuFrameTime は、CPU の総フレーム時間を指します。メインスレッドでのフレーム開始から次のフレームまでの時間として計算されます。
  • cpuMainThreadFrameTime は、メインスレッドの作業時間、つまりフレームの開始からメインスレッドが仕事を終えるまでの時間の合計です。
  • cpuRenderThreadFrameTime は、レンダースレッドの作業時間、つまりレンダースレッドに送信された最初の作業要求から Present() 関数が呼び出されるまでの時間の合計を指します。
  • cpuMainThreadPresentWaitTime は、フレーム中で CPU が Present() の完了を待っている時間です。
  • gpuFrameTime は、GPU の作業時間、つまり GPU に作業が送信された時刻から GPU が作業を終了したことを示す信号が出る時刻までの時間の合計です。後述の「対応プラットフォームと制限事項」の関連する制限事項を参照してください。

cpuMainThreadPresentWaitTime は、表示されている [wait] ブロックの合計です。Present() とターゲット FPS に調整するための待ち時間を含んでいることに注意してください。「Scene rendering」の途中から始まって、次のフレームの前のフレームとの同期点で終わるので、GPU の作業時間を表示するのは難しいです。

始めよう

まず注目すべきは、フレームタイミングマネージャーが開発ビルドで常にアクティブであることです。開発時のみ使用する予定であれば、追加の手順は必要ありません。Frame Timing Manager C# API またはそのカウンターを使用するだけです。

リリースビルドの場合、その機能を使用する前に明示的にアクティベートする必要があります。その方法は複数あります。簡単な方法としては、プロジェクトのプレイヤー設定でチェックボックスをオンにすることです。この場合、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 を使用して、フレームタイミングマネージャーの値を読み取ることもできます。Profiler Recorder API の利点は、フレームタイミングマネージャーの測定は、カウンターにレコーダーを取り付けたときのみ行われ、機能とそのオーバーヘッドをダイナミックに制御できることです。

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

ボトルネック検出

フレームタイミングマネージャーから提供されるデータは、ボトルネックの検出に使用することができます。最も簡単な方法は、メインスレッド 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

フレームタイミングマネージャーは、シンプルなオンスクリーンプロファイラーとして使用することができ、アプリケーションの健全性を評価するために便利に使うことができます。その最も基本的な形は、以下のようになるでしょう。

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

対応プラットフォームと制限事項

フレームタイミングマネージャーは、以下の例外を除き、Unity がサポートするすべてのプラットフォームをサポートしています。

  • OpenGL API を使用する Linux プラットフォームでは、GPU 時間の情報は提供されません。
  • WebGL プラットフォームでは、GPU 時間の情報は提供されません。
  • iOS と macOS で Metal API を使用した場合、GPU 負荷が高い状態では、GPU 時間が総フレーム時間より長くなることがあると報告されています。

フレームタイミングマネージャーの重要な実装仕様は以下の通りです。

  1. フレームタイミングマネージャーは、4 フレーム分の固定された遅延を使って結果を出します。つまり、(現在のフレームではなく)4 フレーム分遅れたフレームの結果を得ることになります。フレームタイミングマネージャーは、CPU と GPU の両方で同じフレームに同期した時間計測の結果を提供します。プラットフォームやハードウェアの制限により、ほとんどのプラットフォームで GPU タイミングの結果をすぐに利用することはできません。
  2. フレームタイミングマネージャーは、GPU がすべてのフレームで使用可能であることを保証しません。GPU は時間通りに結果を返せなかったり、まったく結果を返さなかったりすることがあります。このような場合、GPU Frame Time は 0 として報告されます。
  3. GPU のタイムスタンプが使えないプラットフォームでは、Unity は Frame Complete Time の値を測定するのではなく、計算します。具体的には、Frame Complete Time は、First Submit Timestamp に GPU 時間を足した値として計算されます。GPU が GPU 時間を提供できない場合、Frame Complete Time は自動的に Present Timestamp と等しくなるように設定されます。
  4. モバイルプラットフォームのようにタイルベースのディファードレンダリングアーキテクチャを使用する GPU では、GPU による命令実行が遅延され、レンダリングフェーズの実行が別々に行われる場合があるため、結果の精度は低くなります。フレームタイミングマネージャーは、全体の時間間隔しか測定できません。

高度なトピック

知識のあるユーザーであれば、フレームタイミングマネージャーを使ってフレームタイムラインの可視化や他のマーカーとの差分計算に使用できるタイムスタンプ情報の取得を行うことができます。

提供されるタイムスタンプは以下の通りです。

  • frameStartTimestamp:フレームが最初に開始された CPU クロック時間
  • firstSubmitTimestamp:フレーム中に最初の作業が GPU に送信されたときの CPU クロック時間(プラットフォームと API に依存)。プラットフォームによって送信する時間が異なります。
  • cpuTimePresentCalledフレームに対して Present() が呼び出された時点の CPU クロック時間。Unity がレンダリングするオブジェクトの送信を終え、ユーザーにフレームを表示できることを GPU に通知する時間です。
  • cpuTimeFrameComplete:GPU がフレームのレンダリングを終了した時点の CPU クロック時間。多くのプラットフォームでは、この値は計算され、First Submit Timestamp にフレームの GPU 時間を足した値に等しくなります。

ご意見をお聞かせください

これらの改善が、皆さんのアプリケーションごとに固有のパフォーマンスストーリーを測定し、理解するのに役立つことを期待しています。こうした優れた機能を、すでに Unity 2022.1 でご利用することができます。

プロファイリングツールの今後の展開が気になる方は、こちらのロードマップ をご覧ください。何かありましたら、私たちのフォーラムページを通して、お気軽にチームにご連絡ください。皆さんのご意見を伺いながら、Unity のパフォーマンス関連の機能やツールをさらに向上させる方法を考えていきたいと思います。

2022年6月16日 カテゴリ: テクノロジー | 14 分 で読めます
取り上げているトピック