// Copyright Epic Games, Inc. All Rights Reserved. #include "CookProfiling.h" #include "Algo/GraphConvert.h" #include "Algo/Sort.h" #include "Algo/Unique.h" #include "Containers/Array.h" #include "Containers/BitArray.h" #include "Containers/Map.h" #include "Containers/UnrealString.h" #include "CookOnTheSide/CookLog.h" #include "CoreGlobals.h" #include "DerivedDataBuildRemoteExecutor.h" #include "HAL/FileManager.h" #include "Misc/CommandLine.h" #include "Misc/OutputDevice.h" #include "Misc/PathViews.h" #include "Misc/StringBuilder.h" #include "PackageBuildDependencyTracker.h" #include "Policies/CondensedJsonPrintPolicy.h" #include "Serialization/ArchiveUObject.h" #include "Serialization/JsonSerializer.h" #include "Serialization/JsonWriter.h" #include "Templates/Casts.h" #include "UObject/GCObject.h" #include "UObject/MetaData.h" #include "UObject/Package.h" #include "UObject/Object.h" #include "UObject/UObjectGlobals.h" #include "UObject/UObjectIterator.h" #include "UObject/WeakObjectPtr.h" #if OUTPUT_COOKTIMING || ENABLE_COOK_STATS #include "ProfilingDebugging/ScopedTimers.h" #endif #if OUTPUT_COOKTIMING #include #endif #if ENABLE_COOK_STATS #include "AnalyticsET.h" #include "AnalyticsEventAttribute.h" #include "IAnalyticsProviderET.h" #include "DerivedDataCacheInterface.h" #include "DerivedDataCacheUsageStats.h" #include "Virtualization/VirtualizationSystem.h" #include "Experimental/ZenServerInterface.h" #endif #if OUTPUT_COOKTIMING struct FHierarchicalTimerInfo { public: FHierarchicalTimerInfo(const FHierarchicalTimerInfo& InTimerInfo) = delete; FHierarchicalTimerInfo(FHierarchicalTimerInfo&& InTimerInfo) = delete; explicit FHierarchicalTimerInfo(const char* InName, uint16 InId) : Id(InId) , Name(InName) { } ~FHierarchicalTimerInfo() { ClearChildren(); } void ClearChildren() { for (FHierarchicalTimerInfo* Child = FirstChild; Child;) { FHierarchicalTimerInfo* NextChild = Child->NextSibling; DestroyAndFree(Child); Child = NextChild; } FirstChild = nullptr; } FHierarchicalTimerInfo* GetChild(uint16 InId, const char* InName) { for (FHierarchicalTimerInfo* Child = FirstChild; Child;) { if (Child->Id == InId) return Child; Child = Child->NextSibling; } FHierarchicalTimerInfo* Child = AllocNew(InName, InId); Child->NextSibling = FirstChild; FirstChild = Child; return Child; } uint32 HitCount = 0; uint16 Id = 0; bool IncrementDepth = true; double Length = 0; const char* Name; FHierarchicalTimerInfo* FirstChild = nullptr; FHierarchicalTimerInfo* NextSibling = nullptr; private: static FHierarchicalTimerInfo* AllocNew(const char* InName, uint16 InId); static void DestroyAndFree(FHierarchicalTimerInfo* InPtr); }; static TAllocatorFixedSizeFreeList TimerInfoAllocator; static FHierarchicalTimerInfo RootTimerInfo("Root", 0); static FHierarchicalTimerInfo* CurrentTimerInfo = &RootTimerInfo; FHierarchicalTimerInfo* FHierarchicalTimerInfo::AllocNew(const char* InName, uint16 InId) { return new(TimerInfoAllocator.Allocate()) FHierarchicalTimerInfo(InName, InId); } void FHierarchicalTimerInfo::DestroyAndFree(FHierarchicalTimerInfo* InPtr) { InPtr->~FHierarchicalTimerInfo(); TimerInfoAllocator.Free(InPtr); } FScopeTimer::FScopeTimer(int InId, const char* InName, bool IncrementScope /*= false*/ ) { checkSlow(IsInGameThread()); HierarchyTimerInfo = CurrentTimerInfo->GetChild(static_cast(InId), InName); HierarchyTimerInfo->IncrementDepth = IncrementScope; PrevTimerInfo = CurrentTimerInfo; CurrentTimerInfo = HierarchyTimerInfo; } void FScopeTimer::Start() { if (StartTime) { return; } StartTime = FPlatformTime::Cycles64(); } void FScopeTimer::Stop() { if (!StartTime) { return; } HierarchyTimerInfo->Length += FPlatformTime::ToSeconds64(FPlatformTime::Cycles64() - StartTime); ++HierarchyTimerInfo->HitCount; StartTime = 0; } FScopeTimer::~FScopeTimer() { Stop(); check(CurrentTimerInfo == HierarchyTimerInfo); CurrentTimerInfo = PrevTimerInfo; } void OutputHierarchyTimers(const FHierarchicalTimerInfo* TimerInfo, int32 Depth) { FString TimerName(TimerInfo->Name); static const TCHAR LeftPad[] = TEXT(" "); const SIZE_T PadOffset = FMath::Max(UE_ARRAY_COUNT(LeftPad) - 1 - Depth * 2, 0); UE_LOG(LogCookStats, Display, TEXT(" %s%s: %.3fs (%u)"), &LeftPad[PadOffset], *TimerName, TimerInfo->Length, TimerInfo->HitCount); // We need to print in reverse order since the child list begins with the most recently added child TArray Stack; for (const FHierarchicalTimerInfo* Child = TimerInfo->FirstChild; Child; Child = Child->NextSibling) { Stack.Add(Child); } const int32 ChildDepth = Depth + TimerInfo->IncrementDepth; for (size_t i = Stack.Num(); i > 0; --i) { OutputHierarchyTimers(Stack[i - 1], ChildDepth); } } void OutputHierarchyTimers() { UE_LOG(LogCookStats, Display, TEXT("Hierarchy Timer Information:")); OutputHierarchyTimers(&RootTimerInfo, 0); } void ClearHierarchyTimers() { RootTimerInfo.ClearChildren(); } #endif #if ENABLE_COOK_STATS namespace DetailedCookStats { FString CookProject; FString CookCultures; FString CookLabel; FString TargetPlatforms; double CookStartTime = 0.0; double CookWallTimeSec = 0.0; double StartupWallTimeSec = 0.0; double StartCookByTheBookTimeSec = 0.0; double TickCookOnTheSideTimeSec = 0.0; double TickCookOnTheSideLoadPackagesTimeSec = 0.0; double TickCookOnTheSideResolveRedirectorsTimeSec = 0.0; double TickCookOnTheSideSaveCookedPackageTimeSec = 0.0; double TickCookOnTheSidePrepareSaveTimeSec = 0.0; double BlockOnAssetRegistryTimeSec = 0.0; double GameCookModificationDelegateTimeSec = 0.0; double TickLoopGCTimeSec = 0.0; double TickLoopRecompileShaderRequestsTimeSec = 0.0; double TickLoopShaderProcessAsyncResultsTimeSec = 0.0; double TickLoopProcessDeferredCommandsTimeSec = 0.0; double TickLoopTickCommandletStatsTimeSec = 0.0; double TickLoopFlushRenderingCommandsTimeSec = 0.0; double ShaderFlushTimeSec = 0.0; double ValidationTimeSec = 0.0; bool IsCookAll = false; bool IsCookOnTheFly = false; bool IsIterativeCook = false; bool IsFastCook = false; bool IsUnversioned = false; // Stats tracked through FAutoRegisterCallback int32 PeakRequestQueueSize = 0; int32 PeakLoadQueueSize = 0; int32 PeakSaveQueueSize = 0; std::atomic NumDetectedLoads{ 0 }; int32 NumRequestedLoads = 0; uint32 NumPackagesIncrementallySkipped = 0; FCookStatsManager::FAutoRegisterCallback RegisterCookOnTheFlyServerStats([](FCookStatsManager::AddStatFuncRef AddStat) { AddStat(TEXT("Package.Load"), FCookStatsManager::CreateKeyValueArray( TEXT("NumRequestedLoads"), NumRequestedLoads, TEXT("NumPackagesLoaded"), NumDetectedLoads.load(), TEXT("NumInlineLoads"), NumDetectedLoads - NumRequestedLoads)); AddStat(TEXT("Package.Save"), FCookStatsManager::CreateKeyValueArray(TEXT("NumPackagesIncrementallySkipped"), NumPackagesIncrementallySkipped)); AddStat(TEXT("CookOnTheFlyServer"), FCookStatsManager::CreateKeyValueArray( TEXT("PeakRequestQueueSize"), PeakRequestQueueSize, TEXT("PeakLoadQueueSize"), PeakLoadQueueSize, TEXT("PeakSaveQueueSize"), PeakSaveQueueSize)); }); } #endif namespace UE::Cook { /** The various ways objects can be referenced that keeps them in memory. */ enum class EObjectReferencerType : uint8 { Unknown = 0, Rooted, GCObjectRef, Referenced, }; struct FObjectGraphProfileData; /** * Data for how an object is referenced in the DumpObjClassList graph search, * including the type of reference and the vertex of the referencer. */ struct FObjectReferencer { FObjectReferencer() = default; explicit FObjectReferencer(EObjectReferencerType InLinkType, Algo::Graph::FVertex InVertexArgument = Algo::Graph::InvalidVertex) { Set(InLinkType, InVertexArgument); } Algo::Graph::FVertex GetVertexArgument() const { return VertexArgument; } EObjectReferencerType GetLinkType() { return LinkType; } void Set(EObjectReferencerType InLinkType, Algo::Graph::FVertex InVertexArgument = Algo::Graph::InvalidVertex) { switch (InLinkType) { case EObjectReferencerType::GCObjectRef: check(InVertexArgument != Algo::Graph::InvalidVertex); break; case EObjectReferencerType::Referenced: check(InVertexArgument != Algo::Graph::InvalidVertex); break; default: break; } VertexArgument = InVertexArgument; LinkType = InLinkType; } void ToString(FStringBuilderBase& Builder, FObjectGraphProfileData& ProfileData); private: Algo::Graph::FVertex VertexArgument = Algo::Graph::InvalidVertex; EObjectReferencerType LinkType = EObjectReferencerType::Unknown; }; struct FObjectGraphProfileData { /** The list of UObjects found from a TObectIterator */ TArray AllObjects; /** We assign FVertex N <-> AllObjects[N]; this field records the reverse map. */ TMap VertexOfObject; /** Element N records whether AllObjects[N] is not one of InitialObjects */ TBitArray<> IsNew; /** The first reason found that AllObjects[n] is still referenced. */ TArray AliveReason; /** The first rooted vertex found that has a reference chain to AllObjects[n]. */ TArray RootOfVertex; /** The referencenames reported by FGCObject::GGCObjectReferencer for why it refers to objects. */ TArray AllGCObjectNames; /** We assign (FVertex NumObjects+N) <-> AllGCObjectNames[N]; this field records the reverse map. */ TMap GCObjectNameToVertex; /** Buffer of edges used for ObjectGraph */ TArray64 ObjectGraphBuffer; /** ObjectGraph constructed from the edges between vertices defined by serialization references between objects. */ TArray> ObjectGraph; /** Total number of vertices, both Objects and GCObjectNames */ int32 NumVertices; /** Number of object vertices. The first GCObjectName vertex starts after this number. */ int32 NumObjectVertices; /** Number of GCObjectName vertices. */ int32 NumGCObjectNameVertices; /** The vertex that is assigned to FGCObject::GGCObjectReferencer. */ Algo::Graph::FVertex GCObjectReferencerVertex; void AppendVertexName(Algo::Graph::FVertex Vertex, FStringBuilderBase& Builder) { if (Vertex < 0) { Builder << TEXT("InvalidVertex"); } else if (Vertex < NumObjectVertices) { AllObjects[Vertex]->GetPathName(nullptr, Builder); } else if (Vertex - NumObjectVertices < NumGCObjectNameVertices) { Builder << TEXT("FGCObject ") << AllGCObjectNames[Vertex - NumObjectVertices]; } else { Builder << TEXT("InvalidVertex"); } } }; void FObjectReferencer::ToString(FStringBuilderBase& Builder, FObjectGraphProfileData& ProfileData) { switch (GetLinkType()) { case EObjectReferencerType::Unknown: Builder << TEXT(""); break; case EObjectReferencerType::Rooted: Builder << TEXT(""); break; case EObjectReferencerType::GCObjectRef: { check(VertexArgument != Algo::Graph::InvalidVertex); check(ProfileData.NumObjectVertices <= VertexArgument && VertexArgument < ProfileData.NumObjectVertices + ProfileData.NumGCObjectNameVertices); ProfileData.AppendVertexName(VertexArgument, Builder); break; } case EObjectReferencerType::Referenced: { check(VertexArgument != Algo::Graph::InvalidVertex); check(VertexArgument < ProfileData.NumObjectVertices); ProfileData.AppendVertexName(VertexArgument, Builder); break; } default: checkNoEntry(); break; } } /** An ObjectReferenceCollector to pass to Object->Serialize to collect references into an array. */ class FArchiveGetReferences : public FArchiveUObject { public: FArchiveGetReferences(UObject* Object, TArray& OutReferencedObjects) :ReferencedObjects(OutReferencedObjects) { ArIsObjectReferenceCollector = true; ArIgnoreOuterRef = false; SetShouldSkipCompilingAssets(false); Object->Serialize(*this); } FArchive& operator<<(UObject*& Object) { if (Object) { ReferencedObjects.Add(Object); } return *this; } private: TArray& ReferencedObjects; }; /** * A ReferenceFinder used only when serializing FGCObject::GGCObjectReferencer. * It captures the referencerName from GGCObjectReferencer for each UObject passed to it. */ class FGCObjectReferencerFinder : public FReferenceFinder { public: FGCObjectReferencerFinder(TArray& InObjectArray, TMap& InObjectReferencerNames) : FReferenceFinder(InObjectArray) , ObjectReferencerNames(InObjectReferencerNames) { } virtual void HandleObjectReference(UObject*& InObject, const UObject* InReferencingObject, const FProperty* InReferencingProperty) override { // Avoid duplicate entries. if (InObject != NULL) { // Many places that use FReferenceFinder expect the object to not be const. UObject* Object = const_cast(InObject); // do not add or recursively serialize objects that have already been added bool bAlreadyExists; ObjectSet.Add(Object, &bAlreadyExists); if (!bAlreadyExists) { check(Object->IsValidLowLevel()); ObjectArray.Add(Object); FString ReferencerName; FGCObject::GGCObjectReferencer->GetReferencerName(Object, ReferencerName, true /* bOnlyIfAddingReferenced */); if (!ReferencerName.IsEmpty()) { ObjectReferencerNames.Add(Object, MoveTemp(ReferencerName)); } } } } private: TMap& ObjectReferencerNames; FGCObject* CurrentlySerializingObject; }; /** * Given the list of AllObjects from e.g. a TObjectIterator, use serialization and other methods from Garbage Collection * to find all the dependencies of each Object. * Return the dependencies as a normalized graph in the style of GraphConvert.h, with the vertex of each object defined * by AllObjects and ObjectToVertex. */ void ConstructObjectGraph(TConstArrayView AllObjects, const TMap& ObjectToVertex, TArray64& OutGraphBuffer, TArray>& OutGraph, TMap& OutGCObjectReferencerNames) { using namespace Algo::Graph; TArray> LooseEdges; int32 NumVertices = AllObjects.Num(); LooseEdges.SetNum(NumVertices); TArray TargetObjects; int32 NumEdges = 0; OutGCObjectReferencerNames.Reset(); for (FVertex SourceVertex = 0; SourceVertex < NumVertices; ++SourceVertex) { UObject* SourceObject = AllObjects[SourceVertex]; TargetObjects.Reset(); { if (SourceObject == FGCObject::GGCObjectReferencer) { FGCObjectReferencerFinder Collector(TargetObjects, OutGCObjectReferencerNames); UGCObjectReferencer::AddReferencedObjects(FGCObject::GGCObjectReferencer, Collector); } else { FReferenceFinder Collector(TargetObjects); FArchiveGetReferences Ar(SourceObject, TargetObjects); if (SourceObject->GetClass()) { SourceObject->GetClass()->CallAddReferencedObjects(SourceObject, Collector); } } } if (TargetObjects.Num()) { Algo::Sort(TargetObjects); TargetObjects.SetNum(Algo::Unique(TargetObjects), EAllowShrinking::No); TArray& TargetVertices = LooseEdges[SourceVertex]; TargetVertices.Reserve(TargetObjects.Num()); for (UObject* TargetObject : TargetObjects) { const FVertex* TargetVertex = ObjectToVertex.Find(TargetObject); if (TargetVertex && *TargetVertex != SourceVertex) { TargetVertices.Add(*TargetVertex); } } NumEdges += TargetVertices.Num(); } } OutGraphBuffer.Empty(NumEdges); OutGraph.Empty(NumVertices); for (FVertex SourceVertex = 0; SourceVertex < NumVertices; ++SourceVertex) { TArray& InEdges = LooseEdges[SourceVertex]; TConstArrayView& OutEdges = OutGraph.Emplace_GetRef(); OutEdges = TConstArrayView(OutGraphBuffer.GetData() + OutGraphBuffer.Num(), InEdges.Num()); OutGraphBuffer.Append(InEdges); } } void ConstructObjectGraphProfileData(TConstArrayView InitialObjects, FObjectGraphProfileData& OutProfileData) { using namespace Algo::Graph; // Get the list of Objects OutProfileData.AllObjects.Reset(); for (FThreadSafeObjectIterator Iter; Iter; ++Iter) { UObject* Object = *Iter; if (!Object) { continue; } OutProfileData.AllObjects.Add(Object); } // Convert Objects to Algo::Graph::FVertex to reduce graph search memory OutProfileData.NumObjectVertices = OutProfileData.AllObjects.Num(); OutProfileData.NumVertices = OutProfileData.NumObjectVertices; OutProfileData.VertexOfObject.Reset(); for (FVertex Vertex = 0; Vertex < OutProfileData.NumObjectVertices; ++Vertex) { OutProfileData.VertexOfObject.Add(OutProfileData.AllObjects[Vertex], Vertex); } // Store for each vertex whether the vertex is new - not in InitialObjects OutProfileData.IsNew.Init(true, OutProfileData.NumObjectVertices); for (const FWeakObjectPtr& InitialObjectWeak : InitialObjects) { UObject* InitialObject = InitialObjectWeak.Get(); if (InitialObject) { FVertex* Vertex = OutProfileData.VertexOfObject.Find(InitialObject); if (Vertex) { OutProfileData.IsNew[*Vertex] = false; } } } // Serialize objects to get dependencies and use them to create the ObjectGraph TMap GCObjectReferencerNames; ConstructObjectGraph(OutProfileData.AllObjects, OutProfileData.VertexOfObject, OutProfileData.ObjectGraphBuffer, OutProfileData.ObjectGraph, GCObjectReferencerNames); OutProfileData.GCObjectReferencerVertex = InvalidVertex; OutProfileData.AliveReason.SetNum(OutProfileData.NumObjectVertices); OutProfileData.RootOfVertex.SetNumUninitialized(OutProfileData.NumObjectVertices); for (FVertex& Root : OutProfileData.RootOfVertex) { Root = InvalidVertex; } // Mark the objects that are rooted by IsRooted, and find the special GCObjectReferencerVertex for (FVertex Vertex = 0; Vertex < OutProfileData.NumObjectVertices; ++Vertex) { UObject* Object = OutProfileData.AllObjects[Vertex]; if (Object->IsRooted()) { OutProfileData.AliveReason[Vertex].Set(EObjectReferencerType::Rooted); OutProfileData.RootOfVertex[Vertex] = Vertex; } if (Object == FGCObject::GGCObjectReferencer) { OutProfileData.GCObjectReferencerVertex = Vertex; } } check(OutProfileData.GCObjectReferencerVertex != InvalidVertex); // Mark the objects that are rooted by GCObjectReferencerVertex, and construct a synthetic vertex // for each of the referencer names reported by GCObjectReferencerVertex. OutProfileData.GCObjectNameToVertex.Reset(); OutProfileData.AllGCObjectNames.Reset(); FString UnknownReferencer(TEXT("")); for (FVertex Vertex : OutProfileData.ObjectGraph[OutProfileData.GCObjectReferencerVertex]) { if (OutProfileData.AliveReason[Vertex].GetLinkType() == EObjectReferencerType::Unknown) { UObject* Object = OutProfileData.AllObjects[Vertex]; FString* ReferencerName = &UnknownReferencer; if (Object) { ReferencerName = GCObjectReferencerNames.Find(Object); if (!ReferencerName) { ReferencerName = &UnknownReferencer; } } FVertex& ReferencerVertex = OutProfileData.GCObjectNameToVertex.FindOrAdd(*ReferencerName); if (ReferencerVertex == (FVertex)0) // Having value 0 means it was newly added by FindOrAdd { ReferencerVertex = OutProfileData.NumVertices++; OutProfileData.AllGCObjectNames.Add(*ReferencerName); check(OutProfileData.NumVertices == OutProfileData.AllObjects.Num() + OutProfileData.AllGCObjectNames.Num()); } OutProfileData.AliveReason[Vertex].Set(EObjectReferencerType::GCObjectRef, ReferencerVertex); OutProfileData.RootOfVertex[Vertex] = ReferencerVertex; } } OutProfileData.NumObjectVertices = OutProfileData.AllObjects.Num(); OutProfileData.NumGCObjectNameVertices = OutProfileData.AllGCObjectNames.Num(); // Do a DFS to mark the referencer and root of all non-rooted objects TArray Stack; for (FVertex PotentialRoot = 0; PotentialRoot < OutProfileData.NumObjectVertices; ++PotentialRoot) { if (PotentialRoot == OutProfileData.GCObjectReferencerVertex || (OutProfileData.AliveReason[PotentialRoot].GetLinkType() != EObjectReferencerType::Rooted && OutProfileData.AliveReason[PotentialRoot].GetLinkType() != EObjectReferencerType::GCObjectRef)) { continue; } FVertex RootVertex = OutProfileData.RootOfVertex[PotentialRoot]; Stack.Reset(); Stack.Add(PotentialRoot); while (!Stack.IsEmpty()) { FVertex SourceVertex = Stack.Pop(EAllowShrinking::No); for (FVertex TargetVertex : OutProfileData.ObjectGraph[SourceVertex]) { if (OutProfileData.AliveReason[TargetVertex].GetLinkType() == EObjectReferencerType::Unknown) { OutProfileData.AliveReason[TargetVertex].Set(EObjectReferencerType::Referenced, SourceVertex); OutProfileData.RootOfVertex[TargetVertex] = RootVertex; Stack.Add(TargetVertex); } } } } } void DumpObjClassList(TConstArrayView InitialObjects) { using namespace Algo::Graph; FOutputDevice& LogAr = *(GLog); FObjectGraphProfileData ProfileData; ConstructObjectGraphProfileData(InitialObjects, ProfileData); // Count how many new objects of each class there are, and store all root objects that keep them in memory struct FClassInfo { TMap Roots; int32 Count = 0; UClass* Class = nullptr; }; TMap ClassInfos; for (FVertex Vertex = 0; Vertex < ProfileData.NumObjectVertices; ++Vertex) { // Ignore non-new objects if (!ProfileData.IsNew[Vertex] || Vertex == ProfileData.GCObjectReferencerVertex) { continue; } FObjectReferencer Link = ProfileData.AliveReason[Vertex]; EObjectReferencerType LinkType = Link.GetLinkType(); // Ignore objects that have AliveReason unknown. This can occur if the objects were rooted during garbage // collection but then asynchronous work RemovedThemFromRoot in between GC finishing and our call to IsRooted. if (LinkType == EObjectReferencerType::Unknown) { continue; } UClass* Class = ProfileData.AllObjects[Vertex]->GetClass(); if (!Class || !Class->IsNative()) { continue; } FClassInfo& ClassInfo = ClassInfos.FindOrAdd(Class); ClassInfo.Class = Class; ClassInfo.Roots.FindOrAdd(ProfileData.RootOfVertex[Vertex], 0)++; ClassInfo.Count++; } TArray ClassInfoArray; ClassInfoArray.Reserve(ClassInfos.Num()); for (TPair& Pair : ClassInfos) { ClassInfoArray.Add(MoveTemp(Pair.Value)); } Algo::Sort(ClassInfoArray, [](const FClassInfo& A, const FClassInfo& B) { return FTopLevelAssetPath(A.Class).Compare(FTopLevelAssetPath(B.Class)) < 0; }); LogAr.Logf(TEXT("Memory Analysis: New Objects of each class and the top roots keeping them alive:")); LogAr.Logf(TEXT("\t%6s %s"), TEXT("Count"), TEXT("ClassPath")); LogAr.Logf(TEXT("\t\t%6s %s"), TEXT("Count"), TEXT("RootObjectAndChain")); TStringBuilder<1024> RootObjectString; constexpr int32 MaxRootCount = 2; TArray, TInlineAllocator> MaxRoots; for (FClassInfo& ClassInfo : ClassInfoArray) { MaxRoots.Reset(); for (TPair& RootPair : ClassInfo.Roots) { for (int32 IndexFromMax = 0; IndexFromMax < MaxRootCount; ++IndexFromMax) { if (MaxRoots.Num() <= IndexFromMax || MaxRoots[IndexFromMax].Value < RootPair.Value) { MaxRoots.Insert(RootPair, IndexFromMax); break; } } if (MaxRoots.Num() > MaxRootCount) { MaxRoots.Pop(EAllowShrinking::No); } } LogAr.Logf(TEXT("\t%6d %s"), ClassInfo.Count, *ClassInfo.Class->GetPathName()); for (TPair& RootPair : MaxRoots) { RootObjectString.Reset(); RootObjectString.Appendf(TEXT("\t\t%6d: "), RootPair.Value); ProfileData.AppendVertexName(RootPair.Key, RootObjectString); if (RootPair.Key < ProfileData.NumObjectVertices) { FObjectReferencer Link = ProfileData.AliveReason[RootPair.Key]; while (Link.GetLinkType() == EObjectReferencerType::Referenced) { RootObjectString << TEXT(" <- "); Link.ToString(RootObjectString, ProfileData); Link = ProfileData.AliveReason[Link.GetVertexArgument()]; } RootObjectString << TEXT(" <- "); Link.ToString(RootObjectString, ProfileData); } LogAr.Logf(TEXT("%s"), *RootObjectString); } } } void DumpPackageReferencers(TConstArrayView Packages) { using namespace Algo::Graph; FOutputDevice& LogAr = *(GLog); FObjectGraphProfileData ProfileData; ConstructObjectGraphProfileData(TConstArrayView(), ProfileData); // List all roots that cause any of the Packages to remain alive, and count how many packages each one causes TMap Roots; TMap RootExamples; int32 Unexpected = 0; for (UPackage* Package : Packages) { FVertex* FoundVertex = ProfileData.VertexOfObject.Find(Package); if (!FoundVertex) { ++Unexpected; continue; } FVertex PackageVertex = *FoundVertex; FVertex RootOfThisVertex = ProfileData.RootOfVertex[PackageVertex]; if (RootOfThisVertex == InvalidVertex) { ++Unexpected; continue; } int32& RootCount = Roots.FindOrAdd(RootOfThisVertex); if (RootCount == 0) { RootExamples.Add(RootOfThisVertex, PackageVertex); } ++RootCount; } LogAr.Logf(TEXT("Memory Analysis: Referencers of SoftGCPackages:")); Roots.ValueSort([](int32 A, int32 B) { return A > B; }); for (TPair& Pair : Roots) { TStringBuilder<256> ReferencerName; ProfileData.AppendVertexName(Pair.Key, ReferencerName); LogAr.Logf(TEXT("\t%5d: %s"), Pair.Value, *ReferencerName); FVertex* ExampleVertexPtr = RootExamples.Find(Pair.Key); if (ExampleVertexPtr) { TStringBuilder<256> Chain; FObjectReferencer Link = ProfileData.AliveReason[*ExampleVertexPtr]; ProfileData.AllObjects[*ExampleVertexPtr]->GetFullName(Chain); while (Link.GetLinkType() == EObjectReferencerType::Referenced) { Chain << TEXT(" <- "); Link.ToString(Chain, ProfileData); Link = ProfileData.AliveReason[Link.GetVertexArgument()]; } Chain << TEXT(" <- "); Link.ToString(Chain, ProfileData); LogAr.Logf(TEXT("\t\t Ex: %s"), *Chain); } } if (Unexpected > 0) { LogAr.Logf(TEXT("Memory Analysis: Unknown referenced SoftGCPackages:")); for (UPackage* Package : Packages) { FVertex* FoundVertex = ProfileData.VertexOfObject.Find(Package); if (!FoundVertex) { LogAr.Logf(TEXT("%s: unknown, we did not create a vertex for the package"), *Package->GetName()); continue; } FVertex PackageVertex = *FoundVertex; if (ProfileData.AliveReason[PackageVertex].GetLinkType() == EObjectReferencerType::Unknown) { LogAr.Logf(TEXT("%s: no reference found"), *Package->GetName()); continue; } } } } } // namespace UE::Cook #if ENABLE_COOK_STATS namespace DetailedCookStats { FCookStatsManager::FAutoRegisterCallback RegisterCookStats([](FCookStatsManager::AddStatFuncRef AddStat) { const FString StatName(TEXT("Cook.Profile")); #define ADD_COOK_STAT_FLT(Path, Name) AddStat(StatName, FCookStatsManager::CreateKeyValueArray(TEXT("Path"), TEXT(Path), TEXT(#Name), Name)) ADD_COOK_STAT_FLT(" 0", CookWallTimeSec); ADD_COOK_STAT_FLT(" 0. 0", StartupWallTimeSec); ADD_COOK_STAT_FLT(" 0. 1", StartCookByTheBookTimeSec); ADD_COOK_STAT_FLT(" 0. 1. 0", BlockOnAssetRegistryTimeSec); ADD_COOK_STAT_FLT(" 0. 1. 1", GameCookModificationDelegateTimeSec); ADD_COOK_STAT_FLT(" 0. 2", TickCookOnTheSideTimeSec); ADD_COOK_STAT_FLT(" 0. 2. 0", TickCookOnTheSideLoadPackagesTimeSec); ADD_COOK_STAT_FLT(" 0. 2. 1", TickCookOnTheSideSaveCookedPackageTimeSec); ADD_COOK_STAT_FLT(" 0. 2. 1. 0", TickCookOnTheSideResolveRedirectorsTimeSec); ADD_COOK_STAT_FLT(" 0. 2. 2", TickCookOnTheSidePrepareSaveTimeSec); ADD_COOK_STAT_FLT(" 0. 3", TickLoopGCTimeSec); ADD_COOK_STAT_FLT(" 0. 4", TickLoopRecompileShaderRequestsTimeSec); ADD_COOK_STAT_FLT(" 0. 5", TickLoopShaderProcessAsyncResultsTimeSec); ADD_COOK_STAT_FLT(" 0. 6", TickLoopProcessDeferredCommandsTimeSec); ADD_COOK_STAT_FLT(" 0. 7", TickLoopTickCommandletStatsTimeSec); ADD_COOK_STAT_FLT(" 0. 8", TickLoopFlushRenderingCommandsTimeSec); ADD_COOK_STAT_FLT(" 0. 9", ShaderFlushTimeSec); ADD_COOK_STAT_FLT(" 0. 10", ValidationTimeSec); FString CookParameters; // Empty value to write a header with name "CookParameters" ADD_COOK_STAT_FLT(" 1", CookParameters); ADD_COOK_STAT_FLT(" 1. 0", TargetPlatforms); ADD_COOK_STAT_FLT(" 1. 1", CookProject); ADD_COOK_STAT_FLT(" 1. 2", CookCultures); ADD_COOK_STAT_FLT(" 1. 3", IsCookAll); ADD_COOK_STAT_FLT(" 1. 4", IsCookOnTheFly); ADD_COOK_STAT_FLT(" 1. 5", IsIterativeCook); ADD_COOK_STAT_FLT(" 1. 6", IsUnversioned); ADD_COOK_STAT_FLT(" 1. 7", CookLabel); ADD_COOK_STAT_FLT(" 1. 8", IsFastCook); #undef ADD_COOK_STAT_FLT }); void SendLogCookStats(ECookMode::Type CookMode) { if (IsCookingInEditor(CookMode)) { return; } /** Used to store profile data for custom logging. */ struct FCookProfileData { public: FCookProfileData(FString InPath, FString InKey, FString InValue) : Path(MoveTemp(InPath)), Key(MoveTemp(InKey)), Value(MoveTemp(InValue)) {} FString Path; FString Key; FString Value; }; // instead of printing the usage stats generically, we capture them so we can log a subset of them in an easy-to-read way. TArray DDCResourceUsageStats; TArray DDCSummaryStats; TArray CookProfileData; TArray StatCategories; TMap> StatsInCategories; /** this functor will take a collected cooker stat and log it out using some custom formatting based on known stats that are collected.. */ auto LogStatsFunc = [&DDCResourceUsageStats, &DDCSummaryStats, &CookProfileData, &StatCategories, &StatsInCategories] (const FString& StatName, const TArray& StatAttributes) { // Some stats will use custom formatting to make a visibly pleasing summary. bool bStatUsedCustomFormatting = false; if (StatName == TEXT("DDC.Usage")) { // Don't even log this detailed DDC data. It's mostly only consumable by ingestion into pivot tools. bStatUsedCustomFormatting = true; } else if (StatName.EndsWith(TEXT(".Usage"), ESearchCase::IgnoreCase)) { // These are gathered through GatherResourceStats. bStatUsedCustomFormatting = true; } else if (StatName == TEXT("DDC.Summary")) { DDCSummaryStats.Append(StatAttributes); bStatUsedCustomFormatting = true; } else if (StatName == TEXT("Cook.Profile")) { if (StatAttributes.Num() >= 2) { CookProfileData.Emplace(StatAttributes[0].Value, StatAttributes[1].Key, StatAttributes[1].Value); } bStatUsedCustomFormatting = true; } // if a stat doesn't use custom formatting, just spit out the raw info. if (!bStatUsedCustomFormatting) { TArray& StatsInCategory = StatsInCategories.FindOrAdd(StatName); if (StatsInCategory.Num() == 0) { StatCategories.Add(StatName); } StatsInCategory.Append(StatAttributes); } }; GetDerivedDataCacheRef().GatherResourceStats(DDCResourceUsageStats); FCookStatsManager::LogCookStats(LogStatsFunc); FString CookStatsFileName; FString CookStatsJsonString; TSharedPtr>> CookStatsWriter{}; FStringBuilderBase Builder; if (FParse::Value(FCommandLine::Get(), TEXT("-CookStatsFile="), CookStatsFileName)) { uint32 MultiprocessId = UE::GetMultiprocessId(); if (MultiprocessId == 0) { CookStatsWriter = TJsonWriterFactory>::Create(&CookStatsJsonString).ToSharedPtr(); CookStatsWriter->WriteObjectStart(); } else { // Suppress the file creation on CookWorkers // TODO: Replicate the information back to the CookDirector instead, UE-185774 } } UE_LOG(LogCookStats, Display, TEXT("Misc Cook Stats")); UE_LOG(LogCookStats, Display, TEXT("===============")); for (FString& StatCategory : StatCategories) { UE_LOG(LogCookStats, Display, TEXT("%s"), *StatCategory); TArray& StatsInCategory = StatsInCategories.FindOrAdd(StatCategory); // log each key/value pair, with the equal signs lined up. for (const FCookStatsManager::StringKeyValue& StatKeyValue : StatsInCategory) { UE_LOG(LogCookStats, Display, TEXT(" %s=%s"), *StatKeyValue.Key, *StatKeyValue.Value); } if (CookStatsWriter) { CookStatsWriter->WriteObjectStart(StatCategory); for (const FCookStatsManager::StringKeyValue& StatKeyValue : StatsInCategory) { CookStatsWriter->WriteValue(*StatKeyValue.Key, *StatKeyValue.Value); } CookStatsWriter->WriteObjectEnd(); } } // DDC Usage stats are custom formatted, and the above code just accumulated them into a TSet. Now log it with our special formatting for readability. if (CookProfileData.Num() > 0) { UE_LOG(LogCookStats, Display, TEXT("")); UE_LOG(LogCookStats, Display, TEXT("Cook Profile")); UE_LOG(LogCookStats, Display, TEXT("============")); for (const auto& ProfileEntry : CookProfileData) { UE_LOG(LogCookStats, Display, TEXT("%s.%s=%s"), *ProfileEntry.Path, *ProfileEntry.Key, *ProfileEntry.Value); } if (CookStatsWriter) { CookStatsWriter->WriteObjectStart("CookProfile"); for (const auto& ProfileEntry : CookProfileData) { CookStatsWriter->WriteObjectStart(ProfileEntry.Key); CookStatsWriter->WriteValue(TEXT("Path"), ProfileEntry.Path); CookStatsWriter->WriteValue(TEXT("Value"), ProfileEntry.Value); CookStatsWriter->WriteObjectEnd(); } CookStatsWriter->WriteObjectEnd(); } } if (DDCSummaryStats.Num() > 0) { UE_LOG(LogCookStats, Display, TEXT("")); UE_LOG(LogCookStats, Display, TEXT("DDC Summary Stats")); UE_LOG(LogCookStats, Display, TEXT("=================")); for (const auto& Attr : DDCSummaryStats) { UE_LOG(LogCookStats, Display, TEXT("%-16s=%10s"), *Attr.Key, *Attr.Value); } if (CookStatsWriter) { CookStatsWriter->WriteObjectStart("DDCSummaryStats"); for (const auto& Attr : DDCSummaryStats) { CookStatsWriter->WriteValue(*Attr.Key, *Attr.Value); } CookStatsWriter->WriteObjectEnd(); } } DumpDerivedDataBuildRemoteExecutorStats(); if (!DDCResourceUsageStats.IsEmpty()) { Algo::SortBy(DDCResourceUsageStats, [](const FDerivedDataCacheResourceStat& Stat) { return Stat.BuildTimeSec + Stat.LoadTimeSec; }, TGreater()); UE_LOG(LogCookStats, Display, TEXT("")); UE_LOG(LogCookStats, Display, TEXT("DDC Resource Stats")); UE_LOG(LogCookStats, Display, TEXT("=======================================================================================================")); UE_LOG(LogCookStats, Display, TEXT("Asset Type Total Time (Sec) GameThread Time (Sec) Assets Built MB Processed")); UE_LOG(LogCookStats, Display, TEXT("---------------------------------- ---------------- --------------------- ------------ ------------")); for (const FDerivedDataCacheResourceStat& Stat : DDCResourceUsageStats) { UE_LOG(LogCookStats, Display, TEXT("%-34s %16.2f %21.2f %12d %12.2f"), *Stat.AssetType, Stat.LoadTimeSec + Stat.BuildTimeSec, Stat.GameThreadTimeSec, Stat.BuildCount, Stat.LoadSizeMB + Stat.BuildSizeMB); } if (CookStatsWriter) { CookStatsWriter->WriteObjectStart("DDCResourceStats"); for (const FDerivedDataCacheResourceStat& Stat : DDCResourceUsageStats) { CookStatsWriter->WriteObjectStart(*Stat.AssetType); CookStatsWriter->WriteValue(TEXT("TotalTimeSec"), Stat.LoadTimeSec + Stat.BuildTimeSec); CookStatsWriter->WriteValue(TEXT("GameThreadTimeSec"), Stat.GameThreadTimeSec); CookStatsWriter->WriteValue(TEXT("AssetsBuilt"), Stat.BuildCount); CookStatsWriter->WriteValue(TEXT("MBProcessed"), Stat.LoadSizeMB + Stat.BuildSizeMB); CookStatsWriter->WriteObjectEnd(); } CookStatsWriter->WriteObjectEnd(); } } DumpBuildDependencyTrackerStats(); if (UE::Virtualization::IVirtualizationSystem::IsInitialized()) { UE::Virtualization::IVirtualizationSystem::Get().DumpStats(); } if (CookStatsWriter) { FShaderCompilerStats ShaderCompilerStats; GShaderCompilingManager->GetLocalStats(ShaderCompilerStats); TSharedPtr JsonShaderCompilerStats = ShaderCompilerStats.ToJson(); FJsonSerializer::Serialize( MakeShared(JsonShaderCompilerStats), TEXT("ShaderCompilerStats"), CookStatsWriter.ToSharedRef(), false ); } if (!CookStatsFileName.IsEmpty()) { CookStatsWriter->WriteObjectEnd(); CookStatsWriter->Close(); TUniquePtr CookStatsJsonFile(IFileManager::Get().CreateFileWriter(*CookStatsFileName)); if (!CookStatsJsonFile) { UE_LOG(LogCookStats, Warning, TEXT("Could not write to CookStatsFile %s."), *CookStatsFileName); } else { CookStatsJsonFile->Serialize(TCHAR_TO_ANSI(*CookStatsJsonString), CookStatsJsonString.Len()); CookStatsJsonFile->Close(); } } } } #endif