トレーシングのデベロッパー ガイド

Unreal Insights を使って独自のトレース開発を行うためのチュートリアルです。

Choose your operating system:

Windows

macOS

Linux

トレース (Trace) は、実行中のプロセスからインストルメンテーション イベントをトレースするための構造化されたログ フレームワークです。このフレームワークは、高頻度でトレースされるイベントのストリームを生成するように設計されおり、自己記述型で、簡単に消費して、簡単に共有できます。TraceLog および TraceAnalysis は、このフレームワークの主要な構成モジュールです。

Unreal Insights を構成する主要なコンポーネントは トレース イベント、アプリケーションからのトレースを記録して保存する Unreal Trace Server、そしてデータを解析して視覚化する Timing Insights です。

insights-major-components-diagram

格納されたトレース セッションは自己記述型で、下位互換性があります。これらは .utrace ファイルに格納され、生成された一方のデータは、このトレース ファイルの隣にある .ucache ファイルに格納されます。

トレース データは トランスポート (Transport) と呼ばれるパケットで送信されます。各パケットは、イベントがどのスレッドに由来するかを示す 内部識別子 とサイズで始まります。パケットは、容量が小さすぎて LZ4 形式で圧縮してもメリットがない場合を除き、 LZ4 形式で圧縮されます。

ビルトイン イベント タイプを使用する

Unreal Engine には、さまざまな事前定義されたイベント タイプが用意されています。これらのタイプは、パフォーマンス タイマーやメモリ割り当てなど、一般的なプロファイリング情報に対応します。イベントは、「Core/ProfilingDebugging」フォルダ内のマクロやインターフェースによって公開されます。

独自のカスタム イベント タイプを実装する前に、これらの API を使用することを強くお勧めします。ビルトイン イベント タイプを使用すると、ビルトイン アナライザーおよびビジュアライゼーションによるメリットを活用できます。

タイマー

最も一般的なプロファイリング タスクは、アプリケーションのパフォーマンス測定です。「Core/ProfilingDebugging/CpuProfilingTrace.h」ファイルには、タイマー イベントを発行する機能が含まれています。マクロ ファミリ TRACE_CPUPROFILER_EVENT_SCOPE_ を使用することをお勧めします。このマクロ ファミリは、スコープ内のアプリケーションが使う時間の長さを簡単に測定する方法を提供します。

    {
        TRACE_CPUPROFILER_EVENT_SCOPE_STR("Fancy work");
        // do fancy work…
    }

このコード サンプルでは「Fancy work」というタイマーが生成され、 Timing Insights タイムラインに表示されます。この例では、静的文字列を使用しています。動的文字列はサポートされていますが、静的文字列と比較すると、追加のパフォーマンスおよびメモリのオーバーヘッドが発生します。

多くの組み込みマクロには TRACE_CPUPROFILER_EVENT_SCOPE が含まれています。たとえば、SCOPE_CYCLE_COUNTERQUICK_SCOPE_CYCLE_COUNTERSCOPED_NAMED_EVENT (-statnamedevents が設定されている場合) です。

カウンター

Core/ProfilingDebugging/CountersTrace.h」ファイルには、名前付きの値の宣言およびトレーシングのための汎用インターフェースが含まれています。インターフェースを使用し、時間の経過に沿ってこれらの値を追跡できます。インターフェースでは、整数、浮動小数点数、および一般的な操作 (設定、インクリメント、デクリメント) を含むメモリ値がサポートされています。

次に例を挙げます。

    TRACE_DECLARE_INT_COUNTER(AlienBytes, TEXT("Alien Bytes Written"));
    TRACE_DECLARE_INT_COUNTER(AlienHits, TEXT("Alien Hit Count"));

    void SomeFunc(uint32 WriteSize)
    {
        TRACE_COUNTER_INCREMENT(AlienHits);
        TRACE_COUNTER_ADD(AlienBytes, WriteSize);
    }

このコード サンプルでは 2 つのカウンター (AlienHits および AlienBytes) が生成され、 Timing Insights[Counters (カウンター)] タブに表示されます。

メモリ

メモリ トレーシング は、通常行われる割り当てに対応する GMalloc のラッパーとして実装されています。さらに、該当するプラットフォームでは仮想アロケータ機能も実装されています。しかし、独自のカスタム アロケータを実装する場合は、「Core/ProfilingDebugging/MemoryTrace.h」ファイル内にある関数を使用してそれらをインストルメント化できます。

メモリ トレーシングでは、 LLM[testing-and-optimizing-your-content\unreal-insights\memory-insights] タグ付けシステムを利用し、タグを追跡するための割り当てのトレーシングに役立つ LLM_SCOPE イベントを使用してコードが実装されます。LLM とメモリ トレーシングの両方でマクロが活用できる場合があるため、これらのマクロをそのまま利用することをお勧めします。ただし、特定のケースでは、メモリ トレーシングのみを目的としてカスタム インストルメント化を追加するマクロが「Core/ProfilingDebugging/TagTrace.h」ファイルに含まれている場合があります。

その他のユーティリティ

Core/ProfilingDebugging/MiscTrace.h」ファイルには、プロファイリング時のコンテキストに役立つ一連のユーティリティ マクロがあります。フレーム マーカーブックマーク などです。ブックマークは、アプリケーション内の重要な変更を一目で特定できるため役立ちます。TRACE_BOOKMARK マクロを使用すると、独自のブックマークを追加できます。

次に例を挙げます。

int32 OpenInventory( … )
{
    TRACE_BOOKMARK(TEXT("Inventory.Open"));
}

ブックマークは、Unreal Insights の使用時にタイムラインに表示されます。これにより、この変更が視覚的に示され、ログの表示にも示されて検索しやすくなります。ブックマークは、ゲーム状態が頻繁に変更されない場合に使用されます。高頻度で変更が生じる場合は、イベント タイマーやカウンターの方が適切な選択肢となります。

Custom Event の作成

ビルトイン イベントがニーズに対して十分でない場合は、独自の カスタム イベント を実装できます。カスタム イベントを使用すると独自のカスタム ペイロードを定義する手段を得ることができますが、イベントを処理し、データを抽出するためのアナライザーを実装する必要があります。

イベントを定義する

トレース セッションは一連のイベントで構成されています。イベント はアプリケーション内で静的に記述されており、 ロガー名イベント名イベント フラグ 、そして次のように定義される多数のフィールドで構成されています。

    UE_TRACE_EVENT_BEGIN(LoggerName, EventName[, Flags])

        UE_TRACE_EVENT_FIELD(Type, FieldName)

        ...

    UE_TRACE_EVENT_END()

EventName および FieldName パラメータでイベントを定義し、イベントに含める必要のあるフィールドを指定します。イベントは「ロガー」によってグループ化されています。この概念により、各イベントを一つの名前空間にまとめて、トレース ストリームを解析する際のサブスクリプションを容易にします。オプションである Flags パラメータでは、イベントのトレース方法を変更します。

次の表を参照してください。

イベント フラグ

説明

NoSync

デフォルトでは、イベントは他のスレッドでトレースされているイベントと同期されます。NoSync フラグを含むイベントはこの同期をスキップします。解析時に他のスレッドとの一貫性が失われますが、これらのサイズは小さく、より迅速にトレースすることができます。

Important

重要なイベントとしてマークします。通常のイベントと重要なイベントの詳細については、後述の「重要なイベント」セクションを参照してください。重要なイベントは、通常のイベントの帯域外として処理されるため、NoSync も必要です。

フィールド は名前付けされて、強力にタイプ化されます。フィールドには、標準の整数タイプまたは浮動小数点プリミティブ タイプ (uint8、uint32、float など) のほか、配列タイプや文字列タイプを使用できます。

フィールド タイプ

説明

uint8, uint16, uint32, uint64

よく使用する整数タイプ。

<< FieldName(-10)

float, double

よく使用する浮動小数点タイプ。

<< FieldName(1.0f)

UE::Trace::Widestring

ワイド文字列。

<< FieldName(Ptr, NumChars*)

UE::Trace::Ansistring

Ansi 文字列。

<< FieldName(Ptr, NumChars*)

Type[]

配列。

<< FieldName(Ptr, NumElements)

  • null ターミネータを除く文字の数。

フィールドはパディングなしにストリームに書き込まれます。フィールドとしてネスト化された構造またはイベントはサポートされませんが、一般的なパターンとして、一意の ID フィールド を埋め込んで解析で解決することで、以前のイベントを参照します。

イベントは、通常、.cpp ファイルのグローバル スコープで定義されます。複数の翻訳ユニットからイベントをトレースする必要がある場合は、UE_TRACE_EVENT_BEGIN_[EXTERN|DEFINE] ペアを使用することができます。

配列

単一の可変長のフィールドを未指定サイズの配列として定義することで、トレース イベントに追加することができます。

    UE_TRACE_EVENT_BEGIN(BoniLogger, BerkEvent)
    UE_TRACE_EVENT_FIELD(int32[], DruttField)
    UE_TRACE_EVENT_END()

配列タイプのフィールドには、フィールドにデータが設定されていない場合、イベントのペイロードにストレージ コストはかかりません。配列データはトレース ストリーム内のメイン イベントのデータに従い、解析時に再結合されます。配列フィールドをトレースする際は、配列データへのポインタと、配列内の要素の数を示す整数カウントのみが必要です。

次に例を挙げます。

UE_TRACE_LOG(BoniLogger, BerkEvent, UpstairsChannel)

<< BerkEvent.DruttField(IntPtr, IntNum);

アタッチメント

当初、トレースは可変長フィールドをサポートしていませんでした。アタッチメント は、システムがイベントに追加する不透明型のバイナリ ブロブとして導入されていました。アタッチメントの代わりに、解析時に構造化されて反映されるというメリットを持つ、配列タイプのフィールドを使用することを推奨します。

アタッチメントのサポートでは、ログされたすべてのイベントで (配列タイプのフィールドでは発生しない) 負荷が生じるため、このオーバーヘッドを最適化するために、将来的にはオプトイン コンポーネントに変更される可能性があります。

文字列

トレース イベントでは、 UE_TRACE_EVENT_FIELD() でイベントのフィールドを宣言する際に、 Trace::AnsiString タイプまたは Trace::WideString タイプを使用する文字列タイプ フィールドをサポートします。

    UE_TRACE_EVENT_BEGIN(MyLogger, MyEvent)

        UE_TRACE_EVENT_FIELD(Trace::AnsiString, MyFieldA)
        UE_TRACE_EVENT_FIELD(Trace::WideString, MyFieldW)

    UE_TRACE_EVENT_END()

文字列タイプのフィールドはプリミティブ タイプのフィールドとほぼ同じように記述されますが、いくつかの追加要素があります。ASCII タイプのフィールドではワイド文字列を自動的に 7 ビット文字に切り捨てます。オプションで文字列長を指定することもできます (文字列長がわかっている場合はパフォーマンスの面で望ましい方法)。

    UE_TRACE_LOG(MyLogger, MyEvent)

        << MyFieldA(AnAnsiBuffer, [, ExplicitStringLen])
        << MyFieldW(WideName)

通常のイベント

イベントが UE_TRACE_LOG サイトでトレースされると、システムによってヘッダとイベントのフィールドの値が、現在のスレッドのローカル バッファに スレッド ローカル ストレージ (TLS) として書き込まれます。これらの TLS バッファは小さな固定サイズに設定され、一つのリストにまとめてリンクされます。トレースのワーカー スレッドはバッファのリストをトラバースし、コミットされたイベント データを送信します (そのため、完全に参照可能です)。TLS を使用するメリットは、トレースしているスレッド間での競合を回避できる点にあります。 スレッド間の操作の順序はイベント タイプでは重要で、トレース データの解析時には再構築する必要があります (メモリ アドレスの再利用が可能なメモリ トレーシング イベントなど)。 イベントで同期が必要な場合、トレースでは、各イベントの冒頭にあるアトミックに増分する 24 ビットのシリアル番号を使用します。イベントはデフォルトで同期されますが、NoSync フラグを使用することでこの動作をオプトアウトできます。そうすることで、関連するパフォーマンス コストの発生を回避してサイズを抑えられますが、解析中に他のスレッドと調整する機能は使用できなくなります。

重要なイベント

トレーシングはランタイム時のあらゆる時点で開始/停止できます。しかし、解析において重要で、プロセスの存続期間全体で一度だけ発生するイベントもあります。例えば、プロセッサの頻度を記述するイベントや、人間が判読しやすい名前をタイマーに指定するイベントなどがあります。このようなイベントを新しい接続ごとに発生可能にするため、トレースではイベントを「重要 (Important)」とマークすることができます。

重要なイベント は特殊なバッファに保管されます。このバッファはプロセスの存続期間全体に渡って維持されるため、この機能を使用する際はそのメモリ コストについても考慮する必要があります。

チャンネル

トレースの チャンネル は、ユーザーの関心に基づいてイベントのストリームを制限するのに役立ちます。これにより、ユーザーが観察しようとしているものに関連するイベントのみをトレースすることで、CPU とメモリの使用効率が向上します。以下の構文を使ってチャンネルを定義します。

UE_TRACE_CHANNEL(ItvChannel);

より具体的なユースケース向けには EXTERN/DEFINE ペアがあります。チャンネルはデフォルトで無効になっており、明示的にオプトインする必要があります。チャンネルを有効にする方法については、 トレース について参照してください。

それぞれのチャンネルはログ マクロにまとめることができ、こうすることで複数のチャンネルを含むイベントのトレースを制御できます。UE_TRACE_LOG(..., ItvChannel|BbcChannel) は、ItvBbc の両方のチャンネルが有効になっている場合にのみイベントを発生させます。

チャンネルでは OR 演算子を使ってコンポジット マスクを作成します。これはさまざまなフラグからビットマスクを構築する方法に似ています。

イベントをトレースする

ランタイム時には UE_TRACE_LOG マクロを使ってイベントをログ記録できます。

UE_TRACE_LOG(RainbowLogger, ZippyEvent, ItvChannel)

<< ZippyEvent.Field0(Value0)

<< ZippyEvent.Field1(BundleValue)

<< ZippyEvent.Field2(Data, Num);

<< ZippyEvent.Field3(String[, Num]);

ItvChannel チャンネルが有効になっている場合は、トレース ストリームに「'RainbowLogger.ZippyEvent'」イベントが追加されます。

すべてのフィールドに書き込む必要はありませんが、イベントのトレース時にはデルタ圧縮やランレングス圧縮はありません。フィールドに書き込むデータがない場合でも、定義されたすべてのフィールドが表示されます。フィールド間のパディングはありません。トレースされたイベントは基本的に #pragma pack(1) で宣言された構造体に似ています。UE_TRACE_LOG で表されるのは単一の時点ですが、 時間範囲 を表すことが役立つ場合もあります。

UE_TRACE_LOG_SCOPE を使用すると、イベントを開始点および終了点として発生させることができます。この使用法の詳細については、「重要なイベント」を参照してください。スコープを使用すると、スコープ内で発生するその他のイベントを判定できますが、タイムスタンプは利用できません。時間を使用する他のイベントと関連付ける必要がある場合は、 UE_TRACE_LOG_SCOPE_T を使用できます。

このシステムでは、マクロを多用して大量のボイラー プレートを非表示にし、デベロッパーがコード全体で #if#endif のペアを使用しなくても、トレースがオフのときに定義サイトとログ サイトが何にもコンパイルされないようにします。

重要なイベント

重要なイベントのトレーシングには、いくつかの追加の要件があります。これらのイベントはスレッド全体で共有されるキャッシュに格納されるため、ロギングを行うマクロで、消費される変数のメモリ量を事前に認識されている必要があります。たとえば、以下のイベントを考えてみましょう。

    UE_TRACE_EVENT_BEGIN(BoniLogger, BarkEvent, Important|NoSync)

        UE_TRACE_EVENT_FIELD(WideString, WoofString)

        UE_TRACE_EVENT_FIELD(int64[], DratField)

    UE_TRACE_EVENT_END()

このイベントは、以下のようにトレースされます。

    void Func(const TCHAR* Woof, const TArray<int64>& Drat)
    {
        const uint32 WoofLen = FCString::Len(Woof);

        const uint32 WoofSize = WoofLen * sizeof(TCHAR);

        const uint32 DratSize = Drat.Num() * sizeof(int64);

        UE_TRACE_LOG(BoniLogger, BarkEvent, BoniChannel, WoofSize + DratSize)

         << BarkEvent.WoofString(Woof, WoofLen)

         << BarkEvent.DratField(Drat.GetData(), Drat.Num());
    }

変数データの合計サイズは、ログ マクロの省略記号の引数に渡されます。

カスタム イベントを分析する

新しいイベントを定義して、関連する 1 つまたは複数のチャネルを有効にし、少なくとも 1 つのログ サイトを追加したので、イベントを消費して解析し、それらを公開する準備ができました。アナライザープロバイダ のパターンを使ってこれを行います。アナライザーではデータを各イベントから抽出し、それを対応するプロバイダに渡します。プロバイダでは、そのデータを UI や他の出力に供給します。

アナライザーは IAnalyzer インターフェースから派生し、主要な 2 つのメソッドを実行します。

  • イベントをサブスクライブする OnAnalysisBegin

  • これらのサブスクリプションを受け取る OnEvent

プロバイダは IProvider インターフェースから派生します。所定の実装方法はありませんが、アナライザー スレッドと UI スレッドは非同期でプロバイダにアクセスするため、プロバイダでのデータへのアクセスがスレッドセーフであることを確認してください。

イベントを受け取るには、アナライザーとプロバイダをアナライザー セッションに追加する必要があります。一般的なパターンとしては、構築時にプロバイダとアナライザーへのポインタを供給します。

    FRainbowProvider* RainbowProvider = new FRainbowProvider(Session);  

    Session.AddProvider(TEXT("RainbowProvider"), RainbowProvider);  

    Session.AddAnalyzer(new FRainbowAnalyzer(Session, RainbowProvider));

アナライザー

アナライザーでは、ロガー名とイベントの名前を使ってイベントをサブスクライブします。サブスクリプション インターフェースでは、それぞれのイベント タイプを ルート ID (通常は列挙型で定義) と呼ばれるユーザー定義のインデックスに関連付けます。

    void FRainbowAnalyzer::OnAnalysisBegin(const FOnAnalysisContext& Context)

    {
        auto& Builder = Context.InterfaceBuilder;

        Builder.RouteEvent(RouteId_Zippy, "RainbowLogger", "ZippyEvent");
    }

解析でアナライザーがサブスクライブしたイベントが発生すると、登録されたルート ID とともにアナライザーの OnEvent メソッドが呼び出されます。イベントのコンテキストにより、各 フィールド のデータ、 スレッドタイミング情報 を抽出するためのメソッドが提供されます。この API はトレース ストリームの自己記述型の性質を反映しています。すなわち、トレース ストリームの解釈をバイナリやランタイム コードに依存しないという性質です。

    bool FRainbowAnalyzer::OnEvent(uint16 RouteId, EStyle Style, const FOnEventContext& Context)  
    {  
        switch(RouteId)
        {
            case RouteId_Zippy:
            {
                uint32 Field0 = Context.EventData.GetValue<uint32>("Field0");

                FStringView Field3;

                Context.EventData.GetString("Field3", Field3);

                TArrayReader<int64>& Field4 = EventData.GetArray<int64>("Field4");

                RainbowProvider->AddZippy(Field0, Field3, Field4);  
            }
            break;
    }

スレッド ID

システム ID は使用可能であるものの、重要なスレッド ID はシステム スレッド ID と同じではありません。これにより、オペレーティング システムがスレッド ID を再利用してしまう可能性を防ぐための特別な処理が不要になります。その結果、あるスレッドから次のスレッドへのシステム ID の再利用が発生する可能性は残りますが、トレースからの ID は一意になります。

Unreal Insights のプラグイン

一般に、Unreal Insights のコンポーネントは、プロバイダのデータを消費し、視覚化します。カスタム プロバイダを実装している場合は、カスタムのビジュアライゼーションを実装する必要があるかもしれません。SlateInsightsRenderGraphInsights は、エンジンとともに配布される 2 つのサンプル プラグインであり、参考用として利用できます。

参考

トレースと Unreal Insights は上級ユーザーに向けて柔軟かつ拡張可能に設計されています。Unreal Insights アプリケーションとプラグインの実装にとどまらず、コンポーネントはさまざまな方法で利用できます。

カスタム アナライザーを作成する

データを異なる方法で出力し、レポートを生成したり類似するニーズに対応したりする場合、スタンドアローン プログラムを実装し、カスタム アナライザーを使用して目的のイベントを抽出し、必要な形式でデータを出力できます。 このサンプルは、「\Engine\Source\Developer\TraceInsights\Private\Insights\StoreService\StoreBrowser.cpp」ファイル内の FStoreBrowser::UpdateMetadata() メソッドにあります。 このメソッドでは、分析コンテキストを作成します。アナライザー FDiagnosticsSessionAnalyzer が追加され、ある特定のイベント タイプ ("Session/Session2") を検索します。トレースの読み取り時に他のすべてのイベントはスキップされ、セッション イベントが見つかると、その後の処理は不要になります。この情報は、セッション ブラウザでトレースのメタデータを表示するために使用されます。