트레이스 개발자 가이드

언리얼 인사이트를 사용하여 자체 트레이스를 개발하는 방법에 대한 튜토리얼입니다.

트레이스(Trace) 는 실행 중인 프로세스에서 계측 이벤트를 트레이싱하기 위한 구조화된 로깅 프레임워크입니다. 이 프레임워크는 직관적이며 소비하고 공유하기 쉬운 트레이스 이벤트 스트림을 높은 빈도로 생성하도록 설계되었습니다. TraceLogTraceAnalysis 모듈은 프레임워크를 구성하는 주요 모듈입니다.

언리얼 인사이트(Unreal Insights) 의 주요 구성 요소는 트레이스 이벤트(Trace events), 애플리케이션의 트레이스를 기록하고 저장하는 언리얼 트레이스 서버(Unreal Trace Server), 데이터를 분석 및 시각화 하는 타이밍 인사이트(Timing Insights) 입니다.

언리얼 인사이트 주요 구성 요소 도표

저장한 트레이스 세션은 스스로 설명되고 하위 호환됩니다. 세션은 .utrace 파일로 저장되며, 보조 데이터가 함께 생성되어 트레이스 파일과 같은 위치에 .ucache 파일로 저장됩니다.

트레이스 데이터는 패킷으로 이동하며, 이를 트랜스포트(Transport) 라고 합니다. 각 패킷은 이벤트가 어떤 스레드에서 왔는지, 크기는 어느 정도인지 나타내는 내부 식별자 로 시작합니다. 패킷은 너무 작아서 압축해도 효과가 없는 경우가 아니라면 LZ4 로 압축됩니다.

내장 이벤트 타입 사용하기

언리얼 엔진에는 다양한 이벤트 타입이 사전 정의되어 있습니다. 이벤트 타입에는 퍼포먼스 타이머, 메모리 할당 등의 공통 프로파일링 정보가 있습니다. 매크로 및 인터페이스에 노출되는 이벤트는 Core/ProfilingDebugging 폴더에 있습니다.

자체 커스텀 이벤트 타입을 구현하기에 앞서 이러한 API를 사용할 것을 강력히 권장합니다. 내장 이벤트 타입을 사용하면 내장 분석 툴과 시각화를 활용할 수 있습니다.

타이머

가장 일반적인 프로파일링 작업은 애플리케이션의 퍼포먼스를 측정하는 것입니다. Core/ProfilingDebugging/CpuProfilingTrace.h 파일에서 타이머 이벤트를 발생시키는 함수 기능을 찾을 수 있습니다. 매크로 패밀리 TRACE_CPUPROFILER_EVENT_SCOPE_ 를 사용하기를 권장합니다. 이 매크로를 사용하면 영역 내에서 애플리케이션이 소비하는 시간을 쉽게 측정할 수 있습니다.

    {
        TRACE_CPUPROFILER_EVENT_SCOPE_STR("근사한 작업");
        // 근사한 작업 진행...
    }

이 코드 샘플은 타이밍 인사이트 타임라인에 '근사한 작업' 타이머를 표시합니다. 예시에서는 스태틱 스트링을 사용합니다. 다이내믹 스트링도 지원되지만, 스태틱 스트링에 비해 추가 퍼포먼스 및 메모리 오버헤드가 발생합니다.

많은 내장 매크로에는 TRACE_CPUPROFILER_EVENT_SCOPE 가 포함됩니다. 예를 들어 SCOPE_CYCLE_COUNTER , QUICK_SCOPE_CYCLE_COUNTER , SCOPED_NAMED_EVENT (-statnamedevents 가 설정된 경우)가 있습니다.

카운터

Core/ProfilingDebugging/CountersTrace.h 파일에는 명명된 값을 선언하고 트레이스하기 위한 일반 인터페이스가 포함됩니다. 이 인터페이스를 사용하여 시간에 따른 값을 트래킹할 수 있습니다. 이 인터페이스는 integer, float, 일반 연산자(set, increment, decrement)를 포함한 메모리 값을 지원합니다.

예를 들면 다음과 같습니다.

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

이 코드 샘플은 타이밍 인사이트카운터(Counters) 탭에 두 개의 카운터 AlienHitsAlienBytes 를 표시합니다.

메모리

메모리 트레이싱(Memory tracing) 은 일반적인 할당을 커버하는 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 파일에는 프로파일링 시 컨텍스트를 지원하는 유틸리티 매크로 세트가 있습니다. 예를 들면 프레임 마커(Frame markers)북마크(Bookmarks) 등이 있습니다. 북마크는 애플리케이션에서 중요한 변화를 한눈에 식별하는 데 유용합니다. TRACE_BOOKMARK 매크로를 사용하여 직접 추가할 수 있습니다.

예를 들면 다음과 같습니다.

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

북마크는 언리얼 인사이트 사용 시 타임라인에 표시되어 변경 사항을 로그 뷰에 시각적으로 표시하며 검색을 용이하게 합니다. 북마크는 게임 스테이트의 빈번하지 않은 변경 사항에 사용하는 용도입니다. 변경이 빈번하다면 이벤트 타이머나 카운터가 더 나은 선택입니다.

커스텀 이벤트 생성하기

내장 이벤트로는 충분하지 않다면 커스텀 이벤트(Custom Events) 를 구현할 수 있습니다. 커스텀 이벤트로는 커스텀 페이로드를 정의할 수 있지만, 그러려면 이벤트를 처리하고 데이터를 추출하는 분석 툴을 구현해야 합니다.

이벤트 정의하기

트레이스 세션은 이벤트 스트림으로 구성됩니다. 이벤트 는 애플리케이션에서 통계적으로 설명되며 로거 이름(Logger Name), 이벤트 이름(Event Name), 이벤트 플래그(Event Flags), 그리고 아래와 같이 정의되는 다수의 필드로 구성됩니다.

    UE_TRACE_EVENT_BEGIN(LoggerName, EventName[, Flags])

        UE_TRACE_EVENT_FIELD(Type, FieldName)

        ...

    UE_TRACE_EVENT_END()

EventNameFieldName 파라미터는 이벤트를 정의하고 포함해야 할 필드를 지정합니다. 이벤트는 '로거(Logger)'로 그룹화됩니다. 로거는 이벤트를 네임스페이스로 조직하여 트레이스 스트림을 분석할 때 구독을 쉽게 해 주는 개념입니다. 선택 사항인 플래그(Flags) 파라미터는 이벤트 트레이스가 트레이스되는 방법을 수정합니다.

아래 테이블을 참조하세요.

이벤트 플래그

설명

NoSync

기본적으로 이벤트는 다른 스레드에서 트레이스 중인 이벤트와 동기화됩니다. NoSync 플래그가 있는 이벤트는 이 동기화를 건너뜁니다. 분석 중에 다른 스레드에 맞춰 조정되지 않는 대신, 더 작고 더 빠르게 트레이스됩니다.

Important

이벤트를 중요 이벤트로 표시합니다. 일반 이벤트와 중요 이벤트에 대한 자세한 정보는 아래의 중요 이벤트 섹션을 참고하세요. 중요 이벤트는 일반 이벤트 범위 외부에서 처리되기 때문에 NoSync 플래그가 필요합니다.

필드(Fields) 는 이름이 지정되며, 강타입(strongly typed)입니다. 필드 타입은 표준 인티저일 수도 있고, 부동 소수점 프리미티브(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);

어태치먼트

초기에는 트레이스가 변수 길이 필드를 지원하지 않았습니다. 어태치먼트(Attachments) 는 시스템이 이벤트에 첨부하는 불투명한 바이너리 블롭으로 도입되었습니다. 어태치먼트 대신 배열 타입 필드를 사용할 것을 권장합니다. 구조화되고 분석 시 반영된다는 장점이 있기 때문입니다.

어태치먼트 지원은 로깅되는 모든 이벤트에 비용을 유발하는 반면, 배열 타입 필드는 그러지 않습니다. 향후 어태치먼트는 이 오버헤드를 제거하고 최적화하기 위해 선택 방식으로 변경될 수 있습니다.

스트링

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 사이트에서 이벤트를 트레이스하면 시스템이 헤더와 이벤트의 필드 값을 버퍼에 작성합니다. 이 버퍼는 스레드 로컬 스토리지(Thread Local Storage, TLS) 로서 현재 스레드에 로컬입니다. TLS 버퍼는 작고 고정된 크기를 가지며 서로 연결되어 목록을 형성합니다. 트레이스의 작업자 스레드(worker thread)는 버퍼 목록을 오가며 커밋된 이벤트 데이터를 전송합니다. 그러므로 완전히 가시적입니다. TLS를 사용하면 트레이싱 스레드 간의 상충을 피할 수 있다는 장점이 있습니다. 스레드 간 운영 순서는 이벤트 타입에 중요하므로, 메모리 주소를 재사용할 수 있는 메모리 트레이싱 이벤트처럼 트레이스 데이터가 분석될 때는 반드시 재구성되어야 합니다. 이벤트에 동기화가 필요하다면 트레이스는 원자적으로 증가하는 24비트 일련번호로 각 이벤트에 우선합니다. 이벤트는 기본으로 동기화되지만, 개발자는 NoSync 플래그를 사용하여 이 기능을 해제할 수 있습니다. NoSync는 관련 퍼포먼스 비용을 없애고 크기를 줄이지만, 분석 중에 다른 스레드에 맞춰 조정되는 기능을 제거합니다.

중요 이벤트

트레이싱은 런타임에 언제든 시작 및 중단될 수 있습니다. 그러나 일부 이벤트는 분석에 필수적이며 프로세스의 수명 주기 중에 단 한 번만 수행됩니다. 예를 들어 프로세서 빈도를 설명하는 이벤트 또는 인간이 읽을 수 있는 타이머 이름을 지정하는 이벤트가 여기 해당합니다. 이러한 이벤트를 새 연결 시마다 발생시키려면 트레이스가 이벤트를 중요 이벤트로 표시할 수 있어야 합니다.

중요 이벤트(Important events) 는 프로세스의 전체 수명 주기 동안 유지되는 특수한 버퍼에 저장되며, 따라서 개발자는 이 기능을 사용할 때 메모리 비용을 고려해야 합니다.

채널

트레이스의 채널(Channels) 을 활용하면 사용자의 필요에 따라 이벤트 스트림을 제한할 수 있습니다. 채널은 사용자가 관찰하려는 것과 관련된 이벤트만 트레이싱하여 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는 시간의 한 점을 나타내지만, 때로는 시간 범위(time range) 를 나타내는 데도 유용합니다. 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());
    }

변수 데이터의 총 크기는 로그 매크로의 생략 기호 실행인자로 전달된다는 점에 유의하세요.

커스텀 이벤트 분석하기

새 이벤트를 정의하고, 관련 채널을 활성화하고, 하나 이상의 로그 사이트를 추가했으니, 이제 이벤트를 소비 및 분석하여 퍼블리시할 수 있습니다. 이를 위해 분석 툴(Analyzers)제공자(Providers) 패턴을 사용합니다. 분석 툴은 각 이벤트로부터 데이터를 추출하여 알맞은 제공자에게 입력하며, 제공자는 데이터를 UI 또는 다른 출력으로 보냅니다.

분석 툴은 IAnalyzer 인터페이스로부터 파생되어 두 개의 주요 메서드를 구현합니다.

  • OnAnalysisBegin 은 이벤트를 구독합니다.

  • OnEvent 는 구독을 수신합니다.

제공자는 IProvider 인터페이스로부터 파생됩니다. 이를 구현하는 필수 방식은 없지만, 제공자의 데이터 액세스는 스레드 세이프여야 합니다. 분석 툴 스레드와 UI 스레드의 제공자 액세스는 동기화되지 않기 때문입니다.

분석 툴과 제공자는 이벤트를 수신하기 위한 분석 툴 세션에 추가되어야 합니다. 일반적인 패턴은 생성 시 다음과 같이 제공자와 분석 툴에 포인터를 입력하는 것입니다.

    FRainbowProvider* RainbowProvider = new FRainbowProvider(Session);  

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

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

분석 툴

분석 툴은 로거 이름과 이벤트 이름을 사용하여 이벤트를 구독합니다. 구독 인터페이스는 각 이벤트 타입을 루트 ID(route ID) (보통 열거형으로 정의됨)로 알려진 사용자 정의 인덱스와 연결합니다.

    void FRainbowAnalyzer::OnAnalysisBegin(const FOnAnalysisContext& Context)

    {
        auto& Builder = Context.InterfaceBuilder;

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

분석 툴이 구독한 이벤트가 분석과 조우하면, 분석 툴의 OnEvent 메서드가 등록된 루트 ID와 함께 호출됩니다. 이벤트 컨텍스트는 각 필드, 스레드, 타이밍 정보 에 대한 데이터를 추출하는 메서드를 제공합니다. 이 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는 고유합니다.

언리얼 인사이트 플러그인

일반적으로 언리얼 인사이트의 컴포넌트는 제공자의 데이터를 소비하여 시각화합니다. 커스텀 제공자를 구현했다면 커스텀 시각화를 구현해야 할 수도 있습니다. SlateInsightsRenderGraphInsights 는 엔진과 함께 배포되는 예시 플러그인이며 레퍼런스 역할을 합니다.

더 확장하기

트레이스와 언리얼 인사이트는 고급 사용자를 위해 유연하고 확장 가능하도록 디자인되었습니다. 언리얼 인사이트 애플리케이션을 사용하고 플러그인을 구현할 뿐만 아니라 컴포넌트를 다른 방식으로 사용하는 것도 가능합니다.

커스텀 분석 툴 작성하기

데이터를 다른 방식으로 출력하여 보고서를 생성하거나 유사한 필요 사항을 충족하고 싶다면, 독립형 프로그램을 구현하고 커스텀 분석 툴을 사용하여 관심 있는 이벤트를 추출하고 데이터를 필요한 포맷으로 출력할 수 있습니다. \Engine\Source\Developer\TraceInsights\Private\Insights\StoreService\StoreBrowser.cpp 파일에 있는 FStoreBrowser::UpdateMetadata() 메서드에서 예시를 볼 수 있습니다. 이 메서드에서는 분석 컨텍스트를 생성합니다. 분석 툴 FDiagnosticsSessionAnalyzer가 추가되며, 이 툴은 하나의 특정 이벤트 타입("Session/Session2")을 찾습니다. 트레이스를 읽을 때는 다른 모든 이벤트를 건너뛰고, 세션 이벤트가 발견되면 추가 프로세스가 필요하지 않습니다. 이 정보는 세션 브라우저에서 메타데이터를 표시하는 데 사용됩니다.