EM_Task/TraceInsights/Private/Insights/ViewModels/MinimalTimerExporter.cpp

547 lines
17 KiB
C++
Raw Normal View History

2026-02-13 16:18:33 +08:00
#include "Insights/ViewModels/MinimalTimerExporter.h"
#include "TraceServices/Model/Threads.h"
#include "Misc/Paths.h"
#include "CborReader.h"
namespace TraceTimer
{
ANSICHAR Term = '\n';
FORCEINLINE FArchive& operator<<(FArchive& Ar, const ANSICHAR* Str)
{
if (Str)
{
Ar.Serialize((void*)Str, FCStringAnsi::Strlen(Str));
}
return Ar;
}
FORCEINLINE FArchive& operator<<(FArchive& Ar, const TCHAR* Str)
{
if (Str)
{
FTCHARToUTF8 Converter(Str);
Ar.Serialize((void*)Converter.Get(), Converter.Length());
}
return Ar;
}
FORCEINLINE FArchive& operator<<(FArchive& Ar, uint32 Value)
{
ANSICHAR Buffer[16];
int32 Len = FCStringAnsi::Snprintf(Buffer, 16, "%u", Value);
Ar.Serialize(Buffer, Len);
return Ar;
}
FORCEINLINE FArchive& operator<<(FArchive& Ar, double Value)
{
ANSICHAR Buffer[32];
int32 Len = FCStringAnsi::Snprintf(Buffer, 32, "%.9f", Value);
Ar.Serialize(Buffer, Len);
return Ar;
}
// --- FExportContext 实现 ---
FExportContext::FExportContext(const Trace::IAnalysisSession& InSession): Session(InSession) {}
bool FExportContext::Initialize(const Trace::ITimingProfilerProvider* InProvider)
{
if (!InProvider)
return false;
Provider = InProvider;
Trace::FAnalysisSessionReadScope _(Session);
Provider->ReadTimers([this](const Trace::ITimingProfilerTimerReader& Out)
{
TimerReader = &Out;
});
const Trace::IThreadProvider& ThreadProvider = Trace::ReadThreadProvider(Session);
ThreadProvider.EnumerateThreads([this](const Trace::FThreadInfo& Thread)
{
uint32 TimelineIndex = 0;
if (Provider->GetCpuThreadTimelineIndex(Thread.Id, TimelineIndex))
TimelineIndexToThreadId.Add(TimelineIndex, Thread.Id);
});
uint32 GpuIndex = 0;
if (Provider->GetGpuTimelineIndex(GpuIndex))
TimelineIndexToThreadId.Add(GpuIndex, 0xFFFFFFFF);
if (Provider->GetGpu2TimelineIndex(GpuIndex))
TimelineIndexToThreadId.Add(GpuIndex, 0xFFFFFFFE);
return TimerReader != nullptr;
}
uint32 FExportContext::GetThreadId(uint32 TimelineIndex) const
{
const uint32* Found = TimelineIndexToThreadId.Find(TimelineIndex);
return Found ? *Found : 0;
}
void CsvEventWriter::WriteHeader(FArchive& Ar)
{
if (EnumHasAnyFlags(Fields, EExportField::ThreadId)) Ar << TEXT("ThreadId,");
if (EnumHasAnyFlags(Fields, EExportField::Name)) Ar << TEXT("Name,");
if (EnumHasAnyFlags(Fields, EExportField::Start)) Ar << TEXT("Start,");
if (EnumHasAnyFlags(Fields, EExportField::End)) Ar << TEXT("End,");
if (EnumHasAnyFlags(Fields, EExportField::Duration)) Ar << TEXT("Duration,");
if (EnumHasAnyFlags(Fields, EExportField::Depth)) Ar << TEXT("Depth");
if (EnumHasAnyFlags(Fields, EExportField::Metadata)) Ar << TEXT("Metadata");
Ar << Term;
}
void CsvEventWriter::ProcessEvent(FArchive& Ar, const FEventExportData& Data)
{
// 技巧:定义一个控制变量处理逗号,虽然多一行,但能保证 CSV 格式完美
bool bNeedComma = false;
auto WriteDelimiter = [&]() { if (bNeedComma) Ar << TEXT(","); bNeedComma = true; };
if (EnumHasAnyFlags(Fields, EExportField::ThreadId))
{
WriteDelimiter();
Ar << Data.ThreadId;
}
if (EnumHasAnyFlags(Fields, EExportField::Name))
{
WriteDelimiter();
// 链式写入引号和内容,简洁明了
Ar << TEXT("\"") << (Data.Timer ? Data.Timer->Name : TEXT("Unknown")) << TEXT("\"");
}
// 浮点数部分直接内联转换,不给变量起名
if (EnumHasAnyFlags(Fields, EExportField::Start))
{
WriteDelimiter();
Ar << *FString::Printf(TEXT("%.9f"), Data.Start);
}
if (EnumHasAnyFlags(Fields, EExportField::End))
{
WriteDelimiter();
Ar << *FString::Printf(TEXT("%.9f"), Data.End);
}
if (EnumHasAnyFlags(Fields, EExportField::Duration))
{
WriteDelimiter();
Ar << *FString::Printf(TEXT("%.9f"), Data.End - Data.Start);
}
if (EnumHasAnyFlags(Fields, EExportField::Depth))
{
WriteDelimiter();
Ar << Data.Depth;
}
if (EnumHasAnyFlags(Fields, EExportField::Metadata))
{
WriteDelimiter();
FString OutMetadata = TEXT("");
FTraceExportProcessor::DecodeMetadataToString(OutMetadata, Data.Metadata);
Ar << *OutMetadata;
}
Ar << Term;
}
void JsonEventWriter::ProcessEvent(FArchive& Ar, const FEventExportData& Data)
{
WriteNode(Ar, Data);
Ar << Term;
}
void JsonEventWriter::WriteNode(FArchive& Ar, const FEventExportData& Node)
{
Ar << TEXT("{");
bool bNeedComma = false;
auto WriteKey = [&](const TCHAR* Key) {
if (bNeedComma) Ar << TEXT(",");
Ar << TEXT("\"") << Key << TEXT("\":");
bNeedComma = true;
};
if (EnumHasAnyFlags(Fields, EExportField::Name))
{
WriteKey(TEXT("name"));
Ar << TEXT("\"") << (Node.Timer ? Node.Timer->Name : TEXT("Unknown")) << TEXT("\"");
}
if (EnumHasAnyFlags(Fields, EExportField::Start))
{
WriteKey(TEXT("start"));
Ar << *FString::Printf(TEXT("%.9f"), Node.Start);
}
if (EnumHasAnyFlags(Fields, EExportField::End))
{
WriteKey(TEXT("end"));
Ar << *FString::Printf(TEXT("%.9f"), Node.End);
}
if (EnumHasAnyFlags(Fields, EExportField::Duration))
{
WriteKey(TEXT("duration"));
Ar << *FString::Printf(TEXT("%.9f"), Node.End - Node.Start);
}
if (EnumHasAnyFlags(Fields, EExportField::Depth))
{
WriteKey(TEXT("depth"));
Ar << Node.Depth;
}
if (EnumHasAnyFlags(Fields, EExportField::ThreadId))
{
WriteKey(TEXT("threadId"));
Ar << Node.ThreadId;
}
if (EnumHasAnyFlags(Fields, EExportField::Metadata) && Node.Metadata.Num() > 0)
{
WriteKey(TEXT("metadata"));
FString OutMetadata = TEXT("");
FTraceExportProcessor::DecodeMetadataToString(OutMetadata, Node.Metadata);
Ar << *OutMetadata;
}
if (Node.Children.Num() > 0)
{
WriteKey(TEXT("children"));
Ar << TEXT("[");
for (int32 i = 0; i < Node.Children.Num(); ++i)
{
if (i > 0) Ar << TEXT(",");
WriteNode(Ar, *Node.Children[i]);
}
Ar << TEXT("]");
}
Ar << TEXT("}");
}
FExportConfig::FExportConfig(const FString& Filename)
{
Folder = FPaths::GetPath(Filename);
BaseName = FPaths::GetBaseFilename(Filename);
Extension = FPaths::GetExtension(Filename);
}
void ChunkingFileWriter::Initialize(const FExportConfig& InConfig)
{
Config = InConfig;
PersistentBuffer.Reset();
MemAr = MakeUnique<FMemoryWriter>(PersistentBuffer, false);
FileAr.Reset();
ChunkIndex = 1;
WrittenEventCount = 0;
CurrentFilename.Empty();
}
FArchive* ChunkingFileWriter::GetCurrentArchive()
{
return MemAr.Get();
}
void ChunkingFileWriter::CheckFlush(IEventWriter* Writer)
{
if (PersistentBuffer.Num() >= (int32)Config.MaxMemSize)
{
FlushToDisk();
}
}
void ChunkingFileWriter::FlushToDisk()
{
if (!FileAr || PersistentBuffer.Num() == 0)
return;
FileAr->Serialize(PersistentBuffer.GetData(), PersistentBuffer.Num());
PersistentBuffer.Reset();
MemAr->Seek(0);
}
void ChunkingFileWriter::WriteEvent(IEventWriter* Writer, const FEventExportData& Data)
{
if (!Writer)
return;
Writer->ProcessEvent(*MemAr, Data);
WrittenEventCount++;
CheckFlush(Writer);
if (WrittenEventCount >= Config.EventsPerChunk)
{
FlushToDisk();
SwitchToNextChunk(Writer);
}
}
void ChunkingFileWriter::SwitchToNextChunk(IEventWriter* Writer)
{
FileAr.Reset();
if (!Writer)
return;
FString PartSuffix = ChunkIndex > 1 ? FString::Printf(TEXT("_part%u"), ChunkIndex) : TEXT("");
CurrentFilename = FPaths::Combine(Config.Folder, FString::Printf(TEXT("%s%s.%s"), *Config.BaseName, *PartSuffix, *Config.Extension));
FileAr.Reset(IFileManager::Get().CreateFileWriter(*CurrentFilename));
if (FileAr)
{
PersistentBuffer.Reset();
MemAr = MakeUnique<FMemoryWriter>(PersistentBuffer, false);
Writer->WriteHeader(*MemAr);
}
WrittenEventCount = 0;
ChunkIndex++;
}
void ChunkingFileWriter::Close()
{
FlushToDisk();
FileAr.Reset();
PersistentBuffer.Reset();
MemAr.Reset();
ChunkIndex = 1;
WrittenEventCount = 0;
CurrentFilename.Empty();
}
ChunkingFileWriter::~ChunkingFileWriter()
{
Close();
}
bool FExportFilter::ShouldFilterByEvent(FEventExportData& Node) const
{
if (bOnlyMetadata && Node.Metadata.Num())
{
return false;
}
return true;
}
FTraceExportProcessor::FTraceExportProcessor(FExportContext& InContext, IEventWriter& InWriter, ChunkingFileWriter& InChunkWriter, const FExportFilter& InFilter)
: Context(InContext), Writer(InWriter), ChunkWriter(InChunkWriter), Filter(InFilter) {}
void FTraceExportProcessor::Run()
{
Trace::FAnalysisSessionReadScope _(Context.Session);
uint32 TimelineCount = Context.Provider->GetTimelineCount();
for (uint32 i = 0; i < TimelineCount; ++i)
{
if (Filter.ShouldFilterByTrack(i))
continue;
ProcessTimeline(i);
}
}
void FTraceExportProcessor::ProcessTimeline(uint32 TimelineIndex)
{
uint32 ThreadId = Context.GetThreadId(TimelineIndex);
Context.Provider->ReadTimeline(TimelineIndex, [this, ThreadId](const Trace::ITimingProfilerProvider::Timeline& Timeline)
{
if (Writer.WantsStructuredPath())
EnumerateStructured(Timeline, ThreadId);
else
EnumerateFlat(Timeline, ThreadId);
});
}
void FTraceExportProcessor::EnumerateFlat(const Trace::ITimingProfilerProvider::Timeline& Timeline, uint32 ThreadId)
{
Timeline.EnumerateEvents(Filter.GetStartTime(), Filter.GetEndTime(), [&](double S, double E, uint32 Depth, const Trace::FTimingProfilerEvent& Event)
{
FEventExportData Data;
Data.Timer = Context.TimerReader->GetTimer(Event.TimerIndex);
Data.Start = S;
Data.End = E;
Data.Depth = Depth;
Data.ThreadId = ThreadId;
Data.Metadata = Context.TimerReader->GetMetadata(Event.TimerIndex);
if (Filter.ShouldFilterByEvent(Data))
return Trace::EEventEnumerate::Continue;
ChunkWriter.WriteEvent(&Writer, Data);
return Trace::EEventEnumerate::Continue;
});
}
void FTraceExportProcessor::EnumerateStructured(const Trace::ITimingProfilerProvider::Timeline& Timeline, uint32 ThreadId)
{
TArray<TSharedPtr<FEventExportData>> Stack;
FEventExportData StaticNode;
Timeline.EnumerateEvents(Filter.GetStartTime(), Filter.GetEndTime(), [&](double S, double E, uint32 Depth, const Trace::FTimingProfilerEvent& Event)
{
StaticNode.Timer = Context.TimerReader->GetTimer(Event.TimerIndex);
StaticNode.Start = S;
StaticNode.End = E;
StaticNode.Depth = Depth;
StaticNode.ThreadId = ThreadId;
StaticNode.Metadata = Context.TimerReader->GetMetadata(Event.TimerIndex);
if (Filter.ShouldFilterByEvent(StaticNode))
return Trace::EEventEnumerate::Continue;
ChunkWriter.NotifyEventWritten();
TSharedPtr<FEventExportData> Node = MakeShared<FEventExportData>();
StaticNode.MoveTo(*Node);
if (Depth == 0)
{
if (Stack.Num() > 0)
{
ChunkWriter.WriteEvent(&Writer, *Stack[0]);
}
Stack.Empty();
Stack.Add(Node);
}
else
{
while (Stack.Num() > (int32)Depth)
Stack.Pop();
if (Stack.Num() > 0)
Stack.Last()->Children.Add(Node);
Stack.Add(Node);
}
return Trace::EEventEnumerate::Continue;
});
if (Stack.Num() > 0)
{
ChunkWriter.WriteEvent(&Writer, *Stack[0]);
}
}
void FTraceExportProcessor::DecodeMetadataToString(FString& Str, const TArrayView<const uint8>& Metadata)
{
FMemoryReaderView MemoryReader(Metadata);
FCborReader CborReader(&MemoryReader, ECborEndianness::StandardCompliant);
FCborContext Context;
if (!CborReader.ReadNext(Context) || Context.MajorType() != ECborCode::Map)
{
return;
}
bool bFirst = true;
while (true)
{
// Read key
if (!CborReader.ReadNext(Context) || !Context.IsString())
{
break;
}
if (bFirst)
{
bFirst = false;
// Str += TEXT(" - ");
}
else
{
Str += TEXT(", ");
}
FString Key(Context.AsLength(), Context.AsCString());
Str += Key;
Str += TEXT(":");
if (!CborReader.ReadNext(Context))
{
break;
}
switch (Context.MajorType())
{
case ECborCode::Int:
case ECborCode::Uint: {
uint64 Value = Context.AsUInt();
Str += FString::Printf(TEXT("%llu"), Value);
continue;
}
case ECborCode::TextString: {
Str += Context.AsString();
continue;
}
case ECborCode::ByteString: {
Str.AppendChars(Context.AsCString(), Context.AsLength());
continue;
}
}
if (Context.RawCode() == (ECborCode::Prim | ECborCode::Value_4Bytes))
{
float Value = Context.AsFloat();
Str += FString::Printf(TEXT("%f"), Value);
continue;
}
if (Context.RawCode() == (ECborCode::Prim | ECborCode::Value_8Bytes))
{
double Value = Context.AsDouble();
Str += FString::Printf(TEXT("%g"), Value);
continue;
}
if (Context.IsFiniteContainer())
{
CborReader.SkipContainer(ECborCode::Array);
}
}
}
} // namespace TraceTimer
// --- FMinimalTimerExporter 静态入口实现 ---
bool FMinimalTimerExporter::ExportTimersToCSV(const Trace::IAnalysisSession& Session, const FString& Filename)
{
return false;
}
bool FMinimalTimerExporter::ExportMetadataToCSV(const Trace::IAnalysisSession& Session, const FString& Filename, const TraceTimer::FExportFilter& InFilter)
{
TraceTimer::FExportFilter Filter = InFilter;
Filter.bOnlyMetadata = true;
TraceTimer::CsvEventWriter Writer(Filter.Fields);
return ExecuteTimingExport(Session, Filename, Filter, Writer);
}
bool FMinimalTimerExporter::ExportTimingEventsToCSV(const Trace::IAnalysisSession& Session, const FString& Filename, const TraceTimer::FExportFilter& InFilter)
{
TraceTimer::FExportFilter Filter = InFilter;
TraceTimer::CsvEventWriter Writer(Filter.Fields);
return ExecuteTimingExport(Session, Filename, Filter, Writer);
}
bool FMinimalTimerExporter::ExportTimingEventsToJSON(const Trace::IAnalysisSession& Session, const FString& Filename, const TraceTimer::FExportFilter& InFilter)
{
TraceTimer::FExportFilter Filter = InFilter;
TraceTimer::JsonEventWriter Writer(Filter.Fields);
return ExecuteTimingExport(Session, Filename, Filter, Writer);
}
bool FMinimalTimerExporter::ExecuteTimingExport(const Trace::IAnalysisSession& Session, const FString& Filename, TraceTimer::FExportFilter& Filter, TraceTimer::IEventWriter& Writer)
{
using namespace TraceTimer;
FExportContext Context(Session);
if (!Context.Initialize(Trace::ReadTimingProfilerProvider(Session)))
return false;
FExportConfig Config(Filename);
ChunkingFileWriter ChunkWriter;
ChunkWriter.Initialize(Config);
ChunkWriter.SwitchToNextChunk(&Writer);
FTraceExportProcessor Processor(Context, Writer, ChunkWriter, Filter);
Processor.Run();
return true;
}