Trace

Trace是一种结构化的日志记录框架,用于跟踪正在运行的Unreal Insights流程中的仪表测量事件。

Choose your operating system:

Windows

macOS

Linux

Trace是一种结构化的日志记录框架,用于跟踪正在运行的流程中的仪表测量事件。此框架旨在生成高频跟踪事件的流,此类事件自我描述,可以轻松使用和共享。模块TraceLog和TraceAnalysis是构成该框架的基本模块。Unreal Insights是Trace的主要用户。

Trace简介

事件

整个系统由多个元素组成,这些元素形成了从从运行时的发射到后续在分析期间的使用一条管线。系统的第一部分跟踪事件,定义如下:

UE_TRACE_EVENT_BEGIN(LoggerName, EventName[, Flags])
    UE_TRACE_EVENT_FIELD(Type, FieldName)
UE_TRACE_EVENT_END()

EventName FieldName 参数应该能够自我解释。事件按照"记录器"进行分组,是一种仅限于语义的概念,在分析跟踪流时有助于命名空间事件和简化订阅。可选的 Flags 可以修改事件的跟踪方式。

事件标记

说明

NoSync

默认情况下,事件与其他线程上要跟踪的事件同步。标记为"NoSync"的事件将跳过此同步;它们更小,可以更快地跟踪,但代价是在分析期间与其他线程不协调。

字段类型可以是标准的整数或浮点基元(例如unit32、浮点等),以及数组和字符串(请参见下文以了解后两者的更多信息)。不支持将嵌套结构/事件作为字段。

事件通常在.cpp文件中进行全局范围的定义。如果需要从多个平移单元跟踪事件,则可以使用 UE_TRACE_EVENT_BEGIN_[EXTERN|DEFINE] 对。

运行时的事件日志记录方式如下:

UE_TRACE_LOG(RainbowLogger, ZippyEvent, ItvChannel)
    << ZippyEvent.Field0(Value0)
    << ZippyEvent.Field1(BundleValue)
    << ZippyEvent.Field2(Data, Num);
    << ZippyEvent.Field3(String[, Num]);

只要启用了通道"ItvChannel",就会向跟踪流添加"RainbowLogger.ZippyEvent"事件。将值分配到字段时需要注意避免产生任何副作用,尤其是用于跟踪额外事件的字段,因为日志站点无法重新进入。

跟踪事件时不会发生增量压缩或运行长度压缩。执行特定的 UE_TRACE_LOG() 调用时,所有已定义的字段即使并未写入,也仍会显示。。跟踪的事件基本上类似于 #pragma pack(1) -声明的结构体。明智的做法是,从战略角度思考如何充分利用这些位。

使用宏加速的背后有两重考虑;若已知偏移量等细节,则编译时可以隐藏大量样板文件;其次,在上述情况下,trace关闭时,定义和日志站点编译为空,不会到处散落 #if/end

通道

为了将事件流限制为用户感兴趣的内容,由此产生了通道的概念。这有助于关注需要重点观察的CPU和内存资源。可以按照以下方式轻松定义通道:

UE_TRACE_CHANNEL(ItvChannel);

对于更具体的用例,可以使用 EXTERN/DEFINE 对。默认情况下,通道将会被禁用并且必须通过 -trace=itv 命令行选项或 Trace::ToggleChannel() API明确启用。

通道将覆盖按位OR运算符,以便它们与日志站点一同控制使用多个通道跟踪事件。如果"Itv"和"Bbc"通道均启用, UE_TRACE_LOG(..., ItvChannel|BbcChannel) 将仅发射一个事件。思路敏捷的读者可能会感觉有些疑惑,这与按位OR的预期结果相反。

尽管其外观与位数运算符OR类似,但它的行为类似于AND逻辑运算。

分析

现在我们已经定义了新事件,再加上启用了切断的通道,添加了一个或两个日志站点,我们将能够使用和分析事件。这是使用TraceAnalysis模块执行的。

分析器(Analyzers)派生自 IAnalyzer 接口,主要实现了两个函数:

  • 用于订阅事件的 OnAnalysisBegin

  • 用于接收订阅的 OnEvent

分析时如果遇到分析器已经订阅的事件,将会调用分析器的 OnEvent() 方法。对事件数据的访问是通过 FEventContext.EventData.Get*() 方法完成的。此API反映跟踪流的自我描述性质 - 解释跟踪流时并不依赖二进制或运行时代码。每个接收的事件都具有关联的线程信息,以及(某些非常罕见的情况下)一些计时信息。

在跟踪会话上运行分析是直接通过描述要使用的分析器和向创建的分析会话提供跟踪数据来进行的。

struct : IInDataStream { ... } TraceData;
FGeorgeAnalyzer GeorgeAnalyzer;
FAnalysisContext Context;
Context.AddAnalyzer(GeorgeAnalyzer);
FAnalysisProcessor Processor = Context.Process(TraceData)
Processor.Wait();

虽然系统ID可用,但支配线程ID与系统线程ID不太相同。这是为了更好地防止在操作系统可能重用线程ID的情况下进行处理。因此系统ID预期会从一个线程重用到下一个。跟踪的ID则不会被重复使用,或者至少被认为不太可能出现这种问题。

录制器/存储

TraceAnalysis 模块包括两个组件,这两个组件共同构成了接收、存储和检索跟踪数据的服务。该实现使用异步 IO,因此它可以轻松地扩展到许多客户端(主要是入站跟踪数据)。启动并运行一个存储是很直接的。

FStoreService::FDesc Desc = {
    StoreDir,     /* directory path on disk */
    RecorderPort, /* -1=off, 0=auto-assign */
    1,            /* Number of IOCP threads */
};
auto Store = TUniquePtr<FStoreService>(FStoreService::Create(Desc));

记录器在TCP端口上监听传入的跟踪(trace)。当某个运行时与记录器建立连接时,记录器与存储创建一个新的跟踪,并开始将接收到的数据中继到这个新的跟踪中。默认使用1980端口,但也可以要求操作系统分配一个暂时端口。

存储器(Store)的主要作用是管理跟踪的目录。它允许创建新的跟踪,列举可用的跟踪,以及检索一个特定的跟踪数据。在上面构建了一个类似于REST的协议,用于与存储进行通信。该协议基于CBOR的互操作性。`FStoreClient`是这个协议的一个实现。

const auto* Store = GetWellKnownTraceStoreServer();
if (auto Client = FStoreClient::Connect(Store->Ip, Store->Port))
{
    for (int i = 0, n = Client.GetTraceCount(); i < n; ++i)
    {
        const auto* Info = Client.GetTraceInfo(i);
        /* ... */
    }
}

记录器存储组件是可选的。你可以要求运行时跟踪到某个文件。或者根据情况,用Python写一个类似记录器的脚本,通过套接字(socket)来接收跟踪数据,这样会更方便。

运行时细节

普通事件

当某个事件在 UE_TRACE_LOG() 站点被跟踪到时,一个小型标头以及事件的字段值会被写入当前线程本地的一个缓冲区。这些 TLS 缓冲区大小适中,并以List形式连接在一起。Trace 的工作线程会遍历缓冲区list,发送已经提交的事件数据(因此是完整的、完全可见的)。

使用TLS的好处是,它可以让跟踪线程不受彼此的影响(几乎是这样)。线程间的顺序对于某些事件类型具有重要意义,必须在分析跟踪数据时重建;例如分配和空闲事件,其中的地址可以得到复用。Trace使用一个以原子形式增量的整数进行同步;在每个事件之前使用一个24位的序列号。开发者可以在设置 event 时使用 NoSync 来选择不使用该功能;这样能避免产生相关的性能开销,但在分析过程中失去了与其他线程协调的能力。理想情况下,所有的事件类型都应该选择不使用同步功能。

工作线程

工作线程用于从TSL和共享缓冲方案中收集跟踪的事件,压缩和缓存重要事件,处理命令控制通信,以及管理目标IO(套接字或文件)。此线程以固定的间隔运行,是自由线程(例如,不是作为引擎循环的一部分进行启动的作业或任务)。

在某些情况下,可能不适合由工作线程使用和调度跟踪事件(在Linux上为叉取)。可以在初始化期间选择退出 bUseWorkerThread ,这将默认为 FPlatformProcess::SupportsMultithreading() 返回的值。禁用后,用户负责更新跟踪,以便通过 Trace::Update() 使用事件。请注意,低频更新将导致使用更多内存来缓冲跟踪的事件。默认情况下,这全部是由Core的TraceAuxiliary来执行的。

传输

跟踪数据以数据包(packet)的形式发送,称为 数据传输(Transport) 。每个数据包都有一个内部标识符,表明事件来自哪个线程,以及一个大小数据。数据包采用LZ4压缩,除非小到没有必要压缩。

这些数据包可以由运行时发送到两个地方:用 -tracefile= 参数发送到文件,或用 Trace::WriteTo() 程序化地发送到文件,或通过TCP套接字( -tracehost= / Trace::SendTo() )发送。默认的TCP端口是1980。

在跟踪流的开头,还有一小段握手数据和一段非结构化的元数据,它们会一起发送。

命令控制/延迟连接

在某些情况下,类似Unreal Insights这样的工具可能会希望控制发送数据的跟踪系统。为此,运行时会运行一个基本的命令/控制服务器,它主要镜像了 Trace:: 公共API。为此,运行时运行了一个基本的命令/控制服务器,它主要反映了`Trace::`公共API。TraceAnalysis中有一个简单类来协助通信。

FControlClient ControlClient;
if (ControlClient.Connect(Bungle.GetHostName())
{
    ControlClient.SendToggleChannel(L"ItvChannel", true);
    ControlClient.SendWriteTo(L"X:/Rainbow.utrace");
}

这个简易服务器监听端口是1985(那是影视经典《回到未来》上映的年份)。注意,在默认情况下,端口会不断变化(hop about)。端口号嵌入在流送数据的元数据中,该元数据位于握手数据后面(通过TraceAnalysis的Recorder访问)。

由于端口是一个比较常见的值(至少在默认情况下), 所以可以延迟连接(late-connect)到正在运行的产品,并告诉它在某个地方开始跟踪数据。这就是所谓的延迟连接(late-connect)。

杂项

数组

你可以在Trace事件中添加变量长度字段,方法是将它们定义为未指定大小的数组。

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)

限制

跟踪系统全局运行,每次只能跟踪一件事情。由于编辑器中的部分工具现在使用跟踪系统,编辑器不再自动连接和跟踪到正在运行的Unreal Insights。如果你希望分析编辑器,则在启动编辑器时必须提供 -tracehost=127.0.0.1 参数(或者根据项目的插件,在编辑器中提供 trace.start )。

虚幻引擎4说明

部分跟踪的事件是一次性的,它们仅能跟踪一次。向名称分配ID是常见情况,例如CPU事件的名称。遗憾的是,由于一次性跟踪事件中的上下文信息将会丢失,这就导致无法停止跟踪并会在随后重启该跟踪。此场景的常见特征是Unreal Insights中的"未知"CPU事件或线程。 trace.stop 实际上不是真的停止,而是为了缓解这种限制而禁用通道。未来的引擎版本将采用"重要事件"概念。一次性事件将会放入缓存并在建立跟踪连接之后重新发送,因此可以无缝支持掉入/掉出分析工作流。

欢迎帮助改进虚幻引擎文档!请告诉我们该如何更好地为您服务。
填写问卷调查
取消