找回密码
 立即注册
查看: 128|回复: 0

UE UnrealInsights 使用与实现

[复制链接]
发表于 2024-7-15 18:27 | 显示全部楼层 |阅读模式
Unreal Insights使用

UnrealInsights是UE的新一代Profile东西,用于替代原先的Stats,比如Frontend。它可以Profile CPU,GPU,Network,Animation,Slate,功能更强大,界面也更友好易用。斗劲常用的是CPU Profile,界面如下:


前两个函数用于更新持续状态等内容,可不管,重点看后两个函数。于浏览profile数据。
关于使用方式,官方文档中有详细的介绍
命令行启动
添加-trace=[channel1,channel2…] -statnamedevents -tracehost=127.0.0.1
当地先启动UnrealInsights处事器,然后启动游戏就能连上
一个例子如下,这几乎是最详细的标签了
-trace=CPU,Log,Bookmark,Frame,GPU,LoadTime,File,Net,Stats,Counters -statnamedevents -tracehost=127.0.0.1
UE有快速Prfile方式,Windows环境下,只需先打开UnrealInsights.exe,再打开standalone游戏,就可以进行profile,不需要额外命令行。
标识表记标帜

Unreal Insight可以使用以下标识表记标帜
TRACE_CPUPROFILER_EVENT_SCOPE(xxx)
TRACE_BOOKMARK(TEXT(”xxx %s ”), string)
也撑持现有stats标识表记标帜
SCOPE_CYCLE_COUNTER(xxxxx)
Channel

Channel暗示要Profile哪些类目,比如CpuChannel对应RACE_CPUPROFILER_EVENT_SCOPE标签,CpuChannel打开后该标签才会起效。
Channel通过命令行参数-trace配置,比如-trace=channel1,channel2…
XXXEngine.ini可以配置默认Channel,如果-trace没有跟Channel,就用Presets
[Trace.ChannelPresets]
Default=cpu,frame,log,bookmark
Rendering=gpu,cpu,frame,log,bookmark
其他Insights

Unreal Insights主要用于Profile CPU,UE还提供了其他几种Insights东西,用来Profile分歧部门。
Networking Insights



用于Profile网络数据流。可以看到网络流量变化以及数据包具体内容,比如Actor同步数据,同步了哪个Actor,属性同步数据,同步了哪些属性。
使用方式
-NetTrace=1 -tracehost=localhost -trace=net
其他还有Animation Insights和Slate Insights,就不外多介绍了,官方文档有更详细的说明。
Unreal Insights道理

开启流程

开启Unreal Insights主要使用FTraceAuxiliaryImpl类,该类负责解析命令行,控制连接处事器和打开insight文件,以及Unreal Insights的开启/封锁。
FTraceAuxiliaryImpl
主要属性
Tmap<uint32, FChannel> Channels 当前要profile的channel,比如cpu,frame,network等等,key为channel name的hash。注意此处的Channel只是真正Profile所使用Trace::Channel的handler,它们是两个类型,改削它们的Active属性后,要同步到“真正”的Channel实例。
TraceDest 连接的处事器地址或写入的文件名
初始开启
FEngineLoop::PreInitPreStartupScreen函数调用FTraceAuxiliary::Initialize函数,传入CmdLine,开启UnrealInsight。
FTraceAuxiliary::Initialize()
首先,把法式简要信息写入Buffer,包罗Platform,AppName等。
  1. UE_TRACE_LOG(Diagnostics, Session2, Trace::TraceLogChannel)
  2.         << Session2.Platform(PREPROCESSOR_TO_STRING(UBT_COMPILED_PLATFORM))
  3.         << Session2.AppName(UE_APP_NAME)
  4.         << Session2.CommandLine(CommandLine)
  5.         << Session2.ConfigurationType(uint8(FApp::GetBuildConfiguration()))
  6.         << Session2.TargetType(uint8(FApp::GetBuildTargetType()));
复制代码
接着解析-trace=channel1,channel2…参数中指定的Channel,把它们插手到Channels map中。每个Channel都有Active状态,开启后才会收集该Channel的数据,默认开启。如果用户没有提供Channel,UE会去Engine.ini获取Trace.ChannelPresets指定的预置Channel,默认是cpu,frame,log,bookmark。
之后解析-tracehost和-tracehost,判断要把profile数据发到某个insight处事器还是写入某个文件。

  • 连接处事器会创建一条TCP连接,过程是阻塞的,可指定对方ip:port,如不指定port就用默认的1980端口。
  • 写入文件可指定path和文件名,如果是相对路径,会写入Profiling目录,如果没特殊需求,相对路径就够了。写文件之前要把得到的/Game/….路径转换为各平台的路径,比如安卓就是/sdcard/…开头的路径,之后以write模式打开文件,筹备写入。
到这里初始化unreal insights读取命令行初始化就完成了,我们已配置channel,成立处事器连接或打开了文件。
windows缺省开启
我们大部门情况使用windows开发调试,因此UE为我们提供了windows缺省开启模式,只要当地打开insight客户端,再开游戏,就会使用默认Channel,自动连接,便利了profile。
Channel定义

Channel使用UE_TRACE_CHANNEL_DEFINE定义,例如CpuChannel
UE_TRACE_CHANNEL_DEFINE(CpuChannel)
创建名为CpuChannelObject的FChannel实例,作为静态变量,然后创建名为CpuChannel的引用对象。之后创建名为FCpuChannelRegister的布局体和实例,主要感化为调用CpuChannelObject.Setup函数进行注册。Channel新建后会保留在GNewChannelList链表中,Channel信息被WriterBuffer发送之后,链表被清空。
FChanne::Toggle函数的坑

不雅察看FChannel,发现有Toggle函数可以开启/封锁某个Channel,但其底层使用了原子P/V操作来改削Enabled,Enabled是int类型而不是bool。
  1. bool FChannel::Toggle(bool bEnabled)
  2. {
  3.         using namespace Private;
  4.         int64 OldRefCnt = AtomicAddRelaxed(&Enabled, bEnabled ? 1 : -1);
  5.         UE_TRACE_LOG(Trace, ChannelToggle, TraceLogChannel)
  6.                 << ChannelToggle.Id(Name.Hash)
  7.                 << ChannelToggle.IsEnabled(IsEnabled());
  8.         return IsEnabled();
  9. }
复制代码
意味着要小心Toggle()函数执行的次数,如果执行序列未Toggle(false), Toggle(false), Toggle(true),那么值还是false。
收集Profile数据

收集Profile数据是Unreal Insights最重要的部门。
创建profile标签

我们可以使用宏创建新的profile标签,常用的为TRACE_CPUPROFILER_EVENT_SCOPE(Name),可以在函数的第一行添加它,这样就记录了函数的总执行时间。
宏最终展开内容如下:
  1. #define TRACE_CPUPROFILER_EVENT_SCOPE_ON_CHANNEL_STR(NameStr, Channel) \
  2.         static uint32 PREPROCESSOR_JOIN(__CpuProfilerEventSpecId, __LINE__); \
  3.         if (bool(Channel|CpuChannel) && PREPROCESSOR_JOIN(__CpuProfilerEventSpecId, __LINE__) == 0) { \
  4.                 PREPROCESSOR_JOIN(__CpuProfilerEventSpecId, __LINE__) = FCpuProfilerTrace::OutputEventType(NameStr); \
  5.         } \
  6.         FCpuProfilerTrace::FEventScope PREPROCESSOR_JOIN(__CpuProfilerEventScope, __LINE__)(PREPROCESSOR_JOIN(__CpuProfilerEventSpecId, __LINE__), Channel);
复制代码
PREPROCESSOR_JOIN宏把两个参数拼接到一起,因此创建了名为__CpuProfilerEventSpecIdXXX的uint32变量,注意这是一个static变量,每个标签都对应一个,可以称为SpecId。之后判断该变量是否为0,即我们是否第一次进到这个标签,如果是第一次,要使用FCpuProfilerTrace::OutputEventType()函数获取新的Id,Id是自增的且可多线程访谒。期间还有UE_TRACE_LOG写入操作,这个下文再介绍。
最后创建FCuProfilerTrace::FEventScope对象,该对象的生命周期就是我们的profile周期,构造函数开始profile,析构函数结束profile。
FEventScope
  1. struct FEventScope
  2. {
  3.     FEventScope(uint32 InSpecId, const Trace::FChannel& Channel)
  4.         : bEnabled(Channel | CpuChannel)
  5.     {
  6.         if (bEnabled)
  7.         {
  8.             OutputBeginEvent(InSpecId);
  9.         }
  10.     }
  11.     ~FEventScope()
  12.     {
  13.         if (bEnabled)
  14.         {
  15.             OutputEndEvent();
  16.         }
  17.     }
  18.     bool bEnabled;
  19. };
复制代码
注意到无论构造函数还是析构函数,城市先判断bEnabled,而bEnabled由Chennel值决定。我们可以半途通过Trace.Start和Trace.Stop来动态改变Channel值,使后面代码不走,这样就能按需开启/封锁Profile数据收集了,但打开的TCP连接和文件描述符,还保留着。
构造函数
先看构造函数中的OutputBeginEvent,InSpecId就是我们之前创建的静态变量SpecId,函数内部又是两个宏。
  1. #define CPUPROFILERTRACE_OUTPUTBEGINEVENT_PROLOGUE() \
  2.         ++FCpuProfilerTraceInternal::ThreadDepth; \
  3.         FCpuProfilerTraceInternal::FThreadBuffer* ThreadBuffer = FCpuProfilerTraceInternal::ThreadBuffer; \
  4.         if (!ThreadBuffer) \
  5.         { \
  6.                 ThreadBuffer = FCpuProfilerTraceInternal::CreateThreadBuffer(); \
  7.         } \
  8. #define CPUPROFILERTRACE_OUTPUTBEGINEVENT_EPILOGUE() \
  9.         uint64 Cycle = FPlatformTime::Cycles64(); \
  10.         uint64 CycleDiff = Cycle - ThreadBuffer->LastCycle; \
  11.         ThreadBuffer->LastCycle = Cycle; \
  12.         uint8* BufferPtr = ThreadBuffer->Buffer + ThreadBuffer->BufferSize; \
  13.         FTraceUtils::Encode7bit((CycleDiff << 1) | 1ull, BufferPtr); \
  14.         FTraceUtils::Encode7bit(SpecId, BufferPtr); \
  15.         ThreadBuffer->BufferSize = (uint16)(BufferPtr - ThreadBuffer->Buffer); \
  16.         if (ThreadBuffer->BufferSize >= FCpuProfilerTraceInternal::FullBufferThreshold) \
  17.         { \
  18.                 FCpuProfilerTraceInternal::FlushThreadBuffer(ThreadBuffer); \
  19.         }
  20. void FCpuProfilerTrace::OutputBeginEvent(uint32 SpecId)
  21. {
  22.         CPUPROFILERTRACE_OUTPUTBEGINEVENT_PROLOGUE();
  23.         CPUPROFILERTRACE_OUTPUTBEGINEVENT_EPILOGUE();
  24. }
复制代码
CPUPROFILERTRACE_OUTPUTBEGINEVENT_PROLOGUE
首先递增ThreadDepth,它是ThreadLocal变量,用于暗示当前线程的CPUPROFILETRACE深度。之后按需创建同样是ThreadLocal变量的ThreadBuffer,用于记录Profile数据。
FThreadBuffer
FThreadBuffer可以理解为一块内存,用于存储多个EventScope的相关数据。
主要属性:
uint64 LastCycle 最后一次记录的时钟周期
uint8 Buffer[MaxBufferSize] 存储数据的Buffer,MaxBufferSize默认256
uint16 BufferSize 当前存储数据的size
类FCpuProfilerTraceInternal的静态成员ThreadBuffer就是当前线程所使用的ThreadBuffer。
CPUPROFILERTRACE_OUTPUTBEGINEVENT_EPILOGUE
之后获取当前CPU时钟tick计数,需要高精度,各平台实现分歧,比如Windows使用QueryPerformanceCounter,精度高于1微妙,ios平台则使用mach_absolute_time内核接口。得到tick计数后,只要计算与上次记录计数的Diff,然后把Diff和SpecId写入ThreadBuffer即可,因为后面计数会重置,以减小内存消耗。
注意到写入使用了Encode7bit接口,这是一种变长编码实现,可以节省内存。一个Byte只有7为存储数据,最高位暗示当前值是否还有后续部门待读取。举个例子:
存储127需要一个Byte
01111 1111
存储128需要两个Byte
1111 1111 0000 0001
这种变长编码被广泛应用于Protobuf等序列化框架中。
当ThreadBuffer长度超过了FullBufferThreshold,默认256-15,需要执行一次flush,使用UE_TRACE_LOG写入另一个区域,然后重置ThreadBuffer的Buffer和LastCycle。
析构函数
再看析构函数,其与构造函数对应,得到当前tick计数的Diff,然后写入ThreadBuffer,注意此次不需要写入id了。同样的,Buffer超阈值,或者Depth退回到0,要flush。
UE_TRACE_LOG

Profile数据最终会通过UE_TRACE_LOG进行写入,此刻分析它是如何工作的。
首先看生成SpecId时写入的例子:
  1. UE_TRACE_LOG(CpuProfiler, EventSpec, CpuChannel, NameSize)
  2.         << EventSpec.Id(SpecId)
  3.         << EventSpec.CharSize(uint8(sizeof(TCHAR)))
  4.         << EventSpec.Attachment(Name, NameSize);
复制代码
不是很直不雅观。。
还是需要先把宏都展开
第一步
  1. #define UE_TRACE_LOG(LoggerName, EventName, ChannelsExpr, ...)                        TRACE_PRIVATE_LOG(LoggerName, EventName, ChannelsExpr, ##__VA_ARGS__)
复制代码
UE_TRACE_LOG的前3个参数为Loggername,EventName,ChannelsExpr,最后一个参数为额外内存大小,这里是NameSize,用于之后存储Name。
Loggername不是一个独立的类型名或变量名,重点存眷EventSpec和ChannlExpr,对应上面例子,就是EventSpec,CpuChannel,看下它们是怎么来的。
EventSpec定义
  1. UE_TRACE_EVENT_BEGIN(CpuProfiler, EventSpec, Important)
  2.         UE_TRACE_EVENT_FIELD(uint32, Id)
  3.         UE_TRACE_EVENT_FIELD(uint8, CharSize)
  4. UE_TRACE_EVENT_END()
复制代码
这会创建一个名为CpuProfilerEventSpecEvent的FEventNode实例,以及名为FCpuProfilerEventSpecFields的布局体。
第二步
  1. #define TRACE_PRIVATE_LOG(LoggerName, EventName, ChannelsExpr, ...) \
  2.         TRACE_PRIVATE_LOG_PRELUDE(Enter, LoggerName, EventName, ChannelsExpr, ##__VA_ARGS__) \
  3.                 TRACE_PRIVATE_LOG_EPILOG()
  4.                
  5. #define TRACE_PRIVATE_LOG_PRELUDE(EnterFunc, LoggerName, EventName, ChannelsExpr, ...) \
  6.         if (TRACE_PRIVATE_CHANNELEXPR_IS_ENABLED(ChannelsExpr)) \
  7.                 if (auto LogScope = Trace::Private::TLogScope<F##LoggerName##EventName##Fields>::EnterFunc(__VA_ARGS__)) \
  8. if (const auto& __restrict EventName = *(F##LoggerName##EventName##Fields*)LogScope.GetPointer())
复制代码
这里会调用TLogScope<FCpuProfilerEventSpecFields>::Enter函数,创建一个FLogScope实例。
  1. template <class T>
  2. auto TLogScope<T>::Enter(uint32 ExtraSize)
  3. {
  4.         uint32 Size = T::GetSize() + ExtraSize;
  5.         uint32 Uid = T::GetUid();
  6.         using LogScopeType = typename TLogScopeSelector<T::bIsImportant>::Type;
  7.         return LogScopeType::template Enter<T::EventFlags>(Uid, Size);
  8. }
复制代码
Size包罗了成员变量大小,例子中为Id和CharSize的大小,之后调用Enter函数返回FLogScope实例或FImportantLogScope实例,后者只是简单担任了一下。
  1. class FLogScope
  2. {
  3. public:
  4.         template <uint32 Flags>
  5.         static FLogScope                Enter(uint32 Uid, uint32 Size);
  6.         uint8*                                        GetPointer() const;
  7.         void                                        Commit() const;
  8.         void                                        operator += (const FLogScope&) const;
  9.         const FLogScope&                operator << (bool) const        { return *this; }
  10.         constexpr explicit                operator bool () const                { return true; }
  11. private:
  12.         template <class T> void        EnterPrelude(uint32 Size, bool bMaybeHasAux);
  13.         void                                        Enter(uint32 Uid, uint32 Size, bool bMaybeHasAux);
  14.         void                                        EnterNoSync(uint32 Uid, uint32 Size, bool bMaybeHasAux);
  15.         struct
  16.         {
  17.                 uint8*                                Ptr;
  18.                 FWriteBuffer*                Buffer;
  19.         }                                                Instance;
  20. };
复制代码
最后进入到FLogScope::Enter函数,这里简单起见,只看FEventHeader版本。EnterPrelude函数中会获取当前线程的GTlsWriteBuffer,该Buffer用于实际数据写入,Buffer会预留FCpuProfilerEventSpecFields大小+Fields大小+NameSize(UE_TRACE_LOG最后一个参数)的内存,然后把Size和Uid写入Buffer。
  1. inline void FLogScope::EnterNoSync(uint32 Uid, uint32 Size, bool bMaybeHasAux)
  2. {
  3.         EnterPrelude<FEventHeader>(Size, bMaybeHasAux);
  4.         // Event header
  5.         auto* Header = (uint16*)(Instance.Ptr);
  6.         Header[-1] = uint16(Size);
  7.         Header[-2] = uint16(Uid)|int(EKnownEventUids::Flag_TwoByteUid);
  8. }
  9. template <class HeaderType>
  10. inline void FLogScope::EnterPrelude(uint32 Size, bool bMaybeHasAux)
  11. {
  12.         uint32 AllocSize = sizeof(HeaderType) + Size + int(bMaybeHasAux);
  13.         FWriteBuffer* Buffer = Writer_GetBuffer();
  14.         Buffer->Cursor += AllocSize;
  15.         if (UNLIKELY(Buffer->Cursor > (uint8*)Buffer))
  16.         {
  17.                 Buffer = Writer_NextBuffer(AllocSize);
  18.         }
  19.         // The auxilary data null terminator.
  20.         if (bMaybeHasAux)
  21.         {
  22.                 Buffer->Cursor[-1] = 0;
  23.         }
  24.         uint8* Cursor = Buffer->Cursor - Size - int(bMaybeHasAux);
  25.         Instance = {Cursor, Buffer};
  26. }
复制代码
接着执行下面的宏,FLogScope重载了+=操作符,会执行FLogScope::Commit函数,感化为把Buffer当前Cursor更新到Buffer.Commit,因此称为“提交”。
  1. #define TRACE_PRIVATE_LOG_EPILOG() \
  2.                                 LogScope += LogScope
  3.                                
  4. inline void FLogScope::Commit() const
  5. {
  6.         FWriteBuffer* Buffer = Instance.Buffer;
  7.         AtomicStoreRelease((uint8**) &(Buffer->Committed), Buffer->Cursor);
  8. }
复制代码
到这里,我们看完了第一个UE_TRACE_LOG执行的操作,最后会得到一个FLogScope对象,该对象关联了Buffer,以及一个FCpuProfilerEventSpecFields实例指针,名为EventSpec。
下面是有些怪异的<<操作符,它并不会真正输出什么,只是逐个初始化EventSpec的属性,内存地址就是Buffer中预留的FCpuProfilerEventSpecFields。最后的Attachment函数用于外挂一些未声明的其他属性。
一番操作后,Buffer内容如下:


不妨称一次UE_TRACE_LOG写入Buffer的内容称为一个“EventBlock”,可以认为是最小写入单元。
接下来看FlushThreadBuffer中的UE_TRACE_LOG使用
  1. UE_TRACE_LOG(CpuProfiler, EventBatch, true, InThreadBuffer->BufferSize)
  2. << EventBatch.Attachment(InThreadBuffer->Buffer, InThreadBuffer->BufferSize);
复制代码
与之前例子类似,但这里EventBatch没有成员变量,只申请了BufferSize,然后把Buffer作为attachment数据写入。
此时写入的EventBlock内容:


ThreadBuffer很小,因此要多次写入才能填满一个TlsBuffer,写入操作其实就是memorycpy了ThreadBuffer的内存。


Uid
注意到每个Block开头都是Uid,它是每个FXXXFields布局比如FCpuProfilerEventSpecFields的标识符,一个FXXXFields由LoggerName和EventName独一确定。在使用UE_TRACE_EVENT_BEGIN创建FXXXFields时,会同步创建一个对应的FEventNode对象,它会以单调递增方式创建新的Uid,而且内部记录了LoggerName和EventName。所有创建的FEventNode城市添加到GNewEventList链表中进行打点,类似GNewChannelList,这个链表只会存储新建EventNode,当EventNode信息被WriterBuffer发送之后,GNewEventList会被清空。
FWriteBuffer
前面介绍了UnrealInsight写入TlsBuffer数据的布局,那TlsBuffer有多大?写满了怎么办?因此这里看下UE如何打点TlsBuffer。
TlsBuffer全称为GTlsWriteBuffer,类型是FWriteBuffer,通过thread_local描述符,声明为每个线程独有。每个FWriteBuffer的BufferSize,由GPollBlockSize指定,默认4KB,但不都能存储数据,这块内存头部要存储FWriteBuffer本身,然后要再空一个uint32,还不确定用途。UE把这个Buffer称为Block。然后Block由更大的Page进行打点,类型为FPoolPage,申请内存的单元是Page。Page大小可以分歧,初始化的Page包含64个Block,半途扩容的Page包含16个Block,这点很像UE的mallocbinned内存分配器了。不外不雅察看426引擎代码,发现初始Page大小并没有被使用过,统一用到扩容Page大小。
数据布局
FWriteBuffer
uint16Size 可用空间
FWriteBuffer* NextBuffer 指向的下个FWriteBuffer(Block)节点
Uint8* Cursor 存储数据的游标
Uint8* Commited 提交数据的游标
uint16 ThreadId 对应线程id

FPoolPage
FPoolPage* NextPage 指向的下个FPoolPage节点
Uint32 AllocSize Page大小
Block和Page的布局图如下:


有两点值得存眷

  • Page中第一个Block头部存储了FPoolPage布局体,对比其他BlockBuffer会小一些
  • 每个Block的FWriteBuffer在内存块尾部,Cursor初始为Buffer头部,往后增长,因此判断Buffer满的条件为Cursor>FWriteBuffer
UE维护一个GPoolFreeList指针,当申请完一个Page后,会取走第一个Block去使用,然后把最后一个Block的NextBuffer指向第二个Block,再把GPoolPreeList指向第二个Block,这样当下次需要Block时,直接取GPoolFreeList即可。
回到UE_TRACE_LOG写入Buffer代码,如果此时Buffer已满,就会执行Writer_NextBufferInternal()函数,获取下一个可用Buffer,而且把当前Buffer的NextBuffer指向新获取的Buffer。
被写入数据的Buffer由另一个链表打点,称为GNewThreadList。
因此,一个“满”的Buffer可能存在空隙,比如Buffer还有10Byte空闲,我们要写入12Byte,那么该Buffer就被判定为“满”了,12byte会被写入下个Buffer。
发送Profile数据

我们已经记录了Profile数据,而且写入了Buffer,之后这些数据如何被传到处事器,或写入文件?
回到最开始的UnrealInsight初始化部门,会执行Trace::Initialize()函数,如果平台撑持多线程(一般都撑持),就启动独立的WriterThread,并以17ms为间隔进行tick。
tick执行函数为Writer_WorkerUpdate()
  1. static void Writer_WorkerUpdate()
  2. {
  3.         Writer_UpdateControl();
  4.         Writer_UpdateData();
  5.         Writer_DescribeAnnounce();
  6.         Writer_DrainBuffers();
  7. }
复制代码
前两个函数用于更新持续状态等内容,可以不管,重点看后两个函数。
Writer_DescribeAnnounce
我们每次用UE_TRACE_LOG写入Profile信息,城市用Uid开头作为标识表记标帜,Uid由Loggername(如CpuProfiler)和EventName(如Scope)确定,还可能包含成员变量,因此要把这个对应关系也发到UnrealInsight处事器,这样在收到Uid之后才知道要如何措置后面的数据。UE_TRACE_LOG会生成FEventNode对象,里面包含了Uid对应的LoggerName,EventName,以及Field描述信息,相当于反射数据,指导UnrealInsight处事器如何解析数据。
举个例子:
使用标识表记标帜TRACE_CPUPROFILER_EVENT_SCOPE(Name)来profile一个函数,首先要给这一行的标识表记标帜生成一个SpecId,并把Name和SpecId的关系通过UE_TRACE_LOG写入TlsBuffer,这个Event称为EventSpec:
  1. UE_TRACE_LOG(CpuProfiler, EventSpec, CpuChannel, NameSize)
  2.         << EventSpec.Id(SpecId)
  3.         << EventSpec.CharSize(uint8(sizeof(TCHAR)))
  4.         << EventSpec.Attachment(Name, NameSize);
复制代码
此处的Uid对应CpuProfiler和EventSpec,处事器需要知道数据中包含了成员变量SpecId和CharSize,以及Attachment包含了Name数据。
再到FlushThreadBuffer
  1. UE_TRACE_LOG(CpuProfiler, EventBatch, true, InThreadBuffer->BufferSize)
  2.         << EventBatch.Attachment(InThreadBuffer->Buffer, InThreadBuffer->BufferSize);
复制代码
这里的Uid对应CpuProfiler和EventBatch,处事器需要解析该Buffer,获取SpecId和时钟tick计数。
Writer_DescribeAnnounce函数会不竭查找新创建的EventNode和Channel,并清空GNewEventList和GNewChannelList,把Uid对应的这些反射信息,都发给处事器。
Writer_DrainBuffers
最后终于到了发送FWriteBuffer部门。
UE会遍历GNewThreadList,取出已经写入数据的Buffer,然后按照Buffer.Commited指针获取到要发送的数据,之后交给底层的IoWrite接口进行发送。IoWrite接口屏蔽了网络发送和当地写文件,上层无感知,只管写入即可。
措置完的FWriteBuffer又变成空闲可用状态,UE把它们从头插手到GPoolFreeList链表中,等待下次使用。
总体流程图


本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-9-8 11:16 , Processed in 0.149915 second(s), 27 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表