Files
UnrealEngine/Engine/Source/Editor/UnrealEd/Private/Commandlets/SummarizeTraceUtils.cpp
2025-05-18 13:04:45 +08:00

601 lines
16 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "SummarizeTraceUtils.h"
#include "Containers/Array.h"
#include "Containers/ArrayView.h"
#include "Containers/Map.h"
#include "Containers/Set.h"
#include "Containers/StringConv.h"
#include "Containers/StringFwd.h"
#include "Containers/StringView.h"
#include "GenericPlatform/GenericPlatformAffinity.h"
#include "GenericPlatform/GenericPlatformFile.h"
#include "Logging/LogCategory.h"
#include "Logging/LogMacros.h"
#include "Misc/AssertionMacros.h"
#include "Misc/CString.h"
#include "Misc/Crc.h"
#include "Misc/Optional.h"
#include "Misc/Parse.h"
#include "Misc/Timespan.h"
#include "Model/MonotonicTimeline.h"
#include "Templates/Function.h"
#include "Templates/SharedPointer.h"
#include "Templates/Tuple.h"
#include "Templates/UniquePtr.h"
#include "Templates/UnrealTemplate.h"
#include "Math/NumericLimits.h"
#include "Math/UnrealMathSSE.h"
uint64 FIncrementalVariance::GetCount() const
{
return Count;
}
double FIncrementalVariance::GetMean() const
{
return Mean;
}
double FIncrementalVariance::Variance() const
{
double Result = 0.0;
if (Count > 1)
{
// Welford's final step, dependent on sample count
Result = VarianceAccumulator / double(Count - 1);
}
return Result;
}
double FIncrementalVariance::Deviation() const
{
double Result = 0.0;
if (Count > 1)
{
// Welford's final step, dependent on sample count
double DeviationSqrd = VarianceAccumulator / double(Count - 1);
// stddev is sqrt of variance, to restore to units (vs. units squared)
Result = sqrt(DeviationSqrd);
}
return Result;
}
void FIncrementalVariance::Increment(const double InSample)
{
Count++;
const double OldMean = Mean;
Mean += ((InSample - Mean) / double(Count));
VarianceAccumulator += ((InSample - Mean) * (InSample - OldMean));
}
void FIncrementalVariance::Merge(const FIncrementalVariance& Other)
{
// empty other, nothing to do
if (Other.Count == 0)
{
return;
}
// empty this, just copy other
if (Count == 0)
{
Count = Other.Count;
Mean = Other.Mean;
VarianceAccumulator = Other.VarianceAccumulator;
return;
}
const double TotalPopulation = static_cast<double>(Count + Other.Count);
const double MeanDifference = Mean - Other.Mean;
const double A = ((Count - 1) * Variance()) + ((Other.Count - 1) * Other.Variance());
const double B = (MeanDifference) * (MeanDifference) * (Count * Other.Count / TotalPopulation);
const double MergedVariance = (A + B) / (TotalPopulation - 1);
const uint64 NewCount = Count + Other.Count;
const double NewMean = ((Mean * double(Count)) + (Other.Mean * double(Other.Count))) / double(NewCount);
const double NewVarianceAccumulator = MergedVariance * (NewCount - 1);
Count = NewCount;
Mean = NewMean;
VarianceAccumulator = NewVarianceAccumulator;
}
void FIncrementalVariance::Reset()
{
Count = 0;
Mean = 0.0;
VarianceAccumulator = 0.0;
}
void FSummarizeScope::AddDuration(double StartSeconds, double FinishSeconds)
{
// compute the duration
double DurationSeconds = FinishSeconds - StartSeconds;
DurationVariance.Increment(DurationSeconds);
// only set first for the first sample
if (DurationVariance.GetCount() == 1)
{
FirstStartSeconds = StartSeconds;
FirstFinishSeconds = FinishSeconds;
FirstDurationSeconds = DurationSeconds;
}
LastStartSeconds = StartSeconds;
LastFinishSeconds = FinishSeconds;
LastDurationSeconds = DurationSeconds;
// set duration statistics
TotalDurationSeconds += DurationSeconds;
MinDurationSeconds = FMath::Min(MinDurationSeconds, DurationSeconds);
MaxDurationSeconds = FMath::Max(MaxDurationSeconds, DurationSeconds);
BeginTimeArray.Add(StartSeconds);
EndTimeArray.Add(FinishSeconds);
}
uint64 FSummarizeScope::GetCount() const
{
return DurationVariance.GetCount();
}
double FSummarizeScope::GetMeanDurationSeconds() const
{
return DurationVariance.GetMean();
}
double FSummarizeScope::GetDeviationDurationSeconds() const
{
return DurationVariance.Deviation();
}
double FSummarizeScope::GetCountPerSecond() const
{
double CountPerSecond = 0.0;
const uint64 Count = DurationVariance.GetCount();
if (Count)
{
CountPerSecond = Count / (LastFinishSeconds - FirstStartSeconds);
}
return CountPerSecond;
}
void FSummarizeScope::Merge(const FSummarizeScope& Other)
{
check(Name == Other.Name);
DurationVariance.Merge(Other.DurationVariance);
if (FirstStartSeconds > Other.FirstStartSeconds)
{
FirstStartSeconds = Other.FirstStartSeconds;
FirstFinishSeconds = Other.FirstFinishSeconds;
FirstDurationSeconds = Other.FirstDurationSeconds;
}
if (LastStartSeconds < Other.LastStartSeconds)
{
LastStartSeconds = Other.LastStartSeconds;
LastFinishSeconds = Other.LastFinishSeconds;
LastDurationSeconds = Other.LastDurationSeconds;
}
TotalDurationSeconds += Other.TotalDurationSeconds;
MinDurationSeconds = FMath::Min(MinDurationSeconds, Other.MinDurationSeconds);
MaxDurationSeconds = FMath::Max(MaxDurationSeconds, Other.MaxDurationSeconds);
}
FString FSummarizeScope::GetValue(const FStringView& Statistic) const
{
if (Statistic == TEXT("Name"))
{
return Name;
}
else if (Statistic == TEXT("Count"))
{
return FString::Printf(TEXT("%llu"), DurationVariance.GetCount());
}
else if (Statistic == TEXT("TotalDurationSeconds"))
{
return FString::Printf(TEXT("%f"), TotalDurationSeconds);
}
else if (Statistic == TEXT("FirstStartSeconds"))
{
return FString::Printf(TEXT("%f"), FirstStartSeconds);
}
else if (Statistic == TEXT("FirstFinishSeconds"))
{
return FString::Printf(TEXT("%f"), FirstFinishSeconds);
}
else if (Statistic == TEXT("FirstDurationSeconds"))
{
return FString::Printf(TEXT("%f"), FirstDurationSeconds);
}
else if (Statistic == TEXT("LastStartSeconds"))
{
return FString::Printf(TEXT("%f"), LastStartSeconds);
}
else if (Statistic == TEXT("LastFinishSeconds"))
{
return FString::Printf(TEXT("%f"), LastFinishSeconds);
}
else if (Statistic == TEXT("LastDurationSeconds"))
{
return FString::Printf(TEXT("%f"), LastDurationSeconds);
}
else if (Statistic == TEXT("MinDurationSeconds"))
{
return FString::Printf(TEXT("%f"), MinDurationSeconds);
}
else if (Statistic == TEXT("MaxDurationSeconds"))
{
return FString::Printf(TEXT("%f"), MaxDurationSeconds);
}
else if (Statistic == TEXT("MeanDurationSeconds"))
{
return FString::Printf(TEXT("%f"), DurationVariance.GetMean());
}
else if (Statistic == TEXT("DeviationDurationSeconds"))
{
return FString::Printf(TEXT("%f"), DurationVariance.Deviation());
}
else if (Statistic == TEXT("CountPerSecond"))
{
return FString::Printf(TEXT("%f"), GetCountPerSecond());
}
return FString();
}
// for deduplication
bool FSummarizeScope::operator==(const FSummarizeScope& Scope) const
{
return Name == Scope.Name;
}
// for sorting descending
bool FSummarizeScope::operator<(const FSummarizeScope& Scope) const
{
return TotalDurationSeconds > Scope.TotalDurationSeconds;
}
FSummarizeCpuProfilerProvider::FSummarizeCpuProfilerProvider()
: LookupScopeNameFn([this](uint32 ScopeId) { return LookupScopeName(ScopeId); })
{
}
void FSummarizeCpuProfilerProvider::AddCpuScopeAnalyzer(TSharedPtr<FSummarizeCpuScopeAnalyzer> Analyzer)
{
ScopeAnalyzers.Add(MoveTemp(Analyzer));
}
void FSummarizeCpuProfilerProvider::AnalysisComplete()
{
// Analyze scope trees that contained 'nameless' context when they were captured. Unless the trace was truncated,
// all scope names should be known now.
for (const auto& Thread : Threads)
{
for (FScopeTreeInfo& DelayedScopeTree : Thread.Value->DelayedScopeTreeInfo)
{
// Run summary analysis for this delayed hierarchy.
OnCpuScopeTree(Thread.Key, DelayedScopeTree);
}
}
for (TSharedPtr<FSummarizeCpuScopeAnalyzer>& Analyzer : ScopeAnalyzers)
{
Analyzer->OnCpuScopeAnalysisEnd();
}
}
void FSummarizeCpuProfilerProvider::AddThread(uint32 Id, const TCHAR* Name, EThreadPriority Priority)
{
TUniquePtr<FThread>* Found = Threads.Find(Id);
if (!Found)
{
Threads.Add(Id, MakeUnique<FThread>(Id, this));
}
}
uint32 FSummarizeCpuProfilerProvider::AddCpuTimer(FStringView Name, const TCHAR* File, uint32 Line)
{
TOptional<FString> ScopeName;
if (!Name.IsEmpty())
{
ScopeName.Emplace(FString(Name));
}
uint32 TimerId = ScopeNames.Add(ScopeName);
// Notify the analyzers.
for (TSharedPtr<FSummarizeCpuScopeAnalyzer>& Analyzer : ScopeAnalyzers)
{
Analyzer->OnCpuScopeDiscovered(TimerId);
if (!Name.IsEmpty())
{
Analyzer->OnCpuScopeName(TimerId, Name);
}
}
return TimerId;
}
void FSummarizeCpuProfilerProvider::SetTimerName(uint32 TimerId, FStringView Name)
{
check(TimerId < uint32(ScopeNames.Num()));
check(!Name.IsEmpty());
ScopeNames[TimerId].Emplace(FString(Name));
// Notify the registered scope analyzers.
for (TSharedPtr<FSummarizeCpuScopeAnalyzer>& Analyzer : ScopeAnalyzers)
{
Analyzer->OnCpuScopeName(TimerId, Name);
}
}
void FSummarizeCpuProfilerProvider::SetTimerNameAndLocation(uint32 TimerId, FStringView Name, const TCHAR* File, uint32 Line)
{
SetTimerName(TimerId, Name);
}
uint32 FSummarizeCpuProfilerProvider::AddMetadata(uint32 MasterTimerId, TArray<uint8>&& Metadata)
{
uint32 MetadataId = Metadatas.Num();
Metadatas.Add({ MoveTemp(Metadata), MasterTimerId });
return ~MetadataId;
}
TArrayView<uint8> FSummarizeCpuProfilerProvider::GetEditableMetadata(uint32 TimerId)
{
if (int32(TimerId) >= 0)
{
return TArrayView<uint8>();
}
TimerId = ~TimerId;
if (TimerId >= uint32(Metadatas.Num()))
{
return TArrayView<uint8>();
}
FMetadata& Metadata = Metadatas[TimerId];
return Metadata.Payload;
}
TraceServices::IEditableTimeline<TraceServices::FTimingProfilerEvent>& FSummarizeCpuProfilerProvider::GetCpuThreadEditableTimeline(uint32 ThreadId)
{
TUniquePtr<FThread>* Found = Threads.Find(ThreadId);
if (Found)
{
return *(Found->Get());
}
return *Threads.Add(ThreadId, MakeUnique<FThread>(ThreadId, this));
}
void FSummarizeCpuProfilerProvider::FThread::AppendBeginEvent(double StartTime, const TraceServices::FTimingProfilerEvent& Event)
{
FScopeEnter ScopeEnter{ Event.TimerIndex, StartTime };
ScopeStack.Add(ScopeEnter);
FSummarizeCpuScopeAnalyzer::FScopeEvent ScopeEvent{ FSummarizeCpuScopeAnalyzer::EScopeEventType::Enter, Event.TimerIndex, ThreadId, StartTime };
ScopeTreeInfo.ScopeEvents.Add(ScopeEvent);
Provider->AppendBeginEvent(ScopeEvent);
}
void FSummarizeCpuProfilerProvider::FThread::AppendEndEvent(double EndTime)
{
if (ScopeStack.IsEmpty())
{
return;
}
FScopeEnter ScopeEnter = ScopeStack.Pop();
FSummarizeCpuScopeAnalyzer::FScopeEvent ScopeEvent{ FSummarizeCpuScopeAnalyzer::EScopeEventType::Exit, ScopeEnter.ScopeId, ThreadId, EndTime };
ScopeTreeInfo.ScopeEvents.Add(ScopeEvent);
// Check if at this point if the scope has a name
const FString* ScopeName = Provider->LookupScopeName(ScopeEnter.ScopeId);
ScopeTreeInfo.bHasNamelessScopes |= ScopeName == nullptr || ScopeName->IsEmpty();
FSummarizeCpuScopeAnalyzer::FScope Scope{ ScopeEnter.ScopeId, ThreadId, ScopeEnter.Timestamp, EndTime };
Provider->AppendEndEvent(Scope, ScopeName);
// The root scope on this thread just popped out.
if (ScopeStack.IsEmpty())
{
if (ScopeTreeInfo.bHasNamelessScopes)
{
// Delay the analysis until all the scope names are known.
DelayedScopeTreeInfo.Add(MoveTemp(ScopeTreeInfo));
}
else
{
// Run analysis for this scope tree.
Provider->OnCpuScopeTree(ThreadId, ScopeTreeInfo);
}
ScopeTreeInfo.Reset();
}
}
void FSummarizeCpuProfilerProvider::AppendBeginEvent(const FSummarizeCpuScopeAnalyzer::FScopeEvent& ScopeEvent)
{
// Notify the registered scope analyzers.
for (TSharedPtr<FSummarizeCpuScopeAnalyzer>& Analyzer : ScopeAnalyzers)
{
Analyzer->OnCpuScopeEnter(ScopeEvent, LookupScopeName(ScopeEvent.ScopeId));
}
}
void FSummarizeCpuProfilerProvider::AppendEndEvent(const FSummarizeCpuScopeAnalyzer::FScope& Scope, const FString* ScopeName)
{
// Notify the registered scope analyzers.
for (TSharedPtr<FSummarizeCpuScopeAnalyzer>& Analyzer : ScopeAnalyzers)
{
Analyzer->OnCpuScopeExit(Scope, ScopeName);
}
}
void FSummarizeCpuProfilerProvider::OnCpuScopeTree(uint32 ThreadId, const FScopeTreeInfo& ScopeTreeInfo)
{
// Notify the registered scope analyzers.
for (TSharedPtr<FSummarizeCpuScopeAnalyzer>& Analyzer : ScopeAnalyzers)
{
Analyzer->OnCpuScopeTree(ThreadId, ScopeTreeInfo.ScopeEvents, LookupScopeNameFn);
}
}
const FString* FSummarizeCpuProfilerProvider::LookupScopeName(uint32 ScopeId)
{
if (int32(ScopeId) < 0)
{
ScopeId = Metadatas[~ScopeId].TimerId;
}
if (ScopeId < static_cast<uint32>(ScopeNames.Num()) && ScopeNames[ScopeId])
{
return &ScopeNames[ScopeId].GetValue();
}
return nullptr;
}
FSummarizeCpuScopeDurationAnalyzer::FSummarizeCpuScopeDurationAnalyzer(TFunction<void(const TArray<FSummarizeScope>&)> InPublishFn)
: PublishFn(MoveTemp(InPublishFn))
{
OnCpuScopeDiscovered(FSummarizeCpuScopeAnalyzer::CoroutineSpecId);
OnCpuScopeDiscovered(FSummarizeCpuScopeAnalyzer::CoroutineUnknownSpecId);
OnCpuScopeName(FSummarizeCpuScopeAnalyzer::CoroutineSpecId, TEXT("Coroutine"));
OnCpuScopeName(FSummarizeCpuScopeAnalyzer::CoroutineUnknownSpecId, TEXT("<unknown>"));
}
void FSummarizeCpuScopeDurationAnalyzer::OnCpuScopeDiscovered(uint32 ScopeId)
{
if (!Scopes.Find(ScopeId))
{
Scopes.Add(ScopeId, FSummarizeScope());
}
}
void FSummarizeCpuScopeDurationAnalyzer::OnCpuScopeName(uint32 ScopeId, const FStringView& ScopeName)
{
Scopes[ScopeId].Name = ScopeName;
}
void FSummarizeCpuScopeDurationAnalyzer::OnCpuScopeExit(const FScope& Scope, const FString* ScopeName)
{
// This can miss if we are given a metadata timer's index, which is negative signed
FSummarizeScope* Found = Scopes.Find(Scope.ScopeId);
if (Found)
{
Found->AddDuration(Scope.EnterTimestamp, Scope.ExitTimestamp);
}
}
void FSummarizeCpuScopeDurationAnalyzer::OnCpuScopeAnalysisEnd()
{
TArray<FSummarizeScope> LocalScopes;
// Eliminates scopes that don't have a name. (On scope discovery, the array is expended to creates blank scopes that may never be filled).
for (TMap<uint32, FSummarizeScope>::TIterator Iter = Scopes.CreateIterator(); Iter; ++Iter)
{
if (!Iter.Value().Name.IsEmpty())
{
LocalScopes.Add(Iter.Value());
}
}
// Publish the scopes.
PublishFn(LocalScopes);
}
FString FSummarizeBookmark::GetValue(const FStringView& Statistic) const
{
if (Statistic == TEXT("Name"))
{
return Name;
}
else if (Statistic == TEXT("Count"))
{
return FString::Printf(TEXT("%llu"), Count);
}
else if (Statistic == TEXT("FirstSeconds"))
{
return FString::Printf(TEXT("%f"), FirstSeconds);
}
else if (Statistic == TEXT("LastSeconds"))
{
return FString::Printf(TEXT("%f"), LastSeconds);
}
return FString();
}
void FSummarizeBookmark::AddTimestamp(double Seconds)
{
Count += 1;
// only set first for the first sample, compare exact zero
if (FirstSeconds == 0.0)
{
FirstSeconds = Seconds;
}
LastSeconds = Seconds;
}
bool CsvUtils::IsCsvSafeString(const FString& String)
{
static struct DisallowedCharacter
{
const TCHAR Character;
bool First;
}
DisallowedCharacters[] =
{
// breaks simple csv files
{ TEXT('\n'), true },
{ TEXT('\r'), true },
{ TEXT(','), true },
};
// sanitize strings for a bog-simple csv file
bool bDisallowed = false;
int32 Index = 0;
for (struct DisallowedCharacter& DisallowedCharacter : DisallowedCharacters)
{
if (String.FindChar(DisallowedCharacter.Character, Index))
{
if (DisallowedCharacter.First)
{
UE_LOG(LogSummarizeTrace, Display, TEXT("A string contains disallowed character '%c'. See log for full list."), DisallowedCharacter.Character);
DisallowedCharacter.First = false;
}
UE_LOG(LogSummarizeTrace, Verbose, TEXT("String '%s' contains disallowed character '%c', skipping..."), *String, DisallowedCharacter.Character); bDisallowed = true;
}
if (bDisallowed)
{
break;
}
}
return !bDisallowed;
}
void CsvUtils::WriteAsUTF8String(IFileHandle* Handle, const FString& String)
{
const auto& UTF8String = StringCast<ANSICHAR>(*String);
Handle->Write(reinterpret_cast<const uint8*>(UTF8String.Get()), UTF8String.Length());
}