Взаимодействие Unreal Insights c Unreal Engine 5 с точки зрения исходного кода

от автора

Читая исходный код Unreal Engine 5 я частенько стал натыкаться на загадочный макрос UE_TRACE_LOG (например, использование этого макроса можно заметить в коде UE_LOG). В этой статье я хотел бы рассказать, зачем нужен макрос UE_TRACE_LOG и как он связан с Unreal Insights.

Unreal Insights

Начнем пожалуй с того, что такое Unreal Insights.

Unreal Insights — это отдельная программа, которая по мере хода игры собирает различную полезную информацию и структурирует её. Например в Unreal Insights можно посмотреть количество кадров в разные моменты времени, потребление памяти, сетевую производительность и т.п.

Unreal Insights Interface

Unreal Insights Interface

UE_TRACE_EVENT

Trace ивент — это некоторая структура, которая содержит поля типа TField (1*). Они используются для того, чтобы понять, сколько памяти требуется для хранения тех типов данных, под которые создается TField, в некотором буфере, а также для реализации метода FieldName, в котором вызывается функция Impl класса FFieldSet (2*).

В общем, Trace ивент задает общий размер указанных типов данных, для хранения их в некотором буфере, а также контролирует, в каком именно порядке эти данные должны помещаться в буфер.

Объявление Trace ивента:

Чтобы объявить Trace ивент, нужно вызвать два макроса: UE_TRACE_EVENT_BEGIN и UE_TRACE_EVENT_END; а между ними вставить один или несколько макросов UE_TRACE_EVENT_FIELD.

  • Макрос UE_TRACE_EVENT_BEGIN(LoggerName, EventName, …) объявляет структуру типа F##LoggerName##EventName##Fields (3*), которая как раз таки и является самим Trace ивентом.

  • Макрос UE_TRACE_EVENT_FIELD(FieldType, FieldName) добавляет новое поле типа TField в Trace ивент.

  • Макрос UE_TRACE_EVENT_END() служит завершением объявления структуры F##LoggerName##EventName##Fields.

Пример объявления Trace ивента:

UE_TRACE_EVENT_BEGIN(Logging, LogMessageSpec, NoSync|Important) UE_TRACE_EVENT_FIELD(const void*, LogPoint) UE_TRACE_EVENT_FIELD(const void*, CategoryPointer) UE_TRACE_EVENT_FIELD(int32, Line) UE_TRACE_EVENT_FIELD(uint8, Verbosity) UE_TRACE_EVENT_FIELD(UE::Trace::AnsiString, FileName) UE_TRACE_EVENT_FIELD(UE::Trace::WideString, FormatString) UE_TRACE_EVENT_END() 

Итого, смысл структуры F##LoggerName##EventName##Fields заключается в хранении полей типа TField, которые в свою очередь хранят информацию о некотором типе (выбранном нами макросом UE_TRACE_EVENT_FIELD).

Чтобы записать значения некоторого типа в буфер, нужно вызвать функции с именем FieldName (параметр FieldName мы передаем в макрос UE_TRACE_EVENT_FIELD при объявлении Trace ивента), и передать в них некоторые значения. Важно понимать, что требуется вызывать эти функции по тому же порядку, в котором располагаются поля типа TField в Trace ивенте (сверху вниз).

Trace ивент не используется непосредственно для хранения данных.

(1*) TField — это структура, у которой определены поля Index, Offset и Size (которые задаются в зависимости от переданного в эту структуру типа). Эти поля так или иначе используются при записи данных в буфер через функцию Impl.

Например, в одной из специализаций структуры FFieldSet, используется поле Offset для смещения указателя буфера при записи:

template <typename FieldMeta, typename Type> struct FLogScope::FFieldSet { static void Impl(FLogScope* Scope, const Type& Value) { uint8* Dest = (uint8*)(Scope->Ptr) + FieldMeta::Offset; ::memcpy(Dest, &Value, sizeof(Type)); } };

Здесь TField передается в качестве шаблонного типа FieldMeta.

(2*) FFieldSet — структура, которая содержит метод Impl, использующийся для того, чтобы сказать каким именно образом нужно записывать тот или иной тип данных в буфер. То есть в зависимости от определенного типа данных, у FFieldSet может существовать определенная специализация. Соответственно метод Impl будет отличаться.

Реализация стандартной структуры FFieldSet, где в методе Impl осуществляется обычное копирование данных в буфер через функцию memcpy:

template <typename FieldMeta, typename Type> struct FLogScope::FFieldSet { static void Impl(FLogScope* Scope, const Type& Value) { uint8* Dest = (uint8*)(Scope->Ptr) + FieldMeta::Offset; ::memcpy(Dest, &Value, sizeof(Type)); } };

(3*) Полный код структуры F##LoggerName##EventName##Fields:

struct F##LoggerName##EventName##Fields \ { \ enum \ { \ Important= UE::Trace::Private::FEventInfo::Flag_Important, \ NoSync= UE::Trace::Private::FEventInfo::Flag_NoSync, \ Definition8bit= UE::Trace::Private::FEventInfo::Flag_Definition8, \ Definition16bit= UE::Trace::Private::FEventInfo::Flag_Definition16, \ Definition32bit= UE::Trace::Private::FEventInfo::Flag_Definition32, \ Definition64bit= UE::Trace::Private::FEventInfo::Flag_Definition64, \ DefinitionBits= UE::Trace::Private::FEventInfo::DefinitionBits, \ PartialEventFlags= (0, ##__VA_ARGS__), \ }; \ enum : bool { bIsImportant = ((0, ##__VA_ARGS__) & Important) != 0, bIsDefinition = ((0, ##__VA_ARGS__) & DefinitionBits) != 0,\ bIsDefinition8 = ((0, ##__VA_ARGS__) & Definition8bit) != 0, \ bIsDefinition16 = ((0, ##__VA_ARGS__) & Definition16bit) != 0,\ bIsDefinition32 = ((0, ##__VA_ARGS__) & Definition32bit) != 0, \ bIsDefinition64 = ((0, ##__VA_ARGS__) & Definition64bit) != 0,}; \ typedef std::conditional_t<bIsDefinition8, UE::Trace::FEventRef8, std::conditional_t<bIsDefinition16, UE::Trace::FEventRef16 , std::conditional_t<bIsDefinition64, UE::Trace::FEventRef64, UE::Trace::FEventRef32>>> DefinitionType;\ static constexpr uint32 GetSize() { return EventProps_Meta::Size; } \ - сумма размеров всех типов полей TField (то есть сумма типов, которые хранит TField). static uint32 TSAN_SAFE GetUid() { static uint32 Uid = 0; return (Uid = Uid ? Uid : Initialize()); } \ - ID данного инвента static uint32 FORCENOINLINE Initialize() \ генерирует ID для нашего ивента и определяет информацию о ивенте (UE::Trace::Private::FEventInfo Info), а так же вписывает инфу и ID в экземпляр FEventNode. { \ static const uint32 Uid_ThreadSafeInit = [] () \ { \ using namespace UE::Trace; \ static F##LoggerName##EventName##Fields Fields; \ static UE::Trace::Private::FEventInfo Info = \ { \ FLiteralName(#LoggerName), \ FLiteralName(#EventName), \ (FFieldDesc*)(&Fields), \ EventProps_Meta::NumFields, \ uint16(EventFlags), \ }; \ return LoggerName##EventName##Event.Initialize(&Info); \ }(); \ return Uid_ThreadSafeInit; \ } \ typedef UE::Trace::TField<0 /*Index*/, 0 /*Offset*/, 

Код макроса UE_TRACE_EVENT_FIELD:

#define TRACE_PRIVATE_EVENT_FIELD(FieldType, FieldName) \ FieldType> FieldName##_Meta; \ FieldName##_Meta const FieldName##_Field = UE::Trace::FLiteralName(#FieldName); \ template <typename... Ts> auto FieldName(Ts... ts) const { \ LogScopeType::FFieldSet<FieldName##_Meta, FieldType>::Impl((LogScopeType*)this, Forward<Ts>(ts)...); \ - информация записывается поверх полей TField. return true; \ } \ typedef UE::Trace::TField< \ FieldName##_Meta::Index + 1, \ FieldName##_Meta::Offset + FieldName##_Meta::Size, 

Как можно видеть, макрос UE_TRACE_EVENT_FIELD дополняет структуру F##LoggerName##EventName##Fields, тем самым объявляя новый TField.

UE_TRACE_LOG

Макрос, который регистрирует наш ивент (записывает информацию, которую мы вносим через оператор<<, в некоторый буфер. В конечном итоге этот буфер передается в Unreal Insights).

Пример использования UE_TRACE_LOG:

UE_TRACE_LOG(Logging, LogCategory, LogChannel, NameLen * sizeof(ANSICHAR)) << LogCategory.CategoryPointer(Category) << LogCategory.DefaultVerbosity(DefaultVerbosity) << LogCategory.Name(Name, NameLen);

Параметры Logging и LogCategory являются атрибутами структуры F##LoggerName##EventName##Fields. При этом параметр LogChannel также является «экземпляром» структуры F##LoggerName##EventName##Fields. Последний параметр NameLen * sizeof(ANSICHAR) представляет собой размер, который необходимо выделить в буфере для занесения в него переданных данных.

Скрытый текст

Полный код UE_TRACE_LOG:

#define UE_TRACE_LOG(LoggerName, EventName, ChannelsExpr, ...) \ TRACE_PRIVATE_LOG_PRELUDE(Enter, LoggerName, EventName, ChannelsExpr, ##__VA_ARGS__) \ TRACE_PRIVATE_LOG_EPILOG()

Полный код TRACE_PRIVATE_LOG_PRELUDE:

#define TRACE_PRIVATE_LOG_PRELUDE(EnterFunc, LoggerName, EventName, ChannelsExpr, ...) \ if (TRACE_PRIVATE_CHANNELEXPR_IS_ENABLED(ChannelsExpr)) \ if (auto LogScope = F##LoggerName##EventName##Fields::LogScopeType::EnterFunc<F##LoggerName##EventName##Fields>(__VA_ARGS__)) \ - создание экземпляра класса FLogScope, который будет хранить всю вносимую информацию нашего ивента. if (const auto& __restrict EventName = *UE_LAUNDER((F##LoggerName##EventName##Fields*)(&LogScope))) \ - получаем указатель на начало буффера и читаем его как структуру F##LoggerName##EventName##Fields(ивент), для того чтобы инициализировать память буффера нашими значениями (тут важно сказать, что мы не инициализируем объект F##LoggerName##EventName##Fields, так как он тут и не нужен. Мы просто пользуемся его функционалом для выделения памяти) ((void)EventName), - возможно некоторые компиляторы выдают сообщение о неиспользованной переменной => кастим EventName к void, чтобы предупреждения не выдавало

Полный код TRACE_PRIVATE_LOG_EPILOG:

#define TRACE_PRIVATE_LOG_EPILOG() \ LogScope += LogScope - оператор+=: сохранение указателя на буффер в некоторое хранилище для того чтобы в дальнейшем иметь в нему доступ.

FLogScope

FLogScope — класс, который хранит некоторый буфер. FLogScope также имеет инструменты для записи различных данных в этот буфер (через перегрузки операторов += и <<).

Оператор <<:

const FLogScope&operator << (bool) const{ return *this; }

Оператор +=:

template <bool bMaybeHasAux> inline void TLogScope<bMaybeHasAux>::operator += (const FLogScope&) const { if constexpr (bMaybeHasAux) { FWriteBuffer* LatestBuffer = Writer_GetBuffer(); LatestBuffer->Cursor[0] = uint8(EKnownEventUids::AuxDataTerminal << EKnownEventUids::_UidShift); LatestBuffer->Cursor++;  Commit(LatestBuffer); } else { Commit(); } }

Метод Commit в свою очередь просто назначает полю Commited буфера нужный адрес в памяти (куда были выгружены данные: а именно поле Cursor буфера).

В дальнейшем Unreal Insights будет обращаться именно к полю Commited (также поле Commited помечено ключевым словом volatile).

Итоги

То есть сначала мы в буфер правого LogScope записываем информацию (оператор<<) (она отобразится и для левого LogScope, так как при передаче в оператор+= LogScope не копируется), а затем эта информация сохраняется в буфере этого LogScope.

Тут также стоит сказать, что перед записью основной информации в FLogScope, сначала записывается информация о ID текущего Trace ивента и размере наполняемого пакета (текущего буфера). То есть именно по переданному ID Trace ивента Unreal Insights понимает, в какую вкладку (Log, Frame, …) включать поступающую из памяти информацию.


ссылка на оригинал статьи https://habr.com/ru/articles/843024/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *