Trace

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

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()

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

事件标记

说明

NoSync

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

Important

某些事件仅发送一次,需要解释后续的跟踪事件(例如将名称映射到标识的整数)。此类事件标记为"important",以便可以保留并在稍后发送它们。

此标记用于虚幻引擎5。

字段类型可以是标准的整数或浮点基元(例如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的预期结果相反。

分析

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

分析器是通过从 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,            /* No. 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位的序列号。当然这是有代价的,所以可以选择不用。理想情况下,所有的事件类型都应该选择不采用同步(但也许不会以读取TSC为代价!)。

重要事件

在UE5中又引入了一类名为 "重要(Important)" 的事件类型。这种类型的事件是针对那些只被跟踪一次,但(通常)需要解释其他后续跟踪数据的事件。它们需要用到不同的处理方式,因为这些事件很容易被遗漏(例如,断开连接然后重新连接到运行时的跟踪数据。重要事件需要在两个跟踪数据流中,但只在第一个数据流中被跟踪)。

重要类型的事件会被写入所有线程共享的缓冲区。这里有两个原因:首先,这样能将它们与普通事件分开,从而避免因为解释和多路分解(demultiplex)跟踪数据而产生的复杂性。其次,这应该有助于降低内存使用,因为追踪的重要事件的总数应该是有限的。

Trace的工作线程会轮询共享缓冲区中已提交的数据,并将其复制到储备缓冲区中,进行临时存储。随着重要事件库的不断扩充,数据以LZ4形式压缩并存储在缓存中。每次对跟踪数据进行新的连接时,都会发送这个缓存,以便确保重要事件不会丢失,并且位于数据流的开头位置。

注意,重要事件在分析过程中没有相关的线程信息,因为所有与线程相关的上下文信息早已消失。

工作线程

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

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

传输

跟踪数据以数据包(packet)的形式发送,称为 "数据传输(Transport)"。每个数据包都有一个内部标识符作为前缀,表明事件来自哪个线程(如果是重要事件,则为零),以及一个大小数据。大多数数据包都是LZ4压缩的,那些不是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)。

杂项

数组

附件

早期的跟踪不支持可变长度的字段。此后引入了附件的概念,可以将不透明的二进制块附加到跟踪事件后面。不推荐使用附件,而是应该使用数组类型的字段,此类字段由于结构化而具有优势,在分析时能够反映出来。此外,附件支持还会产生成本,每个记录的事件都需要占用资源,因此在将来有可能变成选择性加入的组件,以优化此类支出。

字符串

跟踪事件在声明带有 UE_TRACE_EVENT_FIELD() 的事件的字段时使用 Trace::AnsiStringTrace::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 实际上不是真的停止,而是为了缓解这种限制而禁用信道。未来的引擎版本将采用"重要事件"概念。一次性事件将会放入缓存并在建立跟踪连接之后重新发送,因此可以无缝支持掉入/掉出分析工作流。

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

欢迎来到全新虚幻引擎4文档站!

我们正在努力开发新功能,包括反馈系统,以便您能对我们的工作作出评价。但它目前还未正式上线。如果您对此页面有任何意见与在使用中遭遇任何问题,请前往文档反馈论坛告知我们。

新系统上线运行后,我们会及时通知您的。

发表反馈意见