EM_Task/CoreUObject/Private/UObject/GarbageCollection.cpp
Boshuang Zhao 5144a49c9b add
2026-02-13 16:18:33 +08:00

3057 lines
134 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
/*=============================================================================
UnObjGC.cpp: Unreal object garbage collection code.
=============================================================================*/
#include "UObject/GarbageCollection.h"
#include "HAL/ThreadSafeBool.h"
#include "Misc/TimeGuard.h"
#include "HAL/IConsoleManager.h"
#include "Misc/App.h"
#include "Misc/CoreDelegates.h"
#include "UObject/ScriptInterface.h"
#include "UObject/UObjectAllocator.h"
#include "UObject/UObjectBase.h"
#include "UObject/Object.h"
#include "UObject/Class.h"
#include "UObject/UObjectIterator.h"
#include "UObject/UnrealType.h"
#include "UObject/LinkerLoad.h"
#include "UObject/GCObject.h"
#include "UObject/GCScopeLock.h"
#include "HAL/ExceptionHandling.h"
#include "UObject/UObjectClusters.h"
#include "HAL/LowLevelMemTracker.h"
#include "UObject/GarbageCollectionVerification.h"
#include "UObject/Package.h"
#include "Async/ParallelFor.h"
#include "ProfilingDebugging/CsvProfiler.h"
#include "HAL/Runnable.h"
#include "HAL/RunnableThread.h"
#include "UObject/FieldPathProperty.h"
/*-----------------------------------------------------------------------------
Garbage collection.
-----------------------------------------------------------------------------*/
// FastReferenceCollector uses PERF_DETAILED_PER_CLASS_GC_STATS
#include "UObject/FastReferenceCollector.h"
DEFINE_LOG_CATEGORY(LogGarbage);
/** Object count during last mark phase */
FThreadSafeCounter GObjectCountDuringLastMarkPhase;
/** Whether incremental object purge is in progress */
bool GObjIncrementalPurgeIsInProgress = false;
/** Whether GC is currently routing BeginDestroy to objects */
bool GObjUnhashUnreachableIsInProgress = false;
/** Time the GC started, needs to be reset on return from being in the background on some OSs */
double GCStartTime = 0.;
/** Whether FinishDestroy has already been routed to all unreachable objects. */
static bool GObjFinishDestroyHasBeenRoutedToAllObjects = false;
/**
* Array that we'll fill with indices to objects that are still pending destruction after
* the first GC sweep (because they weren't ready to be destroyed yet.)
*/
static TArray<UObject*> GGCObjectsPendingDestruction;
/** Number of objects actually still pending destruction */
static int32 GGCObjectsPendingDestructionCount = 0;
/** Whether we need to purge objects or not. */
static bool GObjPurgeIsRequired = false;
/** Current object index for incremental purge. */
static int32 GObjCurrentPurgeObjectIndex = 0;
/** Current object index for incremental purge. */
static bool GObjCurrentPurgeObjectIndexNeedsReset = true;
/** Contains a list of objects that stayed marked as unreachable after the last reachability analysis */
static TArray<FUObjectItem*> GUnreachableObjects;
static FCriticalSection GUnreachableObjectsCritical;
static int32 GUnrechableObjectIndex = 0;
/** Helpful constant for determining how many token slots we need to store a pointer **/
static const uint32 GNumTokensPerPointer = sizeof(void*) / sizeof(uint32); //-V514
FThreadSafeBool GIsGarbageCollecting(false);
/**
* Call back into the async loading code to inform of the destruction of serialized objects
*/
void NotifyUnreachableObjects(const TArrayView<FUObjectItem*>& UnreachableObjects);
/** Locks all UObject hash tables when performing GC */
class FGCScopeLock
{
/** Previous value of the GetGarbageCollectingFlag() */
bool bPreviousGabageCollectingFlagValue;
public:
/**
* We're storing the value of GetGarbageCollectingFlag in the constructor, it's safe as only
* one thread is ever going to be setting it and calling this code - the game thread.
**/
FORCEINLINE FGCScopeLock()
: bPreviousGabageCollectingFlagValue(GIsGarbageCollecting)
{
LockUObjectHashTables();
GIsGarbageCollecting = true;
}
FORCEINLINE ~FGCScopeLock()
{
GIsGarbageCollecting = bPreviousGabageCollectingFlagValue;
UnlockUObjectHashTables();
}
};
FGCCSyncObject::FGCCSyncObject()
{
GCUnlockedEvent = FPlatformProcess::GetSynchEventFromPool(true);
}
FGCCSyncObject::~FGCCSyncObject()
{
FPlatformProcess::ReturnSynchEventToPool(GCUnlockedEvent);
GCUnlockedEvent = nullptr;
}
FGCCSyncObject* GGCSingleton;
void FGCCSyncObject::Create()
{
struct FSingletonOwner
{
FGCCSyncObject Singleton;
FSingletonOwner() { GGCSingleton = &Singleton; }
~FSingletonOwner() { GGCSingleton = nullptr; }
};
static const FSingletonOwner MagicStaticSingleton;
}
FGCCSyncObject& FGCCSyncObject::Get()
{
FGCCSyncObject* Singleton = GGCSingleton;
check(Singleton);
return *Singleton;
}
#define UE_LOG_FGCScopeGuard_LockAsync_Time 0
FGCScopeGuard::FGCScopeGuard()
{
#if UE_LOG_FGCScopeGuard_LockAsync_Time
const double StartTime = FPlatformTime::Seconds();
#endif
FGCCSyncObject::Get().LockAsync();
#if UE_LOG_FGCScopeGuard_LockAsync_Time
const double ElapsedTime = FPlatformTime::Seconds() - StartTime;
if (FPlatformProperties::RequiresCookedData() && ElapsedTime > 0.001)
{
// Note this is expected to take roughly the time it takes to collect garbage and verify GC assumptions, so up to 300ms in development
UE_LOG(LogGarbage, Warning, TEXT("%f ms for acquiring ASYNC lock"), ElapsedTime * 1000);
}
#endif
}
FGCScopeGuard::~FGCScopeGuard()
{
FGCCSyncObject::Get().UnlockAsync();
}
bool IsGarbageCollectionLocked()
{
return FGCCSyncObject::Get().IsAsyncLocked();
}
// Minimum number of objects to spawn a GC sub-task for
static int32 GMinDesiredObjectsPerSubTask = 128;
static FAutoConsoleVariableRef CVarMinDesiredObjectsPerSubTask(
TEXT("gc.MinDesiredObjectsPerSubTask"),
GMinDesiredObjectsPerSubTask,
TEXT("Minimum number of objects to spawn a GC sub-task for."),
ECVF_Default);
static int32 GIncrementalBeginDestroyEnabled = 1;
static FAutoConsoleVariableRef CIncrementalBeginDestroyEnabled(
TEXT("gc.IncrementalBeginDestroyEnabled"),
GIncrementalBeginDestroyEnabled,
TEXT("If true, the engine will destroy objects incrementally using time limit each frame"),
ECVF_Default);
int32 GMultithreadedDestructionEnabled = 0;
static FAutoConsoleVariableRef CMultithreadedDestructionEnabled(
TEXT("gc.MultithreadedDestructionEnabled"),
GMultithreadedDestructionEnabled,
TEXT("If true, the engine will free objects' memory from a worker thread"),
ECVF_Default);
#if PERF_DETAILED_PER_CLASS_GC_STATS
/** Map from a UClass' FName to the number of objects that were purged during the last purge phase of this class. */
static TMap<const FName, uint32> GClassToPurgeCountMap;
/** Map from a UClass' FName to the number of "Disregard For GC" object references followed for all instances. */
static TMap<const FName, uint32> GClassToDisregardedObjectRefsMap;
/** Map from a UClass' FName to the number of regular object references followed for all instances. */
static TMap<const FName, uint32> GClassToRegularObjectRefsMap;
/** Map from a UClass' FName to the number of cycles spent with GC. */
static TMap<const FName, uint32> GClassToCyclesMap;
/** Number of disregarded object refs for current object. */
static uint32 GCurrentObjectDisregardedObjectRefs;
/** Number of regulard object refs for current object. */
static uint32 GCurrentObjectRegularObjectRefs;
/**
* Helper structure used for sorting class to count map.
*/
struct FClassCountInfo
{
FName ClassName;
uint32 InstanceCount;
};
/**
* Helper function to log the various class to count info maps.
*
* @param LogText Text to emit between number and class
* @param ClassToCountMap TMap from a class' FName to "count"
* @param NumItemsToList Number of items to log
* @param TotalCount Total count, if 0 will be calculated
*/
static void LogClassCountInfo(const TCHAR* LogText, TMap<const FName, uint32>& ClassToCountMap, int32 NumItemsToLog, uint32 TotalCount)
{
// Array of class name and counts.
TArray<FClassCountInfo> ClassCountArray;
ClassCountArray.Empty(ClassToCountMap.Num());
// Figure out whether we need to calculate the total count.
bool bNeedToCalculateCount = false;
if (TotalCount == 0)
{
bNeedToCalculateCount = true;
}
// Copy TMap to TArray for sorting purposes (and to calculate count if needed).
for (TMap<const FName, uint32>::TIterator It(ClassToCountMap); It; ++It)
{
FClassCountInfo ClassCountInfo;
ClassCountInfo.ClassName = It.Key();
ClassCountInfo.InstanceCount = It.Value();
ClassCountArray.Add(ClassCountInfo);
if (bNeedToCalculateCount)
{
TotalCount += ClassCountInfo.InstanceCount;
}
}
// Sort array by instance count.
struct FCompareFClassCountInfo
{
FORCEINLINE bool operator()(const FClassCountInfo& A, const FClassCountInfo& B) const
{
return B.InstanceCount < A.InstanceCount;
}
};
ClassCountArray.Sort(FCompareFClassCountInfo());
// Log top NumItemsToLog class counts
for (int32 Index = 0; Index < FMath::Min(NumItemsToLog, ClassCountArray.Num()); Index++)
{
const FClassCountInfo& ClassCountInfo = ClassCountArray[Index];
const float Percent = 100.f * ClassCountInfo.InstanceCount / TotalCount;
const FString PercentString = (TotalCount > 0) ? FString::Printf(TEXT("%6.2f%%"), Percent) : FString(TEXT(" N/A "));
UE_LOG(LogGarbage, Log, TEXT("%5d [%s] %s Class %s"), ClassCountInfo.InstanceCount, *PercentString, LogText, *ClassCountInfo.ClassName.ToString());
}
// Empty the map for the next run.
ClassToCountMap.Empty();
};
#endif
/**
* Helper class for destroying UObjects on a worker thread
*/
class FAsyncPurge: public FRunnable
{
/** Thread to run the worker FRunnable on. Destroys objects. */
volatile FRunnableThread* Thread;
/** Id of the worker thread */
uint32 AsyncPurgeThreadId;
/** Stops this thread */
FThreadSafeCounter StopTaskCounter;
/** Event that triggers the UObject destruction */
FEvent* BeginPurgeEvent;
/** Event that signales the UObject destruction is finished */
FEvent* FinishedPurgeEvent;
/** Current index into the global unreachable objects array (GUnreachableObjects) of the object being destroyed */
int32 ObjCurrentPurgeObjectIndex;
/** Number of objects deferred to the game thread to destroy */
int32 NumObjectsToDestroyOnGameThread;
/** Number of objectsalready destroyed on the game thread */
int32 NumObjectsDestroyedOnGameThread;
/** Current index into the global unreachable objects array (GUnreachableObjects) of the object being destroyed on the game thread */
int32 ObjCurrentPurgeObjectIndexOnGameThread;
/** Number of unreachable objects the last time single-threaded tick was called */
int32 LastUnreachableObjectsCount;
/** Stats for the number of objects destroyed */
int32 ObjectsDestroyedSinceLastMarkPhase;
/** [PURGE/GAME THREAD] Destroys objects that are unreachable */
template <bool bMultithreaded> // Having this template argument lets the compiler strip unnecessary checks
bool TickDestroyObjects(bool bUseTimeLimit, float TimeLimit, double StartTime)
{
const int32 TimeLimitEnforcementGranularityForDeletion = 100;
int32 ProcessedObjectsCount = 0;
bool bFinishedDestroyingObjects = true;
while (ObjCurrentPurgeObjectIndex < GUnreachableObjects.Num())
{
FUObjectItem* ObjectItem = GUnreachableObjects[ObjCurrentPurgeObjectIndex];
check(ObjectItem->IsUnreachable());
UObject* Object = (UObject*)ObjectItem->Object;
check(Object->HasAllFlags(RF_FinishDestroyed | RF_BeginDestroyed));
if (!bMultithreaded || Object->IsDestructionThreadSafe())
{
// Can't lock once for the entire batch here as it could hold the lock for too long
GUObjectArray.LockInternalArray();
Object->~UObject();
GUObjectArray.UnlockInternalArray();
GUObjectAllocator.FreeUObject(Object);
GUnreachableObjects[ObjCurrentPurgeObjectIndex] = nullptr;
}
else
{
FPlatformMisc::MemoryBarrier();
++NumObjectsToDestroyOnGameThread;
}
++ProcessedObjectsCount;
++ObjectsDestroyedSinceLastMarkPhase;
++ObjCurrentPurgeObjectIndex;
// Time slicing when running on the game thread
if (!bMultithreaded && bUseTimeLimit && (ProcessedObjectsCount == TimeLimitEnforcementGranularityForDeletion) && (ObjCurrentPurgeObjectIndex < GUnreachableObjects.Num()))
{
ProcessedObjectsCount = 0;
if ((FPlatformTime::Seconds() - StartTime) > TimeLimit)
{
bFinishedDestroyingObjects = false;
break;
}
}
}
return bFinishedDestroyingObjects;
}
/** [GAME THREAD] Destroys objects that are unreachable and couldn't be destroyed on the worker thread */
bool TickDestroyGameThreadObjects(bool bUseTimeLimit, float TimeLimit, double StartTime)
{
const int32 TimeLimitEnforcementGranularityForDeletion = 100;
int32 ProcessedObjectsCount = 0;
bool bFinishedDestroyingObjects = true;
// Lock once for the entire batch
GUObjectArray.LockInternalArray();
// Cache the number of objects to destroy locally. The number may grow later but that's ok, we'll catch up to it in the next tick
const int32 LocalNumObjectsToDestroyOnGameThread = NumObjectsToDestroyOnGameThread;
while (NumObjectsDestroyedOnGameThread < LocalNumObjectsToDestroyOnGameThread && ObjCurrentPurgeObjectIndexOnGameThread < GUnreachableObjects.Num())
{
FUObjectItem* ObjectItem = GUnreachableObjects[ObjCurrentPurgeObjectIndexOnGameThread];
if (ObjectItem)
{
GUnreachableObjects[ObjCurrentPurgeObjectIndexOnGameThread] = nullptr;
UObject* Object = (UObject*)ObjectItem->Object;
Object->~UObject();
GUObjectAllocator.FreeUObject(Object);
++ProcessedObjectsCount;
++NumObjectsDestroyedOnGameThread;
if (bUseTimeLimit && (ProcessedObjectsCount == TimeLimitEnforcementGranularityForDeletion) && NumObjectsDestroyedOnGameThread < LocalNumObjectsToDestroyOnGameThread)
{
ProcessedObjectsCount = 0;
if ((FPlatformTime::Seconds() - StartTime) > TimeLimit)
{
bFinishedDestroyingObjects = false;
break;
}
}
}
++ObjCurrentPurgeObjectIndexOnGameThread;
}
GUObjectArray.UnlockInternalArray();
// Make sure that when we reach the end of GUnreachableObjects array, there's no objects to destroy left
check(!bFinishedDestroyingObjects || NumObjectsDestroyedOnGameThread == LocalNumObjectsToDestroyOnGameThread);
// Note that even though NumObjectsToDestroyOnGameThread may have been incremented by now or still hasn't but it will be
// after we report we're done with all objects, it doesn't matter since we don't care about the result of this function in MT mode
return bFinishedDestroyingObjects;
}
/** Waits for the worker thread to finish destroying objects */
void WaitForAsyncDestructionToFinish()
{
FinishedPurgeEvent->Wait();
}
public:
/**
* Constructor
* @param bMultithreaded if true, the destruction of objects will happen on a worker thread
*/
FAsyncPurge(bool bMultithreaded)
: Thread(nullptr), AsyncPurgeThreadId(0), BeginPurgeEvent(nullptr), FinishedPurgeEvent(nullptr), ObjCurrentPurgeObjectIndex(0), NumObjectsToDestroyOnGameThread(0), NumObjectsDestroyedOnGameThread(0), ObjCurrentPurgeObjectIndexOnGameThread(0), LastUnreachableObjectsCount(0), ObjectsDestroyedSinceLastMarkPhase(0)
{
BeginPurgeEvent = FPlatformProcess::GetSynchEventFromPool(true);
FinishedPurgeEvent = FPlatformProcess::GetSynchEventFromPool(true);
FinishedPurgeEvent->Trigger();
if (bMultithreaded)
{
check(FPlatformProcess::SupportsMultithreading());
FPlatformAtomics::InterlockedExchangePtr((void**)&Thread, FRunnableThread::Create(this, TEXT("FAsyncPurge"), 0, TPri_BelowNormal));
}
else
{
AsyncPurgeThreadId = GGameThreadId;
}
}
virtual ~FAsyncPurge()
{
check(IsFinished());
delete Thread;
Thread = nullptr;
FPlatformProcess::ReturnSynchEventToPool(BeginPurgeEvent);
FPlatformProcess::ReturnSynchEventToPool(FinishedPurgeEvent);
BeginPurgeEvent = nullptr;
FinishedPurgeEvent = nullptr;
}
/** Returns true if the destruction process is finished */
FORCEINLINE bool IsFinished() const
{
if (Thread)
{
return FinishedPurgeEvent->Wait(0, true) && NumObjectsToDestroyOnGameThread == NumObjectsDestroyedOnGameThread;
}
else
{
return (ObjCurrentPurgeObjectIndex >= LastUnreachableObjectsCount && NumObjectsToDestroyOnGameThread == NumObjectsDestroyedOnGameThread);
}
}
/** [MAIN THREAD] Adds objects to the purge queue */
void BeginPurge()
{
check(IsFinished()); // In single-threaded mode we need to be finished or the condition below will hang
if (FinishedPurgeEvent->Wait())
{
FinishedPurgeEvent->Reset();
ObjCurrentPurgeObjectIndex = 0;
ObjectsDestroyedSinceLastMarkPhase = 0;
NumObjectsToDestroyOnGameThread = 0;
NumObjectsDestroyedOnGameThread = 0;
ObjCurrentPurgeObjectIndexOnGameThread = 0;
BeginPurgeEvent->Trigger();
}
}
/** [GAME THREAD] Ticks the purge process on the game thread */
void TickPurge(bool bUseTimeLimit, float TimeLimit, double StartTime)
{
bool bCanStartDestroyingGameThreadObjects = true;
if (!Thread)
{
// If we're running single-threaded we need to tick the main loop here too
LastUnreachableObjectsCount = GUnreachableObjects.Num();
bCanStartDestroyingGameThreadObjects = TickDestroyObjects<false>(bUseTimeLimit, TimeLimit, StartTime);
}
if (bCanStartDestroyingGameThreadObjects)
{
do
{
// Deal with objects that couldn't be destroyed on the worker thread. This will do nothing when running single-threaded
bool bFinishedDestroyingObjectsOnGameThread = TickDestroyGameThreadObjects(bUseTimeLimit, TimeLimit, StartTime);
if (!Thread && bFinishedDestroyingObjectsOnGameThread)
{
// This only gets triggered here in single-threaded mode
FinishedPurgeEvent->Trigger();
}
} while (!bUseTimeLimit && !IsFinished());
}
}
/** Returns the number of objects already destroyed */
int32 GetObjectsDestroyedSinceLastMarkPhase() const
{
return ObjectsDestroyedSinceLastMarkPhase;
}
/** Resets the number of objects already destroyed */
void ResetObjectsDestroyedSinceLastMarkPhase()
{
ObjectsDestroyedSinceLastMarkPhase = 0;
}
/**
* Returns true if this function is called from the async destruction thread.
* It will also return true if we're running single-threaded and this function is called on the game thread
*/
bool IsInAsyncPurgeThread() const
{
return AsyncPurgeThreadId == FPlatformTLS::GetCurrentThreadId();
}
/* Returns true if it can run multi-threaded destruction */
bool IsMultithreaded() const
{
return !!Thread;
}
//~ Begin FRunnable Interface.
virtual bool Init()
{
return true;
}
virtual uint32 Run()
{
AsyncPurgeThreadId = FPlatformTLS::GetCurrentThreadId();
while (StopTaskCounter.GetValue() == 0)
{
if (BeginPurgeEvent->Wait(15, true))
{
BeginPurgeEvent->Reset();
TickDestroyObjects<true>(/* bUseTimeLimit = */ false, /* TimeLimit = */ 0.0f, /* StartTime = */ 0.0);
FinishedPurgeEvent->Trigger();
}
}
FinishedPurgeEvent->Trigger();
return 0;
}
virtual void Stop()
{
StopTaskCounter.Increment();
}
//~ End FRunnable Interface
void VerifyAllObjectsDestroyed()
{
for (FUObjectItem* ObjectItem: GUnreachableObjects)
{
UE_CLOG(ObjectItem, LogGarbage, Fatal, TEXT("Object 0x%016llx has not been destroyed during async purge"), (int64)(PTRINT)ObjectItem->Object);
}
}
};
static FAsyncPurge* GAsyncPurge = nullptr;
/**
* Returns true if this function is called from the async destruction thread.
* It will also return true if we're running single-threaded and this function is called on the game thread
*/
bool IsInGarbageCollectorThread()
{
return GAsyncPurge ? GAsyncPurge->IsInAsyncPurgeThread() : IsInGameThread();
}
/** Called on shutdown to free GC memory */
void ShutdownGarbageCollection()
{
FGCArrayPool::Get().Cleanup();
delete GAsyncPurge;
GAsyncPurge = nullptr;
}
/**
* Handles UObject references found by TFastReferenceCollector
*/
#if UE_WITH_GC
template <EFastReferenceCollectorOptions Options>
class FGCReferenceProcessor
{
public:
constexpr static FORCEINLINE bool IsParallel()
{
return !!(Options & EFastReferenceCollectorOptions::Parallel);
}
constexpr static FORCEINLINE bool IsWithClusters()
{
return !!(Options & EFastReferenceCollectorOptions::WithClusters);
}
FGCReferenceProcessor()
{
}
void SetCurrentObject(UObject* InObject)
{
}
FORCEINLINE int32 GetMinDesiredObjectsPerSubTask() const
{
return GMinDesiredObjectsPerSubTask;
}
void UpdateDetailedStats(UObject* CurrentObject, uint32 DeltaCycles)
{
#if PERF_DETAILED_PER_CLASS_GC_STATS
// Keep track of how many refs we encountered for the object's class.
const FName& ClassName = CurrentObject->GetClass()->GetFName();
// Refs to objects that reside in permanent object pool.
uint32 ClassDisregardedObjRefs = GClassToDisregardedObjectRefsMap.FindRef(ClassName);
GClassToDisregardedObjectRefsMap.Add(ClassName, ClassDisregardedObjRefs + GCurrentObjectDisregardedObjectRefs);
// Refs to regular objects.
uint32 ClassRegularObjRefs = GClassToRegularObjectRefsMap.FindRef(ClassName);
GClassToRegularObjectRefsMap.Add(ClassName, ClassRegularObjRefs + GCurrentObjectRegularObjectRefs);
// Track per class cycle count spent in GC.
uint32 ClassCycles = GClassToCyclesMap.FindRef(ClassName);
GClassToCyclesMap.Add(ClassName, ClassCycles + DeltaCycles);
// Reset current counts.
GCurrentObjectDisregardedObjectRefs = 0;
GCurrentObjectRegularObjectRefs = 0;
#endif
}
void LogDetailedStatsSummary()
{
#if PERF_DETAILED_PER_CLASS_GC_STATS
LogClassCountInfo(TEXT("references to regular objects from"), GClassToRegularObjectRefsMap, 20, 0);
LogClassCountInfo(TEXT("references to permanent objects from"), GClassToDisregardedObjectRefsMap, 20, 0);
LogClassCountInfo(TEXT("cycles for GC"), GClassToCyclesMap, 20, 0);
#endif
}
/** Marks all objects that can't be directly in a cluster but are referenced by it as reachable */
static FORCENOINLINE bool MarkClusterMutableObjectsAsReachable(FUObjectCluster& Cluster, TArray<UObject*>& ObjectsToSerialize)
{
check(IsWithClusters());
// This is going to be the return value and basically means that we ran across some pending kill objects
bool bAddClusterObjectsToSerialize = false;
for (int32& ReferencedMutableObjectIndex: Cluster.MutableObjects)
{
if (ReferencedMutableObjectIndex >= 0) // Pending kill support
{
FUObjectItem* ReferencedMutableObjectItem = GUObjectArray.IndexToObjectUnsafeForGC(ReferencedMutableObjectIndex);
if (IsParallel())
{
if (!ReferencedMutableObjectItem->IsPendingKill())
{
if (ReferencedMutableObjectItem->IsUnreachable())
{
if (ReferencedMutableObjectItem->ThisThreadAtomicallyClearedRFUnreachable())
{
// Needs doing because this is either a normal unclustered object (clustered objects are never unreachable) or a cluster root
ObjectsToSerialize.Add(static_cast<UObject*>(ReferencedMutableObjectItem->Object));
// So is this a cluster root maybe?
if (ReferencedMutableObjectItem->GetOwnerIndex() < 0)
{
MarkReferencedClustersAsReachable(ReferencedMutableObjectItem->GetClusterIndex(), ObjectsToSerialize);
}
}
}
else if (ReferencedMutableObjectItem->GetOwnerIndex() > 0 && !ReferencedMutableObjectItem->HasAnyFlags(EInternalObjectFlags::ReachableInCluster))
{
// This is a clustered object that maybe hasn't been processed yet
if (ReferencedMutableObjectItem->ThisThreadAtomicallySetFlag(EInternalObjectFlags::ReachableInCluster))
{
// Needs doing, we need to get its cluster root and process it too
FUObjectItem* ReferencedMutableObjectsClusterRootItem = GUObjectArray.IndexToObjectUnsafeForGC(ReferencedMutableObjectItem->GetOwnerIndex());
if (ReferencedMutableObjectsClusterRootItem->IsUnreachable())
{
// The root is also maybe unreachable so process it and all the referenced clusters
if (ReferencedMutableObjectsClusterRootItem->ThisThreadAtomicallyClearedRFUnreachable())
{
MarkReferencedClustersAsReachable(ReferencedMutableObjectsClusterRootItem->GetClusterIndex(), ObjectsToSerialize);
}
}
}
}
}
else
{
// Pending kill support for clusters (multi-threaded case)
ReferencedMutableObjectIndex = -1;
bAddClusterObjectsToSerialize = true;
}
}
else if (!ReferencedMutableObjectItem->IsPendingKill())
{
if (ReferencedMutableObjectItem->IsUnreachable())
{
// Needs doing because this is either a normal unclustered object (clustered objects are never unreachable) or a cluster root
ReferencedMutableObjectItem->ClearFlags(EInternalObjectFlags::Unreachable);
ObjectsToSerialize.Add(static_cast<UObject*>(ReferencedMutableObjectItem->Object));
// So is this a cluster root?
if (ReferencedMutableObjectItem->GetOwnerIndex() < 0)
{
MarkReferencedClustersAsReachable(ReferencedMutableObjectItem->GetClusterIndex(), ObjectsToSerialize);
}
}
else if (ReferencedMutableObjectItem->GetOwnerIndex() > 0 && !ReferencedMutableObjectItem->HasAnyFlags(EInternalObjectFlags::ReachableInCluster))
{
// This is a clustered object that hasn't been processed yet
ReferencedMutableObjectItem->SetFlags(EInternalObjectFlags::ReachableInCluster);
// If the root is also unreachable, process it and all its referenced clusters
FUObjectItem* ReferencedMutableObjectsClusterRootItem = GUObjectArray.IndexToObjectUnsafeForGC(ReferencedMutableObjectItem->GetOwnerIndex());
if (ReferencedMutableObjectsClusterRootItem->IsUnreachable())
{
ReferencedMutableObjectsClusterRootItem->ClearFlags(EInternalObjectFlags::Unreachable);
MarkReferencedClustersAsReachable(ReferencedMutableObjectsClusterRootItem->GetClusterIndex(), ObjectsToSerialize);
}
}
}
else
{
// Pending kill support for clusters (single-threaded case)
ReferencedMutableObjectIndex = -1;
bAddClusterObjectsToSerialize = true;
}
}
}
return bAddClusterObjectsToSerialize;
}
/** Marks all clusters referenced by another cluster as reachable */
static FORCENOINLINE void MarkReferencedClustersAsReachable(int32 ClusterIndex, TArray<UObject*>& ObjectsToSerialize)
{
check(IsWithClusters());
// If we run across some PendingKill objects we need to add all objects from this cluster
// to ObjectsToSerialize so that we can properly null out all the references.
// It also means this cluster will have to be dissolved because we may no longer guarantee all cross-cluster references are correct.
bool bAddClusterObjectsToSerialize = false;
FUObjectCluster& Cluster = GUObjectClusters[ClusterIndex];
// Also mark all referenced objects from outside of the cluster as reachable
for (int32& ReferncedClusterIndex: Cluster.ReferencedClusters)
{
if (ReferncedClusterIndex >= 0) // Pending Kill support
{
FUObjectItem* ReferencedClusterRootObjectItem = GUObjectArray.IndexToObjectUnsafeForGC(ReferncedClusterIndex);
if (!ReferencedClusterRootObjectItem->IsPendingKill())
{
// This condition should get collapsed by the compiler based on the template argument
if (IsParallel())
{
if (ReferencedClusterRootObjectItem->IsUnreachable())
{
ReferencedClusterRootObjectItem->ThisThreadAtomicallyClearedFlag(EInternalObjectFlags::Unreachable);
}
}
else
{
ReferencedClusterRootObjectItem->ClearFlags(EInternalObjectFlags::Unreachable);
}
}
else
{
// Pending kill support for clusters
ReferncedClusterIndex = -1;
bAddClusterObjectsToSerialize = true;
}
}
}
if (MarkClusterMutableObjectsAsReachable(Cluster, ObjectsToSerialize))
{
bAddClusterObjectsToSerialize = true;
}
if (bAddClusterObjectsToSerialize)
{
// We need to process all cluster objects to handle PendingKill objects we nulled out (-1) from the cluster.
for (int32 ClusterObjectIndex: Cluster.Objects)
{
FUObjectItem* ClusterObjectItem = GUObjectArray.IndexToObjectUnsafeForGC(ClusterObjectIndex);
UObject* ClusterObject = static_cast<UObject*>(ClusterObjectItem->Object);
ObjectsToSerialize.Add(ClusterObject);
}
Cluster.bNeedsDissolving = true;
GUObjectClusters.SetClustersNeedDissolving();
}
}
/**
* Handles object reference, potentially NULL'ing
*
* @param Object Object pointer passed by reference
* @param ReferencingObject UObject which owns the reference (can be NULL)
* @param bAllowReferenceElimination Whether to allow NULL'ing the reference if RF_PendingKill is set
*/
FORCEINLINE void HandleObjectReference(TArray<UObject*>& ObjectsToSerialize, const UObject* const ReferencingObject, UObject*& Object, const bool bAllowReferenceElimination)
{
// Disregard NULL objects and perform very fast check to see whether object is part of permanent
// object pool and should therefore be disregarded. The check doesn't touch the object and is
// cache friendly as it's just a pointer compare against to globals.
const bool IsInPermanentPool = GUObjectAllocator.ResidesInPermanentPool(Object);
#if PERF_DETAILED_PER_CLASS_GC_STATS
if (IsInPermanentPool)
{
GCurrentObjectDisregardedObjectRefs++;
}
#endif
if (Object == nullptr || IsInPermanentPool)
{
return;
}
const int32 ObjectIndex = GUObjectArray.ObjectToIndex(Object);
FUObjectItem* ObjectItem = GUObjectArray.IndexToObjectUnsafeForGC(ObjectIndex);
// Remove references to pending kill objects if we're allowed to do so.
if (ObjectItem->IsPendingKill() && bAllowReferenceElimination)
{
// checkSlow(ObjectItem->HasAnyFlags(EInternalObjectFlags::ClusterRoot) == false);
checkSlow(ObjectItem->GetOwnerIndex() <= 0)
// Null out reference.
Object = NULL;
}
// Add encountered object reference to list of to be serialized objects if it hasn't already been added.
else if (ObjectItem->IsUnreachable())
{
if (IsParallel())
{
// Mark it as reachable.
if (ObjectItem->ThisThreadAtomicallyClearedRFUnreachable())
{
// Objects that are part of a GC cluster should never have the unreachable flag set!
checkSlow(ObjectItem->GetOwnerIndex() <= 0);
if (!IsWithClusters() || !ObjectItem->HasAnyFlags(EInternalObjectFlags::ClusterRoot))
{
// Add it to the list of objects to serialize.
ObjectsToSerialize.Add(Object);
}
else
{
// This is a cluster root reference so mark all referenced clusters as reachable
MarkReferencedClustersAsReachable(ObjectItem->GetClusterIndex(), ObjectsToSerialize);
}
}
}
else
{
#if ENABLE_GC_DEBUG_OUTPUT
// this message is to help track down culprits behind "Object in PIE world still referenced" errors
if (GIsEditor && !GIsPlayInEditorWorld && ReferencingObject != nullptr && !ReferencingObject->HasAnyFlags(RF_Transient) && Object->RootPackageHasAnyFlags(PKG_PlayInEditor))
{
UPackage* ReferencingPackage = ReferencingObject->GetOutermost();
if (!ReferencingPackage->HasAnyPackageFlags(PKG_PlayInEditor) && !ReferencingPackage->HasAnyFlags(RF_Transient))
{
UE_LOG(LogGarbage, Warning, TEXT("GC detected illegal reference to PIE object from content [possibly via [todo]]:"));
UE_LOG(LogGarbage, Warning, TEXT(" PIE object: %s"), *Object->GetFullName());
UE_LOG(LogGarbage, Warning, TEXT(" NON-PIE object: %s"), *ReferencingObject->GetFullName());
}
}
#endif
// Mark it as reachable.
ObjectItem->ClearUnreachable();
// Objects that are part of a GC cluster should never have the unreachable flag set!
checkSlow(ObjectItem->GetOwnerIndex() <= 0);
if (!IsWithClusters() || !ObjectItem->HasAnyFlags(EInternalObjectFlags::ClusterRoot))
{
// Add it to the list of objects to serialize.
ObjectsToSerialize.Add(Object);
}
else
{
// This is a cluster root reference so mark all referenced clusters as reachable
MarkReferencedClustersAsReachable(ObjectItem->GetClusterIndex(), ObjectsToSerialize);
}
}
}
else if (IsWithClusters() && (ObjectItem->GetOwnerIndex() > 0 && !ObjectItem->HasAnyFlags(EInternalObjectFlags::ReachableInCluster)))
{
bool bNeedsDoing = true;
if (IsParallel())
{
bNeedsDoing = ObjectItem->ThisThreadAtomicallySetFlag(EInternalObjectFlags::ReachableInCluster);
}
else
{
ObjectItem->SetFlags(EInternalObjectFlags::ReachableInCluster);
}
if (bNeedsDoing)
{
// Make sure cluster root object is reachable too
const int32 OwnerIndex = ObjectItem->GetOwnerIndex();
FUObjectItem* RootObjectItem = GUObjectArray.IndexToObjectUnsafeForGC(OwnerIndex);
checkSlow(RootObjectItem->HasAnyFlags(EInternalObjectFlags::ClusterRoot));
if (IsParallel())
{
if (RootObjectItem->ThisThreadAtomicallyClearedRFUnreachable())
{
// Make sure all referenced clusters are marked as reachable too
MarkReferencedClustersAsReachable(RootObjectItem->GetClusterIndex(), ObjectsToSerialize);
}
}
else if (RootObjectItem->IsUnreachable())
{
RootObjectItem->ClearFlags(EInternalObjectFlags::Unreachable);
// Make sure all referenced clusters are marked as reachable too
MarkReferencedClustersAsReachable(RootObjectItem->GetClusterIndex(), ObjectsToSerialize);
}
}
}
#if PERF_DETAILED_PER_CLASS_GC_STATS
GCurrentObjectRegularObjectRefs++;
#endif
}
/**
* Handles UObject reference from the token stream.
*
* @param ObjectsToSerialize An array of remaining objects to serialize.
* @param ReferencingObject Object referencing the object to process.
* @param Object Object being referenced.
* @param TokenIndex Index to the token stream where the reference was found.
* @param bAllowReferenceElimination True if reference elimination is allowed.
*/
FORCEINLINE void HandleTokenStreamObjectReference(TArray<UObject*>& ObjectsToSerialize, UObject* ReferencingObject, UObject*& Object, const int32 TokenIndex, bool bAllowReferenceElimination)
{
#if ENABLE_GC_OBJECT_CHECKS
if (Object)
{
if (
#if DO_POINTER_CHECKS_ON_GC
!IsPossiblyAllocatedUObjectPointer(Object) ||
#endif
!Object->IsValidLowLevelFast())
{
FString TokenDebugInfo;
if (UClass* Class = (ReferencingObject ? ReferencingObject->GetClass() : nullptr))
{
FTokenInfo TokenInfo = Class->ReferenceTokenStream.GetTokenInfo(TokenIndex);
TokenDebugInfo = FString::Printf(TEXT("ReferencingObjectClass: %s, Property Name: %s, Offset: %d"),
*Class->GetFullName(), *TokenInfo.Name.GetPlainNameString(), TokenInfo.Offset);
}
else
{
// This means this objects is most likely being referenced by AddReferencedObjects
TokenDebugInfo = TEXT("Native Reference");
}
UE_LOG(LogGarbage, Fatal, TEXT("Invalid object in GC: 0x%016llx, ReferencingObject: %s, %s, TokenIndex: %d"),
(int64)(PTRINT)Object,
ReferencingObject ? *ReferencingObject->GetFullName() : TEXT("NULL"),
*TokenDebugInfo, TokenIndex);
}
}
#endif // ENABLE_GC_OBJECT_CHECKS
HandleObjectReference(ObjectsToSerialize, ReferencingObject, Object, bAllowReferenceElimination);
}
};
template <EFastReferenceCollectorOptions Options>
FGCCollector<Options>::FGCCollector(FGCReferenceProcessor<Options>& InProcessor, FGCArrayStruct& InObjectArrayStruct)
: ReferenceProcessor(InProcessor), ObjectArrayStruct(InObjectArrayStruct), bAllowEliminatingReferences(true)
{
}
template <EFastReferenceCollectorOptions Options>
FORCEINLINE void FGCCollector<Options>::InternalHandleObjectReference(UObject*& Object, const UObject* ReferencingObject, const FProperty* ReferencingProperty)
{
#if ENABLE_GC_OBJECT_CHECKS
if (Object && !Object->IsValidLowLevelFast())
{
UE_LOG(LogGarbage, Fatal, TEXT("Invalid object in GC: 0x%016llx, ReferencingObject: %s, ReferencingProperty: %s"),
(int64)(PTRINT)Object,
ReferencingObject ? *ReferencingObject->GetFullName() : TEXT("NULL"),
ReferencingProperty ? *ReferencingProperty->GetFullName() : TEXT("NULL"));
}
#endif // ENABLE_GC_OBJECT_CHECKS
ReferenceProcessor.HandleObjectReference(ObjectArrayStruct.ObjectsToSerialize, const_cast<UObject*>(ReferencingObject), Object, bAllowEliminatingReferences);
}
template <EFastReferenceCollectorOptions Options>
void FGCCollector<Options>::HandleObjectReference(UObject*& Object, const UObject* ReferencingObject, const FProperty* ReferencingProperty)
{
InternalHandleObjectReference(Object, ReferencingObject, ReferencingProperty);
}
template <EFastReferenceCollectorOptions Options>
void FGCCollector<Options>::HandleObjectReferences(UObject** InObjects, const int32 ObjectNum, const UObject* InReferencingObject, const FProperty* InReferencingProperty)
{
for (int32 ObjectIndex = 0; ObjectIndex < ObjectNum; ++ObjectIndex)
{
UObject*& Object = InObjects[ObjectIndex];
InternalHandleObjectReference(Object, InReferencingObject, InReferencingProperty);
}
}
#endif // UE_WITH_GC
/*----------------------------------------------------------------------------
FReferenceFinder.
----------------------------------------------------------------------------*/
FReferenceFinder::FReferenceFinder(TArray<UObject*>& InObjectArray, UObject* InOuter, bool bInRequireDirectOuter, bool bInShouldIgnoreArchetype, bool bInSerializeRecursively, bool bInShouldIgnoreTransient)
: ObjectArray(InObjectArray), LimitOuter(InOuter), SerializedProperty(nullptr), bRequireDirectOuter(bInRequireDirectOuter), bShouldIgnoreArchetype(bInShouldIgnoreArchetype), bSerializeRecursively(false), bShouldIgnoreTransient(bInShouldIgnoreTransient)
{
bSerializeRecursively = bInSerializeRecursively && LimitOuter != NULL;
if (InOuter)
{
// If the outer is specified, try to set the SerializedProperty based on its linker.
auto OuterLinker = InOuter->GetLinker();
if (OuterLinker)
{
SerializedProperty = OuterLinker->GetSerializedProperty();
}
}
}
void FReferenceFinder::FindReferences(UObject* Object, UObject* InReferencingObject, FProperty* InReferencingProperty)
{
check(Object != NULL);
if (!Object->GetClass()->IsChildOf(UClass::StaticClass()))
{
FVerySlowReferenceCollectorArchiveScope CollectorScope(GetVerySlowReferenceCollectorArchive(), InReferencingObject, SerializedProperty);
Object->SerializeScriptProperties(CollectorScope.GetArchive());
}
Object->CallAddReferencedObjects(*this);
}
void FReferenceFinder::HandleObjectReference(UObject*& InObject, const UObject* InReferencingObject /*= NULL*/, const FProperty* InReferencingProperty /*= NULL*/)
{
// Avoid duplicate entries.
if (InObject != NULL)
{
if (LimitOuter == NULL || (InObject->GetOuter() == LimitOuter || (!bRequireDirectOuter && InObject->IsIn(LimitOuter))))
{
// Many places that use FReferenceFinder expect the object to not be const.
UObject* Object = const_cast<UObject*>(InObject);
// do not attempt to serialize objects that have already been
if (ObjectArray.Contains(Object) == false)
{
check(Object->IsValidLowLevel());
ObjectArray.Add(Object);
}
// check this object for any potential object references
if (bSerializeRecursively == true && !SerializedObjects.Find(Object))
{
SerializedObjects.Add(Object);
FindReferences(Object, const_cast<UObject*>(InReferencingObject), const_cast<FProperty*>(InReferencingProperty));
}
}
}
}
/**
* Implementation of parallel realtime garbage collector using recursive subdivision
*
* The approach is to create an array of uint32 tokens for each class that describe object references. This is done for
* script exposed classes by traversing the properties and additionally via manual function calls to emit tokens for
* native only classes in the construction singleton IMPLEMENT_INTRINSIC_CLASS.
* A third alternative is a AddReferencedObjects callback per object which
* is used to deal with object references from types that aren't supported by the reflectable type system.
* interface doesn't make sense to implement for.
*/
#if UE_WITH_GC
class FRealtimeGC: public FGarbageCollectionTracer
{
typedef void (FRealtimeGC::*MarkObjectsFn)(TArray<UObject*>&, const EObjectFlags);
typedef void (FRealtimeGC::*ReachabilityAnalysisFn)(FGCArrayStruct*);
/** Pointers to functions used for Marking objects as unreachable */
MarkObjectsFn MarkObjectsFunctions[4];
/** Pointers to functions used for Reachability Analysis */
ReachabilityAnalysisFn ReachabilityAnalysisFunctions[4];
template <EFastReferenceCollectorOptions CollectorOptions>
void PerformReachabilityAnalysisOnObjectsInternal(FGCArrayStruct* ArrayStruct)
{
FGCReferenceProcessor<CollectorOptions> ReferenceProcessor;
// NOTE: we want to run with automatic token stream generation off as it should be already generated at this point,
// BUT we want to be ignoring Noop tokens as they're only pointing either at null references or at objects that never get GC'd (native classes)
TFastReferenceCollector<
FGCReferenceProcessor<CollectorOptions>,
FGCCollector<CollectorOptions>,
FGCArrayPool,
CollectorOptions>
ReferenceCollector(ReferenceProcessor, FGCArrayPool::Get());
ReferenceCollector.CollectReferences(*ArrayStruct);
}
/** Calculates GC function index based on current settings */
static FORCEINLINE int32 GetGCFunctionIndex(bool bParallel, bool bWithClusters)
{
return (int32(bParallel) | (int32(bWithClusters) << 1));
}
public:
/** Default constructor, initializing all members. */
FRealtimeGC()
{
MarkObjectsFunctions[GetGCFunctionIndex(false, false)] = &FRealtimeGC::MarkObjectsAsUnreachable<false, false>;
MarkObjectsFunctions[GetGCFunctionIndex(true, false)] = &FRealtimeGC::MarkObjectsAsUnreachable<true, false>;
MarkObjectsFunctions[GetGCFunctionIndex(false, true)] = &FRealtimeGC::MarkObjectsAsUnreachable<false, true>;
MarkObjectsFunctions[GetGCFunctionIndex(true, true)] = &FRealtimeGC::MarkObjectsAsUnreachable<true, true>;
ReachabilityAnalysisFunctions[GetGCFunctionIndex(false, false)] = &FRealtimeGC::PerformReachabilityAnalysisOnObjectsInternal<EFastReferenceCollectorOptions::None | EFastReferenceCollectorOptions::None>;
ReachabilityAnalysisFunctions[GetGCFunctionIndex(true, false)] = &FRealtimeGC::PerformReachabilityAnalysisOnObjectsInternal<EFastReferenceCollectorOptions::Parallel | EFastReferenceCollectorOptions::None>;
ReachabilityAnalysisFunctions[GetGCFunctionIndex(false, true)] = &FRealtimeGC::PerformReachabilityAnalysisOnObjectsInternal<EFastReferenceCollectorOptions::None | EFastReferenceCollectorOptions::WithClusters>;
ReachabilityAnalysisFunctions[GetGCFunctionIndex(true, true)] = &FRealtimeGC::PerformReachabilityAnalysisOnObjectsInternal<EFastReferenceCollectorOptions::Parallel | EFastReferenceCollectorOptions::WithClusters>;
}
/**
* Marks all objects that don't have KeepFlags and EInternalObjectFlags::GarbageCollectionKeepFlags as unreachable
* This function is a template to speed up the case where we don't need to assemble the token stream (saves about 6ms on PS4)
*/
template <bool bParallel, bool bWithClusters>
void MarkObjectsAsUnreachable(TArray<UObject*>& ObjectsToSerialize, const EObjectFlags KeepFlags)
{
const EInternalObjectFlags FastKeepFlags = EInternalObjectFlags::GarbageCollectionKeepFlags;
const int32 MaxNumberOfObjects = GUObjectArray.GetObjectArrayNum() - GUObjectArray.GetFirstGCIndex();
const int32 NumThreads = FMath::Max(1, FTaskGraphInterface::Get().GetNumWorkerThreads());
const int32 NumberOfObjectsPerThread = (MaxNumberOfObjects / NumThreads) + 1;
TLockFreePointerListFIFO<FUObjectItem, PLATFORM_CACHE_LINE_SIZE> ClustersToDissolveList;
TLockFreePointerListFIFO<FUObjectItem, PLATFORM_CACHE_LINE_SIZE> KeepClusterRefsList;
FGCArrayStruct** ObjectsToSerializeArrays = new FGCArrayStruct*[NumThreads];
for (int32 ThreadIndex = 0; ThreadIndex < NumThreads; ++ThreadIndex)
{
ObjectsToSerializeArrays[ThreadIndex] = FGCArrayPool::Get().GetArrayStructFromPool();
}
// Iterate over all objects. Note that we iterate over the UObjectArray and usually check only internal flags which
// are part of the array so we don't suffer from cache misses as much as we would if we were to check ObjectFlags.
ParallelFor(NumThreads, [ObjectsToSerializeArrays, &ClustersToDissolveList, &KeepClusterRefsList, FastKeepFlags, KeepFlags, NumberOfObjectsPerThread, NumThreads, MaxNumberOfObjects](int32 ThreadIndex)
{
int32 FirstObjectIndex = ThreadIndex * NumberOfObjectsPerThread + GUObjectArray.GetFirstGCIndex();
int32 NumObjects = (ThreadIndex < (NumThreads - 1)) ? NumberOfObjectsPerThread : (MaxNumberOfObjects - (NumThreads - 1) * NumberOfObjectsPerThread);
int32 LastObjectIndex = FMath::Min(GUObjectArray.GetObjectArrayNum() - 1, FirstObjectIndex + NumObjects - 1);
int32 ObjectCountDuringMarkPhase = 0;
TArray<UObject*>& LocalObjectsToSerialize = ObjectsToSerializeArrays[ThreadIndex]->ObjectsToSerialize;
for (int32 ObjectIndex = FirstObjectIndex; ObjectIndex <= LastObjectIndex; ++ObjectIndex)
{
FUObjectItem* ObjectItem = &GUObjectArray.GetObjectItemArrayUnsafe()[ObjectIndex];
if (ObjectItem->Object)
{
UObject* Object = (UObject*)ObjectItem->Object;
// We can't collect garbage during an async load operation and by now all unreachable objects should've been purged.
checkf(!ObjectItem->HasAnyFlags(EInternalObjectFlags::Unreachable | EInternalObjectFlags::PendingConstruction), TEXT("%s"), *Object->GetFullName());
// Keep track of how many objects are around.
ObjectCountDuringMarkPhase++;
if (bWithClusters)
{
ObjectItem->ClearFlags(EInternalObjectFlags::ReachableInCluster);
}
// Special case handling for objects that are part of the root set.
if (ObjectItem->IsRootSet())
{
// IsValidLowLevel is extremely slow in this loop so only do it in debug
checkSlow(Object->IsValidLowLevel());
// We cannot use RF_PendingKill on objects that are part of the root set.
#if DO_GUARD_SLOW
checkCode(if (ObjectItem->IsPendingKill()) { UE_LOG(LogGarbage, Fatal, TEXT("Object %s is part of root set though has been marked RF_PendingKill!"), *Object->GetFullName()); });
#endif
if (bWithClusters)
{
if (ObjectItem->HasAnyFlags(EInternalObjectFlags::ClusterRoot) || ObjectItem->GetOwnerIndex() > 0)
{
KeepClusterRefsList.Push(ObjectItem);
}
}
LocalObjectsToSerialize.Add(Object);
}
// Regular objects or cluster root objects
else if (!bWithClusters || ObjectItem->GetOwnerIndex() <= 0)
{
bool bMarkAsUnreachable = true;
// Internal flags are super fast to check and is used by async loading and must have higher precedence than PendingKill
if (ObjectItem->HasAnyFlags(FastKeepFlags))
{
bMarkAsUnreachable = false;
}
// If KeepFlags is non zero this is going to be very slow due to cache misses
else if (!ObjectItem->IsPendingKill() && KeepFlags != RF_NoFlags && Object->HasAnyFlags(KeepFlags))
{
bMarkAsUnreachable = false;
}
else if (ObjectItem->IsPendingKill() && bWithClusters && ObjectItem->HasAnyFlags(EInternalObjectFlags::ClusterRoot))
{
ClustersToDissolveList.Push(ObjectItem);
}
// Mark objects as unreachable unless they have any of the passed in KeepFlags set and it's not marked for elimination..
if (!bMarkAsUnreachable)
{
// IsValidLowLevel is extremely slow in this loop so only do it in debug
checkSlow(Object->IsValidLowLevel());
LocalObjectsToSerialize.Add(Object);
if (bWithClusters)
{
if (ObjectItem->HasAnyFlags(EInternalObjectFlags::ClusterRoot))
{
KeepClusterRefsList.Push(ObjectItem);
}
}
}
else
{
ObjectItem->SetFlags(EInternalObjectFlags::Unreachable);
}
}
// Cluster objects
else if (bWithClusters && ObjectItem->GetOwnerIndex() > 0)
{
// treat cluster objects with FastKeepFlags the same way as if they are in the root set
if (ObjectItem->HasAnyFlags(FastKeepFlags))
{
KeepClusterRefsList.Push(ObjectItem);
LocalObjectsToSerialize.Add(Object);
}
}
}
}
GObjectCountDuringLastMarkPhase.Add(ObjectCountDuringMarkPhase);
},
!bParallel);
// Collect all objects to serialize from all threads and put them into a single array
{
int32 NumObjectsToSerialize = 0;
for (int32 ThreadIndex = 0; ThreadIndex < NumThreads; ++ThreadIndex)
{
NumObjectsToSerialize += ObjectsToSerializeArrays[ThreadIndex]->ObjectsToSerialize.Num();
}
ObjectsToSerialize.Reserve(NumObjectsToSerialize);
for (int32 ThreadIndex = 0; ThreadIndex < NumThreads; ++ThreadIndex)
{
ObjectsToSerialize.Append(ObjectsToSerializeArrays[ThreadIndex]->ObjectsToSerialize);
FGCArrayPool::Get().ReturnToPool(ObjectsToSerializeArrays[ThreadIndex]);
}
delete[] ObjectsToSerializeArrays;
}
if (bWithClusters)
{
TArray<FUObjectItem*> ClustersToDissolve;
ClustersToDissolveList.PopAll(ClustersToDissolve);
for (FUObjectItem* ObjectItem: ClustersToDissolve)
{
// Check if the object is still a cluster root - it's possible one of the previous
// DissolveClusterAndMarkObjectsAsUnreachable calls already dissolved its cluster
if (ObjectItem->HasAnyFlags(EInternalObjectFlags::ClusterRoot))
{
GUObjectClusters.DissolveClusterAndMarkObjectsAsUnreachable(ObjectItem);
GUObjectClusters.SetClustersNeedDissolving();
}
}
}
if (bWithClusters)
{
TArray<FUObjectItem*> KeepClusterRefs;
KeepClusterRefsList.PopAll(KeepClusterRefs);
for (FUObjectItem* ObjectItem: KeepClusterRefs)
{
if (ObjectItem->GetOwnerIndex() > 0)
{
checkSlow(!ObjectItem->HasAnyFlags(EInternalObjectFlags::ClusterRoot));
bool bNeedsDoing = !ObjectItem->HasAnyFlags(EInternalObjectFlags::ReachableInCluster);
if (bNeedsDoing)
{
ObjectItem->SetFlags(EInternalObjectFlags::ReachableInCluster);
// Make sure cluster root object is reachable too
const int32 OwnerIndex = ObjectItem->GetOwnerIndex();
FUObjectItem* RootObjectItem = GUObjectArray.IndexToObjectUnsafeForGC(OwnerIndex);
checkSlow(RootObjectItem->HasAnyFlags(EInternalObjectFlags::ClusterRoot));
// if it is reachable via keep flags we will do this below (or maybe already have)
if (RootObjectItem->IsUnreachable())
{
RootObjectItem->ClearFlags(EInternalObjectFlags::Unreachable);
// Make sure all referenced clusters are marked as reachable too
FGCReferenceProcessor<EFastReferenceCollectorOptions::WithClusters>::MarkReferencedClustersAsReachable(RootObjectItem->GetClusterIndex(), ObjectsToSerialize);
}
}
}
else
{
checkSlow(ObjectItem->HasAnyFlags(EInternalObjectFlags::ClusterRoot));
// this thing is definitely not marked unreachable, so don't test it here
// Make sure all referenced clusters are marked as reachable too
FGCReferenceProcessor<EFastReferenceCollectorOptions::WithClusters>::MarkReferencedClustersAsReachable(ObjectItem->GetClusterIndex(), ObjectsToSerialize);
}
}
}
}
/**
* Performs reachability analysis.
*
* @param KeepFlags Objects with these flags will be kept regardless of being referenced or not
*/
void PerformReachabilityAnalysis(EObjectFlags KeepFlags, bool bForceSingleThreaded, bool bWithClusters)
{
LLM_SCOPE(ELLMTag::GC);
SCOPED_NAMED_EVENT(FRealtimeGC_PerformReachabilityAnalysis, FColor::Red);
DECLARE_SCOPE_CYCLE_COUNTER(TEXT("FRealtimeGC::PerformReachabilityAnalysis"), STAT_FArchiveRealtimeGC_PerformReachabilityAnalysis, STATGROUP_GC);
/** Growing array of objects that require serialization */
FGCArrayStruct* ArrayStruct = FGCArrayPool::Get().GetArrayStructFromPool();
TArray<UObject*>& ObjectsToSerialize = ArrayStruct->ObjectsToSerialize;
// Reset object count.
GObjectCountDuringLastMarkPhase.Reset();
// Make sure GC referencer object is checked for references to other objects even if it resides in permanent object pool
if (FPlatformProperties::RequiresCookedData() && FGCObject::GGCObjectReferencer && GUObjectArray.IsDisregardForGC(FGCObject::GGCObjectReferencer))
{
ObjectsToSerialize.Add(FGCObject::GGCObjectReferencer);
}
{
const double StartTime = FPlatformTime::Seconds();
(this->*MarkObjectsFunctions[GetGCFunctionIndex(!bForceSingleThreaded, bWithClusters)])(ObjectsToSerialize, KeepFlags);
UE_LOG(LogGarbage, Verbose, TEXT("%f ms for MarkObjectsAsUnreachable Phase (%d Objects To Serialize)"), (FPlatformTime::Seconds() - StartTime) * 1000, ObjectsToSerialize.Num());
}
{
const double StartTime = FPlatformTime::Seconds();
PerformReachabilityAnalysisOnObjects(ArrayStruct, bForceSingleThreaded, bWithClusters);
UE_LOG(LogGarbage, Verbose, TEXT("%f ms for Reachability Analysis"), (FPlatformTime::Seconds() - StartTime) * 1000);
}
// Allowing external systems to add object roots. This can't be done through AddReferencedObjects
// because it may require tracing objects (via FGarbageCollectionTracer) multiple times
FCoreUObjectDelegates::TraceExternalRootsForReachabilityAnalysis.Broadcast(*this, KeepFlags, bForceSingleThreaded);
FGCArrayPool::Get().ReturnToPool(ArrayStruct);
#if UE_BUILD_DEBUG
FGCArrayPool::Get().CheckLeaks();
#endif
}
virtual void PerformReachabilityAnalysisOnObjects(FGCArrayStruct* ArrayStruct, bool bForceSingleThreaded, bool bWithClusters) override
{
(this->*ReachabilityAnalysisFunctions[GetGCFunctionIndex(!bForceSingleThreaded, bWithClusters)])(ArrayStruct);
}
};
#endif // UE_WITH_GC
// Allow parallel GC to be overridden to single threaded via console command.
static int32 GAllowParallelGC = 1;
static FAutoConsoleVariableRef CVarAllowParallelGC(
TEXT("gc.AllowParallelGC"),
GAllowParallelGC,
TEXT("Used to control parallel GC."),
ECVF_Default);
/** Returns true if Garbage Collection should be forced to run on a single thread */
static bool ShouldForceSingleThreadedGC()
{
const bool bForceSingleThreadedGC = !FApp::ShouldUseThreadingForPerformance() || !FPlatformProcess::SupportsMultithreading() ||
#if PLATFORM_SUPPORTS_MULTITHREADED_GC
(FPlatformMisc::NumberOfCores() < 2 || GAllowParallelGC == 0 || PERF_DETAILED_PER_CLASS_GC_STATS);
#else // PLATFORM_SUPPORTS_MULTITHREADED_GC
true;
#endif // PLATFORM_SUPPORTS_MULTITHREADED_GC
return bForceSingleThreadedGC;
}
void AcquireGCLock()
{
const double StartTime = FPlatformTime::Seconds();
FGCCSyncObject::Get().GCLock();
const double ElapsedTime = FPlatformTime::Seconds() - StartTime;
if (FPlatformProperties::RequiresCookedData() && ElapsedTime > 0.001)
{
UE_LOG(LogGarbage, Warning, TEXT("%f ms for acquiring GC lock"), ElapsedTime * 1000);
}
}
void ReleaseGCLock()
{
FGCCSyncObject::Get().GCUnlock();
}
/** Locks GC within a scope but only if it hasn't been locked already */
struct FConditionalGCLock
{
bool bNeedsUnlock;
FConditionalGCLock()
: bNeedsUnlock(false)
{
if (!FGCCSyncObject::Get().IsGCLocked())
{
AcquireGCLock();
bNeedsUnlock = true;
}
}
~FConditionalGCLock()
{
if (bNeedsUnlock)
{
ReleaseGCLock();
}
}
};
static bool IncrementalDestroyGarbage(bool bUseTimeLimit, float TimeLimit);
/**
* Incrementally purge garbage by deleting all unreferenced objects after routing Destroy.
*
* Calling code needs to be EXTREMELY careful when and how to call this function as
* RF_Unreachable cannot change on any objects unless any pending purge has completed!
*
* @param bUseTimeLimit whether the time limit parameter should be used
* @param TimeLimit soft time limit for this function call
*/
void IncrementalPurgeGarbage(bool bUseTimeLimit, float TimeLimit)
{
#if !UE_WITH_GC
return;
#else
SCOPED_NAMED_EVENT(IncrementalPurgeGarbage, FColor::Red);
DECLARE_SCOPE_CYCLE_COUNTER(TEXT("IncrementalPurgeGarbage"), STAT_IncrementalPurgeGarbage, STATGROUP_GC);
CSV_SCOPED_TIMING_STAT_EXCLUSIVE(GarbageCollection);
if (GExitPurge)
{
GObjPurgeIsRequired = true;
GUObjectArray.DisableDisregardForGC();
GObjCurrentPurgeObjectIndexNeedsReset = true;
}
// Early out if there is nothing to do.
if (!GObjPurgeIsRequired)
{
return;
}
bool bCompleted = false;
struct FResetPurgeProgress
{
bool& bCompletedRef;
FResetPurgeProgress(bool& bInCompletedRef)
: bCompletedRef(bInCompletedRef)
{
// Incremental purge is now in progress.
GObjIncrementalPurgeIsInProgress = true;
FPlatformMisc::MemoryBarrier();
}
~FResetPurgeProgress()
{
if (bCompletedRef)
{
GObjIncrementalPurgeIsInProgress = false;
FPlatformMisc::MemoryBarrier();
}
}
} ResetPurgeProgress(bCompleted);
{
// Lock before settting GCStartTime as it could be slow to lock if async loading is in progress
// but we still want to perform some GC work otherwise we'd be keeping objects in memory for a long time
FConditionalGCLock ScopedGCLock;
// Keep track of start time to enforce time limit unless bForceFullPurge is true;
GCStartTime = FPlatformTime::Seconds();
bool bTimeLimitReached = false;
if (GUnrechableObjectIndex < GUnreachableObjects.Num())
{
bTimeLimitReached = UnhashUnreachableObjects(bUseTimeLimit, TimeLimit);
if (GUnrechableObjectIndex >= GUnreachableObjects.Num())
{
FScopedCBDProfile::DumpProfile();
}
}
if (!bTimeLimitReached)
{
bCompleted = IncrementalDestroyGarbage(bUseTimeLimit, TimeLimit);
}
}
#endif // !UE_WITH_GC
}
#if UE_WITH_GC
bool IncrementalDestroyGarbage(bool bUseTimeLimit, float TimeLimit)
{
const bool bMultithreadedPurge = !ShouldForceSingleThreadedGC() && GMultithreadedDestructionEnabled;
if (!GAsyncPurge)
{
GAsyncPurge = new FAsyncPurge(bMultithreadedPurge);
}
else if (GAsyncPurge->IsMultithreaded() != bMultithreadedPurge)
{
check(GAsyncPurge->IsFinished());
delete GAsyncPurge;
GAsyncPurge = new FAsyncPurge(bMultithreadedPurge);
}
bool bCompleted = false;
bool bTimeLimitReached = false;
// Keep track of time it took to destroy objects for stats
double IncrementalDestroyGarbageStartTime = FPlatformTime::Seconds();
// Depending on platform FPlatformTime::Seconds might take a noticeable amount of time if called thousands of times so we avoid
// enforcing the time limit too often, especially as neither Destroy nor actual deletion should take significant
// amounts of time.
const int32 TimeLimitEnforcementGranularityForDestroy = 10;
const int32 TimeLimitEnforcementGranularityForDeletion = 100;
// Set 'I'm garbage collecting' flag - might be checked inside UObject::Destroy etc.
FGCScopeLock GCLock;
if (!GObjFinishDestroyHasBeenRoutedToAllObjects && !bTimeLimitReached)
{
check(GUnrechableObjectIndex >= GUnreachableObjects.Num());
// Try to dispatch all FinishDestroy messages to unreachable objects. We'll iterate over every
// single object and destroy any that are ready to be destroyed. The objects that aren't yet
// ready will be added to a list to be processed afterwards.
int32 TimeLimitTimePollCounter = 0;
int32 FinishDestroyTimePollCounter = 0;
if (GObjCurrentPurgeObjectIndexNeedsReset)
{
GObjCurrentPurgeObjectIndex = 0;
GObjCurrentPurgeObjectIndexNeedsReset = false;
}
while (GObjCurrentPurgeObjectIndex < GUnreachableObjects.Num())
{
FUObjectItem* ObjectItem = GUnreachableObjects[GObjCurrentPurgeObjectIndex];
checkSlow(ObjectItem);
//@todo UE4 - A prefetch was removed here. Re-add it. It wasn't right anyway, since it was ten items ahead and the consoles on have 8 prefetch slots
check(ObjectItem->IsUnreachable());
{
UObject* Object = static_cast<UObject*>(ObjectItem->Object);
// Object should always have had BeginDestroy called on it and never already be destroyed
check(Object->HasAnyFlags(RF_BeginDestroyed) && !Object->HasAnyFlags(RF_FinishDestroyed));
// Only proceed with destroying the object if the asynchronous cleanup started by BeginDestroy has finished.
if (Object->IsReadyForFinishDestroy())
{
#if PERF_DETAILED_PER_CLASS_GC_STATS
// Keep track of how many objects of a certain class we're purging.
const FName& ClassName = Object->GetClass()->GetFName();
int32 InstanceCount = GClassToPurgeCountMap.FindRef(ClassName);
GClassToPurgeCountMap.Add(ClassName, ++InstanceCount);
#endif
// Send FinishDestroy message.
Object->ConditionalFinishDestroy();
}
else
{
// The object isn't ready for FinishDestroy to be called yet. This is common in the
// case of a graphics resource that is waiting for the render thread "release fence"
// to complete. Just calling IsReadyForFinishDestroy may begin the process of releasing
// a resource, so we don't want to block iteration while waiting on the render thread.
// Add the object index to our list of objects to revisit after we process everything else
GGCObjectsPendingDestruction.Add(Object);
GGCObjectsPendingDestructionCount++;
}
}
// We've processed the object so increment our global iterator. It's important to do this before
// we test for the time limit so that we don't process the same object again next tick!
++GObjCurrentPurgeObjectIndex;
// Only check time limit every so often to avoid calling FPlatformTime::Seconds too often.
const bool bPollTimeLimit = ((TimeLimitTimePollCounter++) % TimeLimitEnforcementGranularityForDestroy == 0);
if (bUseTimeLimit && bPollTimeLimit && ((FPlatformTime::Seconds() - GCStartTime) > TimeLimit))
{
bTimeLimitReached = true;
break;
}
}
// Have we finished the first round of attempting to call FinishDestroy on unreachable objects?
if (GObjCurrentPurgeObjectIndex >= GUnreachableObjects.Num())
{
double MaxTimeForFinishDestroy = 10.00;
bool bFinishDestroyTimeExtended = false;
FString FirstObjectNotReadyWhenTimeExtended;
int32 StartObjectsPendingDestructionCount = GGCObjectsPendingDestructionCount;
// We've finished iterating over all unreachable objects, but we need still need to handle
// objects that were deferred.
int32 LastLoopObjectsPendingDestructionCount = GGCObjectsPendingDestructionCount;
while (GGCObjectsPendingDestructionCount > 0)
{
int32 CurPendingObjIndex = 0;
while (CurPendingObjIndex < GGCObjectsPendingDestructionCount)
{
// Grab the actual object for the current pending object list iteration
UObject* Object = GGCObjectsPendingDestruction[CurPendingObjIndex];
// Object should never have been added to the list if it failed this criteria
check(Object != NULL && Object->IsUnreachable());
// Object should always have had BeginDestroy called on it and never already be destroyed
check(Object->HasAnyFlags(RF_BeginDestroyed) && !Object->HasAnyFlags(RF_FinishDestroyed));
// Only proceed with destroying the object if the asynchronous cleanup started by BeginDestroy has finished.
if (Object->IsReadyForFinishDestroy())
{
#if PERF_DETAILED_PER_CLASS_GC_STATS
// Keep track of how many objects of a certain class we're purging.
const FName& ClassName = Object->GetClass()->GetFName();
int32 InstanceCount = GClassToPurgeCountMap.FindRef(ClassName);
GClassToPurgeCountMap.Add(ClassName, ++InstanceCount);
#endif
// Send FinishDestroy message.
Object->ConditionalFinishDestroy();
// Remove the object index from our list quickly (by swapping with the last object index).
// NOTE: This is much faster than calling TArray.RemoveSwap and avoids shrinking allocations
{
// Swap the last index into the current index
GGCObjectsPendingDestruction[CurPendingObjIndex] = GGCObjectsPendingDestruction[GGCObjectsPendingDestructionCount - 1];
// Decrement the object count
GGCObjectsPendingDestructionCount--;
}
}
else
{
// We'll revisit this object the next time around. Move on to the next.
CurPendingObjIndex++;
}
// Only check time limit every so often to avoid calling FPlatformTime::Seconds too often.
const bool bPollTimeLimit = ((TimeLimitTimePollCounter++) % TimeLimitEnforcementGranularityForDestroy == 0);
if (bUseTimeLimit && bPollTimeLimit && ((FPlatformTime::Seconds() - GCStartTime) > TimeLimit))
{
bTimeLimitReached = true;
break;
}
}
if (bUseTimeLimit)
{
// A time limit is set and we've completed a full iteration over all leftover objects, so
// go ahead and bail out even if we have more time left or objects left to process. It's
// likely in this case that we're waiting for the render thread.
break;
}
else if (GGCObjectsPendingDestructionCount > 0)
{
if (FPlatformProperties::RequiresCookedData())
{
const bool bPollTimeLimit = ((FinishDestroyTimePollCounter++) % TimeLimitEnforcementGranularityForDestroy == 0);
#if PLATFORM_IOS || PLATFORM_ANDROID
if (bPollTimeLimit && !bFinishDestroyTimeExtended && (FPlatformTime::Seconds() - GCStartTime) > MaxTimeForFinishDestroy)
{
MaxTimeForFinishDestroy = 30.0;
bFinishDestroyTimeExtended = true;
#if USE_HITCH_DETECTION
GHitchDetected = true;
#endif
FirstObjectNotReadyWhenTimeExtended = GetFullNameSafe(GGCObjectsPendingDestruction[0]);
}
else
#endif
// Check if we spent too much time on waiting for FinishDestroy without making any progress
if (LastLoopObjectsPendingDestructionCount == GGCObjectsPendingDestructionCount && bPollTimeLimit &&
((FPlatformTime::Seconds() - GCStartTime) > MaxTimeForFinishDestroy))
{
UE_LOG(LogGarbage, Warning, TEXT("Spent more than %.2fs on routing FinishDestroy to objects (objects in queue: %d)"), MaxTimeForFinishDestroy, GGCObjectsPendingDestructionCount);
UObject* LastObjectNotReadyForFinishDestroy = nullptr;
for (int32 ObjectIndex = 0; ObjectIndex < GGCObjectsPendingDestructionCount; ++ObjectIndex)
{
UObject* Obj = GGCObjectsPendingDestruction[ObjectIndex];
bool bReady = Obj->IsReadyForFinishDestroy();
UE_LOG(LogGarbage, Warning, TEXT(" [%d]: %s, IsReadyForFinishDestroy: %s"),
ObjectIndex,
*GetFullNameSafe(Obj),
bReady ? TEXT("true") : TEXT("false"));
if (!bReady)
{
LastObjectNotReadyForFinishDestroy = Obj;
}
}
#if PLATFORM_DESKTOP
ensureMsgf(0, TEXT("Spent to much time waiting for FinishDestroy for %d object(s) (last object: %s), check log for details"),
GGCObjectsPendingDestructionCount,
*GetFullNameSafe(LastObjectNotReadyForFinishDestroy));
#else
// for non-desktop platforms, make this a warning so that we can die inside of an object member call.
// this will give us a greater chance of getting useful memory inside of the platform minidump.
UE_LOG(LogGarbage, Warning, TEXT("Spent to much time waiting for FinishDestroy for %d object(s) (last object: %s), check log for details"),
GGCObjectsPendingDestructionCount,
*GetFullNameSafe(LastObjectNotReadyForFinishDestroy));
if (LastObjectNotReadyForFinishDestroy)
{
LastObjectNotReadyForFinishDestroy->AbortInsideMemberFunction();
}
else
{
// go through the standard fatal error path if LastObjectNotReadyForFinishDestroy is null.
// this could happen in the current code flow, in the odd case where an object finished readying just in time for the loop above.
UE_LOG(LogGarbage, Fatal, TEXT("LastObjectNotReadyForFinishDestroy is NULL."));
}
#endif
}
}
// Sleep before the next pass to give the render thread some time to release fences.
FPlatformProcess::Sleep(0);
}
LastLoopObjectsPendingDestructionCount = GGCObjectsPendingDestructionCount;
}
// Have all objects been destroyed now?
if (GGCObjectsPendingDestructionCount == 0)
{
if (bFinishDestroyTimeExtended)
{
FString Msg = FString::Printf(TEXT("Additional time was required to finish routing FinishDestroy, spent %.2fs on routing FinishDestroy to %d objects. 1st obj not ready: '%s'."), (FPlatformTime::Seconds() - GCStartTime), StartObjectsPendingDestructionCount, *FirstObjectNotReadyWhenTimeExtended);
UE_LOG(LogGarbage, Warning, TEXT("%s"), *Msg);
FCoreDelegates::OnGCFinishDestroyTimeExtended.Broadcast(Msg);
}
// Release memory we used for objects pending destruction, leaving some slack space
GGCObjectsPendingDestruction.Empty(256);
// Destroy has been routed to all objects so it's safe to delete objects now.
GObjFinishDestroyHasBeenRoutedToAllObjects = true;
GObjCurrentPurgeObjectIndexNeedsReset = true;
}
}
}
if (GObjFinishDestroyHasBeenRoutedToAllObjects && !bTimeLimitReached)
{
// Perform actual object deletion.
int32 ProcessCount = 0;
if (GObjCurrentPurgeObjectIndexNeedsReset)
{
GAsyncPurge->BeginPurge();
// Reset the reset flag but don't reset the actual index yet for stat purposes
GObjCurrentPurgeObjectIndexNeedsReset = false;
}
GAsyncPurge->TickPurge(bUseTimeLimit, TimeLimit, GCStartTime);
if (GAsyncPurge->IsFinished())
{
#if UE_BUILD_DEBUG
GAsyncPurge->VerifyAllObjectsDestroyed();
#endif
bCompleted = true;
// Incremental purge is finished, time to reset variables.
GObjFinishDestroyHasBeenRoutedToAllObjects = false;
GObjPurgeIsRequired = false;
GObjCurrentPurgeObjectIndexNeedsReset = true;
// Log status information.
const int32 PurgedObjectCountSinceLastMarkPhase = GAsyncPurge->GetObjectsDestroyedSinceLastMarkPhase();
UE_LOG(LogGarbage, Log, TEXT("GC purged %i objects (%i -> %i) in %.3fms"), PurgedObjectCountSinceLastMarkPhase,
GObjectCountDuringLastMarkPhase.GetValue(),
GObjectCountDuringLastMarkPhase.GetValue() - PurgedObjectCountSinceLastMarkPhase,
(FPlatformTime::Seconds() - IncrementalDestroyGarbageStartTime) * 1000);
#if PERF_DETAILED_PER_CLASS_GC_STATS
LogClassCountInfo(TEXT("objects of"), GClassToPurgeCountMap, 10, PurgedObjectCountSinceLastMarkPhase);
#endif
GAsyncPurge->ResetObjectsDestroyedSinceLastMarkPhase();
}
}
if (bUseTimeLimit && !bCompleted)
{
UE_LOG(LogGarbage, Log, TEXT("%.3f ms for incrementally purging unreachable objects (FinishDestroyed: %d, Destroyed: %d / %d)"),
(FPlatformTime::Seconds() - IncrementalDestroyGarbageStartTime) * 1000,
GObjCurrentPurgeObjectIndex,
GAsyncPurge->GetObjectsDestroyedSinceLastMarkPhase(),
GUnreachableObjects.Num());
}
return bCompleted;
}
#endif // UE_WITH_GC
/**
* Returns whether an incremental purge is still pending/ in progress.
*
* @return true if incremental purge needs to be kicked off or is currently in progress, false othwerise.
*/
bool IsIncrementalPurgePending()
{
return GObjIncrementalPurgeIsInProgress || GObjPurgeIsRequired;
}
// This counts how many times GC was skipped
static int32 GNumAttemptsSinceLastGC = 0;
// Number of times GC can be skipped.
static int32 GNumRetriesBeforeForcingGC = 10;
static FAutoConsoleVariableRef CVarNumRetriesBeforeForcingGC(
TEXT("gc.NumRetriesBeforeForcingGC"),
GNumRetriesBeforeForcingGC,
TEXT("Maximum number of times GC can be skipped if worker threads are currently modifying UObject state."),
ECVF_Default);
// Force flush streaming on GC console variable
static int32 GFlushStreamingOnGC = 0;
static FAutoConsoleVariableRef CVarFlushStreamingOnGC(
TEXT("gc.FlushStreamingOnGC"),
GFlushStreamingOnGC,
TEXT("If enabled, streaming will be flushed each time garbage collection is triggered."),
ECVF_Default);
void GatherUnreachableObjects(bool bForceSingleThreaded)
{
DECLARE_SCOPE_CYCLE_COUNTER(TEXT("CollectGarbageInternal.GatherUnreachableObjects"), STAT_CollectGarbageInternal_GatherUnreachableObjects, STATGROUP_GC);
const double StartTime = FPlatformTime::Seconds();
GUnreachableObjects.Reset();
GUnrechableObjectIndex = 0;
int32 MaxNumberOfObjects = GUObjectArray.GetObjectArrayNum() - (GExitPurge ? 0 : GUObjectArray.GetFirstGCIndex());
int32 NumThreads = FMath::Max(1, FTaskGraphInterface::Get().GetNumWorkerThreads());
int32 NumberOfObjectsPerThread = (MaxNumberOfObjects / NumThreads) + 1;
TArray<FUObjectItem*> ClusterItemsToDestroy;
int32 ClusterObjects = 0;
// Iterate over all objects. Note that we iterate over the UObjectArray and usually check only internal flags which
// are part of the array so we don't suffer from cache misses as much as we would if we were to check ObjectFlags.
ParallelFor(NumThreads, [&ClusterItemsToDestroy, NumberOfObjectsPerThread, NumThreads, MaxNumberOfObjects](int32 ThreadIndex)
{
int32 FirstObjectIndex = ThreadIndex * NumberOfObjectsPerThread + (GExitPurge ? 0 : GUObjectArray.GetFirstGCIndex());
int32 NumObjects = (ThreadIndex < (NumThreads - 1)) ? NumberOfObjectsPerThread : (MaxNumberOfObjects - (NumThreads - 1) * NumberOfObjectsPerThread);
int32 LastObjectIndex = FMath::Min(GUObjectArray.GetObjectArrayNum() - 1, FirstObjectIndex + NumObjects - 1);
TArray<FUObjectItem*> ThisThreadUnreachableObjects;
TArray<FUObjectItem*> ThisThreadClusterItemsToDestroy;
for (int32 ObjectIndex = FirstObjectIndex; ObjectIndex <= LastObjectIndex; ++ObjectIndex)
{
FUObjectItem* ObjectItem = &GUObjectArray.GetObjectItemArrayUnsafe()[ObjectIndex];
if (ObjectItem->IsUnreachable())
{
ThisThreadUnreachableObjects.Add(ObjectItem);
if (ObjectItem->HasAnyFlags(EInternalObjectFlags::ClusterRoot))
{
// We can't mark cluster objects as unreachable here as they may be currently being processed on another thread
ThisThreadClusterItemsToDestroy.Add(ObjectItem);
}
}
}
if (ThisThreadUnreachableObjects.Num())
{
FScopeLock UnreachableObjectsLock(&GUnreachableObjectsCritical);
GUnreachableObjects.Append(ThisThreadUnreachableObjects);
ClusterItemsToDestroy.Append(ThisThreadClusterItemsToDestroy);
}
},
bForceSingleThreaded);
{
// @todo: if GUObjectClusters.FreeCluster() was thread safe we could do this in parallel too
for (FUObjectItem* ClusterRootItem: ClusterItemsToDestroy)
{
#if UE_GCCLUSTER_VERBOSE_LOGGING
UE_LOG(LogGarbage, Log, TEXT("Destroying cluster (%d) %s"), ClusterRootItem->GetClusterIndex(), *static_cast<UObject*>(ClusterRootItem->Object)->GetFullName());
#endif
ClusterRootItem->ClearFlags(EInternalObjectFlags::ClusterRoot);
const int32 ClusterIndex = ClusterRootItem->GetClusterIndex();
FUObjectCluster& Cluster = GUObjectClusters[ClusterIndex];
for (int32 ClusterObjectIndex: Cluster.Objects)
{
FUObjectItem* ClusterObjectItem = GUObjectArray.IndexToObjectUnsafeForGC(ClusterObjectIndex);
ClusterObjectItem->SetOwnerIndex(0);
if (!ClusterObjectItem->HasAnyFlags(EInternalObjectFlags::ReachableInCluster))
{
ClusterObjectItem->SetFlags(EInternalObjectFlags::Unreachable);
ClusterObjects++;
GUnreachableObjects.Add(ClusterObjectItem);
}
}
GUObjectClusters.FreeCluster(ClusterIndex);
}
}
UE_LOG(LogGarbage, Log, TEXT("%f ms for Gather Unreachable Objects (%d objects collected including %d cluster objects from %d clusters)"),
(FPlatformTime::Seconds() - StartTime) * 1000,
GUnreachableObjects.Num(),
ClusterObjects,
ClusterItemsToDestroy.Num());
}
/**
* Deletes all unreferenced objects, keeping objects that have any of the passed in KeepFlags set
*
* @param KeepFlags objects with those flags will be kept regardless of being referenced or not
* @param bPerformFullPurge if true, perform a full purge after the mark pass
*/
void CollectGarbageInternal(EObjectFlags KeepFlags, bool bPerformFullPurge)
{
#if !UE_WITH_GC
return;
#else
if (GIsInitialLoad)
{
// During initial load classes may not yet have their GC token streams assembled
UE_LOG(LogGarbage, Log, TEXT("Skipping CollectGarbage() call during initial load. It's not safe."));
return;
}
SCOPE_TIME_GUARD(TEXT("Collect Garbage"));
SCOPED_NAMED_EVENT(CollectGarbageInternal, FColor::Red);
CSV_EVENT_GLOBAL(TEXT("GC"));
CSV_SCOPED_TIMING_STAT_EXCLUSIVE(GarbageCollection);
LLM_SCOPE(ELLMTag::GC);
FGCCSyncObject::Get().ResetGCIsWaiting();
#if defined(WITH_CODE_GUARD_HANDLER) && WITH_CODE_GUARD_HANDLER
void CheckImageIntegrityAtRuntime();
CheckImageIntegrityAtRuntime();
#endif
DECLARE_SCOPE_CYCLE_COUNTER(TEXT("CollectGarbageInternal"), STAT_CollectGarbageInternal, STATGROUP_GC);
STAT_ADD_CUSTOMMESSAGE_NAME(STAT_NamedMarker, TEXT("GarbageCollection - Begin"));
// We can't collect garbage while there's a load in progress. E.g. one potential issue is Import.XObject
check(!IsLoading());
// Reset GC skip counter
GNumAttemptsSinceLastGC = 0;
// Flush streaming before GC if requested
if (GFlushStreamingOnGC)
{
if (IsAsyncLoading())
{
UE_LOG(LogGarbage, Log, TEXT("CollectGarbageInternal() is flushing async loading"));
}
FGCCSyncObject::Get().GCUnlock();
FlushAsyncLoading();
FGCCSyncObject::Get().GCLock();
}
// Route callbacks so we can ensure that we are e.g. not in the middle of loading something by flushing
// the async loading, etc...
FCoreUObjectDelegates::GetPreGarbageCollectDelegate().Broadcast();
GLastGCFrame = GFrameCounter;
{
// Set 'I'm garbage collecting' flag - might be checked inside various functions.
// This has to be unlocked before we call post GC callbacks
FGCScopeLock GCLock;
UE_LOG(LogGarbage, Log, TEXT("Collecting garbage%s"), IsAsyncLoading() ? TEXT(" while async loading") : TEXT(""));
// Make sure previous incremental purge has finished or we do a full purge pass in case we haven't kicked one
// off yet since the last call to garbage collection.
if (GObjIncrementalPurgeIsInProgress || GObjPurgeIsRequired)
{
IncrementalPurgeGarbage(false);
FMemory::Trim();
}
check(!GObjIncrementalPurgeIsInProgress);
check(!GObjPurgeIsRequired);
// This can happen if someone disables clusters from the console (gc.CreateGCClusters)
if (!GCreateGCClusters && GUObjectClusters.GetNumAllocatedClusters())
{
GUObjectClusters.DissolveClusters(true);
}
#if VERIFY_DISREGARD_GC_ASSUMPTIONS
// Only verify assumptions if option is enabled. This avoids false positives in the Editor or commandlets.
if ((GUObjectArray.DisregardForGCEnabled() || GUObjectClusters.GetNumAllocatedClusters()) && GShouldVerifyGCAssumptions)
{
DECLARE_SCOPE_CYCLE_COUNTER(TEXT("CollectGarbageInternal.VerifyGCAssumptions"), STAT_CollectGarbageInternal_VerifyGCAssumptions, STATGROUP_GC);
const double StartTime = FPlatformTime::Seconds();
VerifyGCAssumptions();
VerifyClustersAssumptions();
UE_LOG(LogGarbage, Log, TEXT("%f ms for Verify GC Assumptions"), (FPlatformTime::Seconds() - StartTime) * 1000);
}
#endif
// Fall back to single threaded GC if processor count is 1 or parallel GC is disabled
// or detailed per class gc stats are enabled (not thread safe)
// Temporarily forcing single-threaded GC in the editor until Modify() can be safely removed from HandleObjectReference.
const bool bForceSingleThreadedGC = ShouldForceSingleThreadedGC();
// Run with GC clustering code enabled only if clustering is enabled and there's actual allocated clusters
const bool bWithClusters = !!GCreateGCClusters && GUObjectClusters.GetNumAllocatedClusters();
// Perform reachability analysis.
{
const double StartTime = FPlatformTime::Seconds();
FRealtimeGC TagUsedRealtimeGC;
TagUsedRealtimeGC.PerformReachabilityAnalysis(KeepFlags, bForceSingleThreadedGC, bWithClusters);
UE_LOG(LogGarbage, Log, TEXT("%f ms for GC"), (FPlatformTime::Seconds() - StartTime) * 1000);
}
// Reconstruct clusters if needed
if (GUObjectClusters.ClustersNeedDissolving())
{
const double StartTime = FPlatformTime::Seconds();
GUObjectClusters.DissolveClusters();
UE_LOG(LogGarbage, Log, TEXT("%f ms for dissolving GC clusters"), (FPlatformTime::Seconds() - StartTime) * 1000);
}
// Fire post-reachability analysis hooks
FCoreUObjectDelegates::PostReachabilityAnalysis.Broadcast();
{
GatherUnreachableObjects(bForceSingleThreadedGC);
NotifyUnreachableObjects(GUnreachableObjects);
// This needs to happen after GatherUnreachableObjects since GatherUnreachableObjects can mark more (clustered) objects as unreachable
FGCArrayPool::Get().ClearWeakReferences(bPerformFullPurge);
if (bPerformFullPurge || !GIncrementalBeginDestroyEnabled)
{
UnhashUnreachableObjects(/**bUseTimeLimit = */ false);
FScopedCBDProfile::DumpProfile();
}
}
// Set flag to indicate that we are relying on a purge to be performed.
GObjPurgeIsRequired = true;
// Perform a full purge by not using a time limit for the incremental purge. The Editor always does a full purge.
if (bPerformFullPurge || GIsEditor)
{
IncrementalPurgeGarbage(false);
}
if (bPerformFullPurge)
{
ShrinkUObjectHashTables();
}
// Destroy all pending delete linkers
DeleteLoaders();
// Trim allocator memory
FMemory::Trim();
}
// Route callbacks to verify GC assumptions
FCoreUObjectDelegates::GetPostGarbageCollect().Broadcast();
STAT_ADD_CUSTOMMESSAGE_NAME(STAT_NamedMarker, TEXT("GarbageCollection - End"));
#endif // UE_WITH_GC
}
bool IsIncrementalUnhashPending()
{
return GUnrechableObjectIndex < GUnreachableObjects.Num();
}
bool UnhashUnreachableObjects(bool bUseTimeLimit, float TimeLimit)
{
DECLARE_SCOPE_CYCLE_COUNTER(TEXT("UnhashUnreachableObjects"), STAT_UnhashUnreachableObjects, STATGROUP_GC);
TGuardValue<bool> GuardObjUnhashUnreachableIsInProgress(GObjUnhashUnreachableIsInProgress, true);
FCoreUObjectDelegates::PreGarbageCollectConditionalBeginDestroy.Broadcast();
// Unhash all unreachable objects.
const double StartTime = FPlatformTime::Seconds();
const int32 TimeLimitEnforcementGranularityForBeginDestroy = 10;
int32 Items = 0;
int32 TimePollCounter = 0;
const bool bFirstIteration = (GUnrechableObjectIndex == 0);
while (GUnrechableObjectIndex < GUnreachableObjects.Num())
{
//@todo UE4 - A prefetch was removed here. Re-add it. It wasn't right anyway, since it was ten items ahead and the consoles on have 8 prefetch slots
FUObjectItem* ObjectItem = GUnreachableObjects[GUnrechableObjectIndex++];
{
UObject* Object = static_cast<UObject*>(ObjectItem->Object);
FScopedCBDProfile Profile(Object);
// Begin the object's asynchronous destruction.
Object->ConditionalBeginDestroy();
}
Items++;
const bool bPollTimeLimit = ((TimePollCounter++) % TimeLimitEnforcementGranularityForBeginDestroy == 0);
if (bUseTimeLimit && bPollTimeLimit && ((FPlatformTime::Seconds() - StartTime) > TimeLimit))
{
break;
}
}
const bool bTimeLimitReached = (GUnrechableObjectIndex < GUnreachableObjects.Num());
if (!bUseTimeLimit)
{
UE_LOG(LogGarbage, Log, TEXT("%f ms for %sunhashing unreachable objects (%d objects unhashed)"),
(FPlatformTime::Seconds() - StartTime) * 1000,
bUseTimeLimit ? TEXT("incrementally ") : TEXT(""),
Items,
GUnrechableObjectIndex, GUnreachableObjects.Num());
}
else if (!bTimeLimitReached)
{
// When doing incremental unhashing log only the first and last iteration (this was the last one)
UE_LOG(LogGarbage, Log, TEXT("Finished unhashing unreachable objects (%d objects unhashed)."), GUnreachableObjects.Num());
}
else if (bFirstIteration)
{
// When doing incremental unhashing log only the first and last iteration (this was the first one)
UE_LOG(LogGarbage, Log, TEXT("Starting unhashing unreachable objects (%d objects to unhash)."), GUnreachableObjects.Num());
}
FCoreUObjectDelegates::PostGarbageCollectConditionalBeginDestroy.Broadcast();
// Return true if time limit has been reached
return bTimeLimitReached;
}
void CollectGarbage(EObjectFlags KeepFlags, bool bPerformFullPurge)
{
// No other thread may be performing UObject operations while we're running
AcquireGCLock();
// Perform actual garbage collection
CollectGarbageInternal(KeepFlags, bPerformFullPurge);
// Other threads are free to use UObjects
ReleaseGCLock();
}
bool TryCollectGarbage(EObjectFlags KeepFlags, bool bPerformFullPurge)
{
// No other thread may be performing UObject operations while we're running
bool bCanRunGC = FGCCSyncObject::Get().TryGCLock();
if (!bCanRunGC)
{
if (GNumRetriesBeforeForcingGC > 0 && GNumAttemptsSinceLastGC > GNumRetriesBeforeForcingGC)
{
// Force GC and block main thread
UE_LOG(LogGarbage, Warning, TEXT("TryCollectGarbage: forcing GC after %d skipped attempts."), GNumAttemptsSinceLastGC);
GNumAttemptsSinceLastGC = 0;
AcquireGCLock();
bCanRunGC = true;
}
}
if (bCanRunGC)
{
// Perform actual garbage collection
CollectGarbageInternal(KeepFlags, bPerformFullPurge);
// Other threads are free to use UObjects
ReleaseGCLock();
}
else
{
GNumAttemptsSinceLastGC++;
}
return bCanRunGC;
}
void UObject::CallAddReferencedObjects(FReferenceCollector& Collector)
{
GetClass()->CallAddReferencedObjects(this, Collector);
}
void UObject::AddReferencedObjects(UObject* This, FReferenceCollector& Collector)
{
#if WITH_EDITOR
//@todo UE4 - This seems to be required and it should not be. Seems to be related to the texture streamer.
FLinkerLoad* LinkerLoad = This->GetLinker();
if (LinkerLoad)
{
LinkerLoad->AddReferencedObjects(Collector);
}
// Required by the unified GC when running in the editor
if (GIsEditor)
{
UObject* LoadOuter = This->GetOuter();
UClass* Class = This->GetClass();
UPackage* Package = This->GetExternalPackageInternal();
Collector.AllowEliminatingReferences(false);
Collector.AddReferencedObject(LoadOuter, This);
Collector.AddReferencedObject(Package, This);
Collector.AllowEliminatingReferences(true);
Collector.AddReferencedObject(Class, This);
}
#endif
}
bool UObject::IsDestructionThreadSafe() const
{
return false;
}
/*-----------------------------------------------------------------------------
Implementation of realtime garbage collection helper functions in
FProperty, UClass, ...
-----------------------------------------------------------------------------*/
/**
* Returns true if this property, or in the case of e.g. array or struct properties any sub- property, contains a
* UObject reference.
*
* @return true if property (or sub- properties) contain a UObject reference, false otherwise
*/
bool FProperty::ContainsObjectReference(TArray<const FStructProperty*>& EncounteredStructProps, EPropertyObjectReferenceType InReferenceType /*= EPropertyObjectReferenceType::Strong*/) const
{
return false;
}
/**
* Returns true if this property, or in the case of e.g. array or struct properties any sub- property, contains a
* UObject reference.
*
* @return true if property (or sub- properties) contain a UObject reference, false otherwise
*/
bool FArrayProperty::ContainsObjectReference(TArray<const FStructProperty*>& EncounteredStructProps, EPropertyObjectReferenceType InReferenceType /*= EPropertyObjectReferenceType::Strong*/) const
{
check(Inner);
return Inner->ContainsObjectReference(EncounteredStructProps, InReferenceType);
}
/**
* Returns true if this property, or in the case of e.g. array or struct properties any sub- property, contains a
* UObject reference.
*
* @return true if property (or sub- properties) contain a UObject reference, false otherwise
*/
bool FMapProperty::ContainsObjectReference(TArray<const FStructProperty*>& EncounteredStructProps, EPropertyObjectReferenceType InReferenceType /*= EPropertyObjectReferenceType::Strong*/) const
{
check(KeyProp);
check(ValueProp);
return KeyProp->ContainsObjectReference(EncounteredStructProps, InReferenceType) || ValueProp->ContainsObjectReference(EncounteredStructProps, InReferenceType);
}
/**
* Returns true if this property, or in the case of e.g. array or struct properties any sub- property, contains a
* UObject reference.
*
* @return true if property (or sub- properties) contain a UObject reference, false otherwise
*/
bool FSetProperty::ContainsObjectReference(TArray<const FStructProperty*>& EncounteredStructProps, EPropertyObjectReferenceType InReferenceType /*= EPropertyObjectReferenceType::Strong*/) const
{
check(ElementProp);
return ElementProp->ContainsObjectReference(EncounteredStructProps, InReferenceType);
}
/**
* Returns true if this property, or in the case of e.g. array or struct properties any sub- property, contains a
* UObject reference.
*
* @return true if property (or sub- properties) contain a UObject reference, false otherwise
*/
bool FStructProperty::ContainsObjectReference(TArray<const FStructProperty*>& EncounteredStructProps, EPropertyObjectReferenceType InReferenceType /*= EPropertyObjectReferenceType::Strong*/) const
{
if (EncounteredStructProps.Contains(this))
{
return false;
}
else
{
if (!Struct)
{
UE_LOG(LogGarbage, Warning, TEXT("Broken FStructProperty does not have a UStruct: %s"), *GetFullName());
}
else if (Struct->StructFlags & STRUCT_AddStructReferencedObjects)
{
return true;
}
else
{
EncounteredStructProps.Add(this);
FProperty* Property = Struct->PropertyLink;
while (Property)
{
if (Property->ContainsObjectReference(EncounteredStructProps, InReferenceType))
{
EncounteredStructProps.RemoveSingleSwap(this);
return true;
}
Property = Property->PropertyLinkNext;
}
EncounteredStructProps.RemoveSingleSwap(this);
}
return false;
}
}
bool FFieldPathProperty::ContainsObjectReference(TArray<const FStructProperty*>& EncounteredStructProps, EPropertyObjectReferenceType InReferenceType /*= EPropertyObjectReferenceType::Strong*/) const
{
return !!(InReferenceType & EPropertyObjectReferenceType::Strong);
}
// Returns true if this property contains a weak UObject reference.
bool FDelegateProperty::ContainsObjectReference(TArray<const FStructProperty*>& EncounteredStructProps, EPropertyObjectReferenceType InReferenceType /*= EPropertyObjectReferenceType::Strong*/) const
{
return !!(InReferenceType & EPropertyObjectReferenceType::Weak);
}
// Returns true if this property contains a weak UObject reference.
bool FMulticastDelegateProperty::ContainsObjectReference(TArray<const FStructProperty*>& EncounteredStructProps, EPropertyObjectReferenceType InReferenceType /*= EPropertyObjectReferenceType::Strong*/) const
{
return !!(InReferenceType & EPropertyObjectReferenceType::Weak);
}
/**
* Scope helper structure to emit tokens for fixed arrays in the case of ArrayDim (passed in count) being > 1.
*/
struct FGCReferenceFixedArrayTokenHelper
{
/**
* Constructor, emitting necessary tokens for fixed arrays if count > 1 and also keeping track of count so
* destructor can do the same.
*
* @param InReferenceTokenStream Token stream to emit tokens to
* @param InOffset offset into object/ struct
* @param InCount array count
* @param InStride array type stride (e.g. sizeof(struct) or sizeof(UObject*))
* @param InProperty the property this array represents
*/
FGCReferenceFixedArrayTokenHelper(UClass& OwnerClass, int32 InOffset, int32 InCount, int32 InStride, const FProperty& InProperty)
: ReferenceTokenStream(&OwnerClass.ReferenceTokenStream), Count(InCount)
{
if (InCount > 1)
{
OwnerClass.EmitObjectReference(InOffset, InProperty.GetFName(), GCRT_FixedArray);
OwnerClass.ReferenceTokenStream.EmitStride(InStride);
OwnerClass.ReferenceTokenStream.EmitCount(InCount);
}
}
/** Destructor, emitting return if ArrayDim > 1 */
~FGCReferenceFixedArrayTokenHelper()
{
if (Count > 1)
{
ReferenceTokenStream->EmitReturn();
}
}
private:
/** Reference token stream used to emit to */
FGCReferenceTokenStream* ReferenceTokenStream;
/** Size of fixed array */
int32 Count;
};
/**
* Emits tokens used by realtime garbage collection code to passed in ReferenceTokenStream. The offset emitted is relative
* to the passed in BaseOffset which is used by e.g. arrays of structs.
*/
void FProperty::EmitReferenceInfo(UClass& OwnerClass, int32 BaseOffset, TArray<const FStructProperty*>& EncounteredStructProps)
{
}
/**
* Emits tokens used by realtime garbage collection code to passed in OwnerClass' ReferenceTokenStream. The offset emitted is relative
* to the passed in BaseOffset which is used by e.g. arrays of structs.
*/
void FObjectProperty::EmitReferenceInfo(UClass& OwnerClass, int32 BaseOffset, TArray<const FStructProperty*>& EncounteredStructProps)
{
FGCReferenceFixedArrayTokenHelper FixedArrayHelper(OwnerClass, BaseOffset + GetOffset_ForGC(), ArrayDim, sizeof(UObject*), *this);
OwnerClass.EmitObjectReference(BaseOffset + GetOffset_ForGC(), GetFName(), GCRT_Object);
}
void FWeakObjectProperty::EmitReferenceInfo(UClass& OwnerClass, int32 BaseOffset, TArray<const FStructProperty*>& EncounteredStructProps)
{
FGCReferenceFixedArrayTokenHelper FixedArrayHelper(OwnerClass, BaseOffset + GetOffset_ForGC(), ArrayDim, sizeof(FWeakObjectPtr), *this);
OwnerClass.EmitObjectReference(BaseOffset + GetOffset_ForGC(), GetFName(), GCRT_WeakObject);
}
void FLazyObjectProperty::EmitReferenceInfo(UClass& OwnerClass, int32 BaseOffset, TArray<const FStructProperty*>& EncounteredStructProps)
{
FGCReferenceFixedArrayTokenHelper FixedArrayHelper(OwnerClass, BaseOffset + GetOffset_ForGC(), ArrayDim, sizeof(FLazyObjectPtr), *this);
OwnerClass.EmitObjectReference(BaseOffset + GetOffset_ForGC(), GetFName(), GCRT_LazyObject);
}
void FSoftObjectProperty::EmitReferenceInfo(UClass& OwnerClass, int32 BaseOffset, TArray<const FStructProperty*>& EncounteredStructProps)
{
FGCReferenceFixedArrayTokenHelper FixedArrayHelper(OwnerClass, BaseOffset + GetOffset_ForGC(), ArrayDim, sizeof(FSoftObjectPtr), *this);
OwnerClass.EmitObjectReference(BaseOffset + GetOffset_ForGC(), GetFName(), GCRT_SoftObject);
}
void FDelegateProperty::EmitReferenceInfo(UClass& OwnerClass, int32 BaseOffset, TArray<const FStructProperty*>& EncounteredStructProps)
{
FGCReferenceFixedArrayTokenHelper FixedArrayHelper(OwnerClass, BaseOffset + GetOffset_ForGC(), ArrayDim, this->ElementSize, *this);
OwnerClass.EmitObjectReference(BaseOffset + GetOffset_ForGC(), GetFName(), GCRT_Delegate);
}
void FMulticastDelegateProperty::EmitReferenceInfo(UClass& OwnerClass, int32 BaseOffset, TArray<const FStructProperty*>& EncounteredStructProps)
{
FGCReferenceFixedArrayTokenHelper FixedArrayHelper(OwnerClass, BaseOffset + GetOffset_ForGC(), ArrayDim, this->ElementSize, *this);
OwnerClass.EmitObjectReference(BaseOffset + GetOffset_ForGC(), GetFName(), GCRT_MulticastDelegate);
}
/**
* Emits tokens used by realtime garbage collection code to passed in OwnerClass' ReferenceTokenStream. The offset emitted is relative
* to the passed in BaseOffset which is used by e.g. arrays of structs.
*/
void FArrayProperty::EmitReferenceInfo(UClass& OwnerClass, int32 BaseOffset, TArray<const FStructProperty*>& EncounteredStructProps)
{
if (Inner->ContainsObjectReference(EncounteredStructProps, EPropertyObjectReferenceType::Strong | EPropertyObjectReferenceType::Weak))
{
bool bUsesFreezableAllocator = EnumHasAnyFlags(ArrayFlags, EArrayPropertyFlags::UsesMemoryImageAllocator);
if (Inner->IsA(FStructProperty::StaticClass()))
{
OwnerClass.EmitObjectReference(BaseOffset + GetOffset_ForGC(), GetFName(), bUsesFreezableAllocator ? GCRT_ArrayStructFreezable : GCRT_ArrayStruct);
OwnerClass.ReferenceTokenStream.EmitStride(Inner->ElementSize);
const uint32 SkipIndexIndex = OwnerClass.ReferenceTokenStream.EmitSkipIndexPlaceholder();
Inner->EmitReferenceInfo(OwnerClass, 0, EncounteredStructProps);
const uint32 SkipIndex = OwnerClass.ReferenceTokenStream.EmitReturn();
OwnerClass.ReferenceTokenStream.UpdateSkipIndexPlaceholder(SkipIndexIndex, SkipIndex);
}
else if (Inner->IsA(FObjectProperty::StaticClass()))
{
OwnerClass.EmitObjectReference(BaseOffset + GetOffset_ForGC(), GetFName(), bUsesFreezableAllocator ? GCRT_ArrayObjectFreezable : GCRT_ArrayObject);
}
else if (Inner->IsA(FInterfaceProperty::StaticClass()))
{
OwnerClass.EmitObjectReference(BaseOffset + GetOffset_ForGC(), GetFName(), bUsesFreezableAllocator ? GCRT_ArrayStructFreezable : GCRT_ArrayStruct);
OwnerClass.ReferenceTokenStream.EmitStride(Inner->ElementSize);
const uint32 SkipIndexIndex = OwnerClass.ReferenceTokenStream.EmitSkipIndexPlaceholder();
OwnerClass.EmitObjectReference(0, GetFName(), GCRT_Object);
const uint32 SkipIndex = OwnerClass.ReferenceTokenStream.EmitReturn();
OwnerClass.ReferenceTokenStream.UpdateSkipIndexPlaceholder(SkipIndexIndex, SkipIndex);
}
else if (Inner->IsA(FFieldPathProperty::StaticClass()))
{
OwnerClass.EmitObjectReference(BaseOffset + GetOffset_ForGC(), GetFName(), GCRT_ArrayAddFieldPathReferencedObject);
}
else if (Inner->IsA(FWeakObjectProperty::StaticClass()))
{
OwnerClass.EmitObjectReference(BaseOffset + GetOffset_ForGC(), GetFName(), GCRT_ArrayWeakObject);
}
else if (Inner->IsA(FLazyObjectProperty::StaticClass()))
{
OwnerClass.EmitObjectReference(BaseOffset + GetOffset_ForGC(), GetFName(), GCRT_ArrayLazyObject);
}
else if (Inner->IsA(FSoftObjectProperty::StaticClass()))
{
OwnerClass.EmitObjectReference(BaseOffset + GetOffset_ForGC(), GetFName(), GCRT_ArraySoftObject);
}
else if (Inner->IsA(FDelegateProperty::StaticClass()))
{
OwnerClass.EmitObjectReference(BaseOffset + GetOffset_ForGC(), GetFName(), GCRT_ArrayDelegate);
}
else if (Inner->IsA(FMulticastDelegateProperty::StaticClass()))
{
OwnerClass.EmitObjectReference(BaseOffset + GetOffset_ForGC(), GetFName(), GCRT_ArrayMulticastDelegate);
}
else
{
UE_LOG(LogGarbage, Fatal, TEXT("Encountered unknown property containing object or name reference: %s in %s"), *Inner->GetFullName(), *GetFullName());
}
}
}
/**
* Emits tokens used by realtime garbage collection code to passed in OwnerClass' ReferenceTokenStream. The offset emitted is relative
* to the passed in BaseOffset which is used by e.g. arrays of structs.
*/
void FMapProperty::EmitReferenceInfo(UClass& OwnerClass, int32 BaseOffset, TArray<const FStructProperty*>& EncounteredStructProps)
{
if (ContainsObjectReference(EncounteredStructProps, EPropertyObjectReferenceType::Strong | EPropertyObjectReferenceType::Weak))
{
// TMap reference tokens are processed by GC in a similar way to an array of structs
OwnerClass.EmitObjectReference(BaseOffset + GetOffset_ForGC(), GetFName(), GCRT_AddTMapReferencedObjects);
OwnerClass.ReferenceTokenStream.EmitPointer((const void*)this);
const uint32 SkipIndexIndex = OwnerClass.ReferenceTokenStream.EmitSkipIndexPlaceholder();
if (KeyProp->ContainsObjectReference(EncounteredStructProps, EPropertyObjectReferenceType::Strong | EPropertyObjectReferenceType::Weak))
{
KeyProp->EmitReferenceInfo(OwnerClass, 0, EncounteredStructProps);
}
if (ValueProp->ContainsObjectReference(EncounteredStructProps, EPropertyObjectReferenceType::Strong | EPropertyObjectReferenceType::Weak))
{
ValueProp->EmitReferenceInfo(OwnerClass, 0, EncounteredStructProps);
}
const uint32 SkipIndex = OwnerClass.ReferenceTokenStream.EmitReturn();
OwnerClass.ReferenceTokenStream.UpdateSkipIndexPlaceholder(SkipIndexIndex, SkipIndex);
}
}
/**
* Emits tokens used by realtime garbage collection code to passed in OwnerClass' ReferenceTokenStream. The offset emitted is relative
* to the passed in BaseOffset which is used by e.g. arrays of structs.
*/
void FSetProperty::EmitReferenceInfo(UClass& OwnerClass, int32 BaseOffset, TArray<const FStructProperty*>& EncounteredStructProps)
{
if (ContainsObjectReference(EncounteredStructProps, EPropertyObjectReferenceType::Strong | EPropertyObjectReferenceType::Weak))
{
// TSet reference tokens are processed by GC in a similar way to an array of structs
OwnerClass.EmitObjectReference(BaseOffset + GetOffset_ForGC(), GetFName(), GCRT_AddTSetReferencedObjects);
OwnerClass.ReferenceTokenStream.EmitPointer((const void*)this);
const uint32 SkipIndexIndex = OwnerClass.ReferenceTokenStream.EmitSkipIndexPlaceholder();
ElementProp->EmitReferenceInfo(OwnerClass, 0, EncounteredStructProps);
const uint32 SkipIndex = OwnerClass.ReferenceTokenStream.EmitReturn();
OwnerClass.ReferenceTokenStream.UpdateSkipIndexPlaceholder(SkipIndexIndex, SkipIndex);
}
}
/**
* Emits tokens used by realtime garbage collection code to passed in ReferenceTokenStream. The offset emitted is relative
* to the passed in BaseOffset which is used by e.g. arrays of structs.
*/
void FStructProperty::EmitReferenceInfo(UClass& OwnerClass, int32 BaseOffset, TArray<const FStructProperty*>& EncounteredStructProps)
{
check(Struct);
if (Struct->StructFlags & STRUCT_AddStructReferencedObjects)
{
UScriptStruct::ICppStructOps* CppStructOps = Struct->GetCppStructOps();
check(CppStructOps); // else should not have STRUCT_AddStructReferencedObjects
FGCReferenceFixedArrayTokenHelper FixedArrayHelper(OwnerClass, BaseOffset + GetOffset_ForGC(), ArrayDim, ElementSize, *this);
OwnerClass.EmitObjectReference(BaseOffset + GetOffset_ForGC(), GetFName(), GCRT_AddStructReferencedObjects);
void* FunctionPtr = (void*)CppStructOps->AddStructReferencedObjects();
OwnerClass.ReferenceTokenStream.EmitPointer(FunctionPtr);
}
// Check if the struct has any properties that reference UObjects
bool bHasPropertiesWithObjectReferences = false;
if (Struct->PropertyLink)
{
// Can't use ContainObjectReference here as it also checks for STRUCT_AddStructReferencedObjects but we only care about property exposed refs
EncounteredStructProps.Add(this);
for (FProperty* Property = Struct->PropertyLink; Property && !bHasPropertiesWithObjectReferences; Property = Property->PropertyLinkNext)
{
bHasPropertiesWithObjectReferences = Property->ContainsObjectReference(EncounteredStructProps, EPropertyObjectReferenceType::Strong | EPropertyObjectReferenceType::Weak);
}
EncounteredStructProps.RemoveSingleSwap(this);
}
// If the struct has UObject properties (and only if) emit tokens for them
if (bHasPropertiesWithObjectReferences)
{
FGCReferenceFixedArrayTokenHelper FixedArrayHelper(OwnerClass, BaseOffset + GetOffset_ForGC(), ArrayDim, ElementSize, *this);
FProperty* Property = Struct->PropertyLink;
while (Property)
{
Property->EmitReferenceInfo(OwnerClass, BaseOffset + GetOffset_ForGC(), EncounteredStructProps);
Property = Property->PropertyLinkNext;
}
}
}
/**
* Emits tokens used by realtime garbage collection code to passed in ReferenceTokenStream. The offset emitted is relative
* to the passed in BaseOffset which is used by e.g. arrays of structs.
*/
void FInterfaceProperty::EmitReferenceInfo(UClass& OwnerClass, int32 BaseOffset, TArray<const FStructProperty*>& EncounteredStructProps)
{
FGCReferenceFixedArrayTokenHelper FixedArrayHelper(OwnerClass, BaseOffset + GetOffset_ForGC(), ArrayDim, sizeof(FScriptInterface), *this);
OwnerClass.EmitObjectReference(BaseOffset + GetOffset_ForGC(), GetFName(), GCRT_Object);
}
void FFieldPathProperty::EmitReferenceInfo(UClass& OwnerClass, int32 BaseOffset, TArray<const FStructProperty*>& EncounteredStructProps)
{
static_assert(sizeof(FFieldPath) == sizeof(TFieldPath<FProperty>), "TFieldPath should have the same size as the underlying FFieldPath");
FGCReferenceFixedArrayTokenHelper FixedArrayHelper(OwnerClass, BaseOffset + GetOffset_ForGC(), ArrayDim, sizeof(FFieldPath), *this);
OwnerClass.EmitObjectReference(BaseOffset + GetOffset_ForGC(), GetFName(), GCRT_AddFieldPathReferencedObject);
}
void UClass::EmitObjectReference(int32 Offset, const FName& DebugName, EGCReferenceType Kind)
{
FGCReferenceInfo ObjectReference(Kind, Offset);
ReferenceTokenStream.EmitReferenceInfo(ObjectReference, DebugName);
}
void UClass::EmitObjectArrayReference(int32 Offset, const FName& DebugName)
{
check(HasAnyClassFlags(CLASS_Intrinsic));
EmitObjectReference(Offset, DebugName, GCRT_ArrayObject);
}
uint32 UClass::EmitStructArrayBegin(int32 Offset, const FName& DebugName, int32 Stride)
{
check(HasAnyClassFlags(CLASS_Intrinsic));
EmitObjectReference(Offset, DebugName, GCRT_ArrayStruct);
ReferenceTokenStream.EmitStride(Stride);
const uint32 SkipIndexIndex = ReferenceTokenStream.EmitSkipIndexPlaceholder();
return SkipIndexIndex;
}
/**
* Realtime garbage collection helper function used to indicate the end of an array of structs. The
* index following the current one will be written to the passed in SkipIndexIndex in order to be
* able to skip tokens for empty dynamic arrays.
*
* @param SkipIndexIndex
*/
void UClass::EmitStructArrayEnd(uint32 SkipIndexIndex)
{
check(HasAnyClassFlags(CLASS_Intrinsic));
const uint32 SkipIndex = ReferenceTokenStream.EmitReturn();
ReferenceTokenStream.UpdateSkipIndexPlaceholder(SkipIndexIndex, SkipIndex);
}
void UClass::EmitFixedArrayBegin(int32 Offset, const FName& DebugName, int32 Stride, int32 Count)
{
check(HasAnyClassFlags(CLASS_Intrinsic));
EmitObjectReference(Offset, DebugName, GCRT_FixedArray);
ReferenceTokenStream.EmitStride(Stride);
ReferenceTokenStream.EmitCount(Count);
}
/**
* Realtime garbage collection helper function used to indicated the end of a fixed array.
*/
void UClass::EmitFixedArrayEnd()
{
check(HasAnyClassFlags(CLASS_Intrinsic));
ReferenceTokenStream.EmitReturn();
}
void UClass::EmitExternalPackageReference()
{
#if WITH_EDITOR
static const FName TokenName("ExternalPackageToken");
ReferenceTokenStream.EmitReferenceInfo(FGCReferenceInfo(GCRT_ExternalPackage, 0), TokenName);
#endif
}
struct FScopeLockIfNotNative
{
FCriticalSection& ScopeCritical;
const bool bNotNative;
FScopeLockIfNotNative(FCriticalSection& InScopeCritical, bool bIsNotNative)
: ScopeCritical(InScopeCritical), bNotNative(bIsNotNative)
{
if (bNotNative)
{
ScopeCritical.Lock();
}
}
~FScopeLockIfNotNative()
{
if (bNotNative)
{
ScopeCritical.Unlock();
}
}
};
void UClass::AssembleReferenceTokenStream(bool bForce)
{
// Lock for non-native classes
FScopeLockIfNotNative ReferenceTokenStreamLock(ReferenceTokenStreamCritical, !(ClassFlags & CLASS_Native));
UE_CLOG(!IsInGameThread() && !IsGarbageCollectionLocked(), LogGarbage, Fatal, TEXT("AssembleReferenceTokenStream for %s called on a non-game thread while GC is not locked."), *GetFullName());
if (!HasAnyClassFlags(CLASS_TokenStreamAssembled) || bForce)
{
if (bForce)
{
ReferenceTokenStream.Empty();
ClassFlags &= ~CLASS_TokenStreamAssembled;
}
TArray<const FStructProperty*> EncounteredStructProps;
// Iterate over properties defined in this class
for (TFieldIterator<FProperty> It(this, EFieldIteratorFlags::ExcludeSuper); It; ++It)
{
FProperty* Property = *It;
Property->EmitReferenceInfo(*this, 0, EncounteredStructProps);
}
if (UClass* SuperClass = GetSuperClass())
{
// We also need to lock the super class stream in case something (like PostLoad) wants to reconstruct it on GameThread
FScopeLockIfNotNative SuperClassReferenceTokenStreamLock(SuperClass->ReferenceTokenStreamCritical, !(SuperClass->ClassFlags & CLASS_Native));
// Make sure super class has valid token stream.
SuperClass->AssembleReferenceTokenStream();
if (!SuperClass->ReferenceTokenStream.IsEmpty())
{
// Prepend super's stream. This automatically handles removing the EOS token.
ReferenceTokenStream.PrependStream(SuperClass->ReferenceTokenStream);
}
}
else
{
UObjectBase::EmitBaseReferences(this);
}
{
check(ClassAddReferencedObjects != NULL);
const bool bKeepOuter = true; // GetFName() != NAME_Package;
const bool bKeepClass = true; //! HasAnyInternalFlags(EInternalObjectFlags::Native) || IsA(UDynamicClass::StaticClass());
ClassAddReferencedObjectsType AddReferencedObjectsFn = nullptr;
#if !WITH_EDITOR
// In no-editor builds UObject::ARO is empty, thus only classes
// which implement their own ARO function need to have the ARO token generated.
if (ClassAddReferencedObjects != &UObject::AddReferencedObjects)
{
AddReferencedObjectsFn = ClassAddReferencedObjects;
}
#else
AddReferencedObjectsFn = ClassAddReferencedObjects;
#endif
ReferenceTokenStream.Fixup(AddReferencedObjectsFn, bKeepOuter, bKeepClass);
}
if (ReferenceTokenStream.IsEmpty())
{
return;
}
// Emit end of stream token.
static const FName EOSDebugName("EndOfStreamToken");
EmitObjectReference(0, EOSDebugName, GCRT_EndOfStream);
// Shrink reference token stream to proper size.
ReferenceTokenStream.Shrink();
check(!HasAnyClassFlags(CLASS_TokenStreamAssembled)); // recursion here is probably bad
ClassFlags |= CLASS_TokenStreamAssembled;
}
}
/**
* Prepends passed in stream to existing one.
*
* @param Other stream to concatenate
*/
void FGCReferenceTokenStream::PrependStream(const FGCReferenceTokenStream& Other)
{
// Remove embedded EOS token if needed.
FGCReferenceInfo EndOfStream(GCRT_EndOfStream, 0);
int32 NumTokensToPrepend = (Other.Tokens.Num() && Other.Tokens.Last() == EndOfStream) ? (Other.Tokens.Num() - 1) : Other.Tokens.Num();
TArray<uint32> TempTokens;
TempTokens.Reserve(NumTokensToPrepend + Tokens.Num());
#if ENABLE_GC_OBJECT_CHECKS
check(TokenDebugInfo.Num() == Tokens.Num());
check(Other.TokenDebugInfo.Num() == Other.Tokens.Num());
TArray<FName> TempTokenDebugInfo;
TempTokenDebugInfo.Reserve(NumTokensToPrepend + TokenDebugInfo.Num());
#endif // ENABLE_GC_OBJECT_CHECKS
for (int32 TokenIndex = 0; TokenIndex < NumTokensToPrepend; ++TokenIndex)
{
TempTokens.Add(Other.Tokens[TokenIndex]);
#if ENABLE_GC_OBJECT_CHECKS
TempTokenDebugInfo.Add(Other.TokenDebugInfo[TokenIndex]);
#endif // ENABLE_GC_OBJECT_CHECKS
}
TempTokens.Append(Tokens);
Tokens = MoveTemp(TempTokens);
#if ENABLE_GC_OBJECT_CHECKS
TempTokenDebugInfo.Append(TokenDebugInfo);
TokenDebugInfo = MoveTemp(TempTokenDebugInfo);
#endif // ENABLE_GC_OBJECT_CHECKS
}
void FGCReferenceTokenStream::Fixup(void (*AddReferencedObjectsPtr)(UObject*, class FReferenceCollector&), bool bKeepOuterToken, bool bKeepClassToken)
{
bool bReplacedARO = false;
// Try to find exiting ARO pointer and replace it (to avoid removing and readding tokens).
for (int32 TokenStreamIndex = 0; TokenStreamIndex < Tokens.Num(); ++TokenStreamIndex)
{
uint32 TokenIndex = (uint32)TokenStreamIndex;
FGCReferenceInfo Token = Tokens[TokenIndex];
// Read token type and skip additional data if present.
switch (Token.Type)
{
case GCRT_ArrayStruct:
case GCRT_ArrayStructFreezable: {
// Skip stride and move to Skip Info
TokenIndex += 2;
const FGCSkipInfo SkipInfo = ReadSkipInfo(TokenIndex);
// Set the TokenIndex to the skip index - 1 because we're going to
// increment in the for loop anyway.
TokenIndex = SkipInfo.SkipIndex - 1;
}
break;
case GCRT_FixedArray: {
// Skip stride
TokenIndex++;
// Skip count
TokenIndex++;
}
break;
case GCRT_AddStructReferencedObjects: {
// Skip pointer
TokenIndex += GNumTokensPerPointer;
}
break;
case GCRT_AddReferencedObjects: {
// Store the pointer after the ARO token.
if (AddReferencedObjectsPtr)
{
StorePointer(&Tokens[TokenIndex + 1], (const void*)AddReferencedObjectsPtr);
}
bReplacedARO = true;
TokenIndex += GNumTokensPerPointer;
}
break;
case GCRT_AddTMapReferencedObjects:
case GCRT_AddTSetReferencedObjects: {
// Skip pointer
TokenIndex += GNumTokensPerPointer;
TokenIndex += 1; // GCRT_EndOfPointer;
// Move to Skip Info
TokenIndex += 1;
const FGCSkipInfo SkipInfo = ReadSkipInfo(TokenIndex);
// Set the TokenIndex to the skip index - 1 because we're going to
// increment in the for loop anyway.
TokenIndex = SkipInfo.SkipIndex - 1;
}
break;
case GCRT_Class:
case GCRT_NoopClass: {
if (bKeepClassToken)
{
Token.Type = GCRT_Class;
}
else
{
Token.Type = GCRT_NoopClass;
}
Tokens[TokenIndex] = Token;
}
break;
case GCRT_PersistentObject:
case GCRT_NoopPersistentObject: {
if (bKeepOuterToken)
{
Token.Type = GCRT_PersistentObject;
}
else
{
Token.Type = GCRT_NoopPersistentObject;
}
Tokens[TokenIndex] = Token;
}
break;
case GCRT_Optional: {
// Move to Skip Info
TokenIndex++;
const FGCSkipInfo SkipInfo = ReadSkipInfo(TokenIndex);
// Set the TokenIndex to the skip index - 1 because we're going to
// increment in the for loop anyway.
TokenIndex = SkipInfo.SkipIndex - 1;
}
break;
case GCRT_None:
case GCRT_Object:
case GCRT_ExternalPackage:
case GCRT_ArrayObject:
case GCRT_ArrayObjectFreezable:
case GCRT_AddFieldPathReferencedObject:
case GCRT_ArrayAddFieldPathReferencedObject:
case GCRT_EndOfPointer:
case GCRT_EndOfStream:
case GCRT_WeakObject:
case GCRT_ArrayWeakObject:
case GCRT_LazyObject:
case GCRT_ArrayLazyObject:
case GCRT_SoftObject:
case GCRT_ArraySoftObject:
case GCRT_Delegate:
case GCRT_ArrayDelegate:
case GCRT_MulticastDelegate:
case GCRT_ArrayMulticastDelegate:
break;
default:
UE_LOG(LogGarbage, Fatal, TEXT("Unknown token type (%u) when trying to add ARO token."), (uint32)Token.Type);
break;
};
TokenStreamIndex = (int32)TokenIndex;
}
// ARO is not in the token stream yet.
if (!bReplacedARO && AddReferencedObjectsPtr)
{
static const FName TokenName("AROToken");
EmitReferenceInfo(FGCReferenceInfo(GCRT_AddReferencedObjects, 0), TokenName);
EmitPointer((const void*)AddReferencedObjectsPtr);
}
}
int32 FGCReferenceTokenStream::EmitReferenceInfo(FGCReferenceInfo ReferenceInfo, const FName& DebugName)
{
int32 TokenIndex = Tokens.Add(ReferenceInfo);
#if ENABLE_GC_OBJECT_CHECKS
check(TokenDebugInfo.Num() == TokenIndex);
TokenDebugInfo.Add(DebugName);
#endif
return TokenIndex;
}
/**
* Emit placeholder for aray skip index, updated in UpdateSkipIndexPlaceholder
*
* @return the index of the skip index, used later in UpdateSkipIndexPlaceholder
*/
uint32 FGCReferenceTokenStream::EmitSkipIndexPlaceholder()
{
uint32 TokenIndex = Tokens.Add(E_GCSkipIndexPlaceholder);
#if ENABLE_GC_OBJECT_CHECKS
static const FName TokenName("SkipIndexPlaceholder");
check(TokenDebugInfo.Num() == TokenIndex);
TokenDebugInfo.Add(TokenName);
#endif
return TokenIndex;
}
/**
* Updates skip index place holder stored and passed in skip index index with passed
* in skip index. The skip index is used to skip over tokens in the case of an empty
* dynamic array.
*
* @param SkipIndexIndex index where skip index is stored at.
* @param SkipIndex index to store at skip index index
*/
void FGCReferenceTokenStream::UpdateSkipIndexPlaceholder(uint32 SkipIndexIndex, uint32 SkipIndex)
{
check(SkipIndex > 0 && SkipIndex <= (uint32)Tokens.Num());
const FGCReferenceInfo& ReferenceInfo = Tokens[SkipIndex - 1];
check(ReferenceInfo.Type != GCRT_None);
check(Tokens[SkipIndexIndex] == E_GCSkipIndexPlaceholder);
check(SkipIndexIndex < SkipIndex);
check(ReferenceInfo.ReturnCount >= 1);
FGCSkipInfo SkipInfo;
SkipInfo.SkipIndex = SkipIndex - SkipIndexIndex;
// We need to subtract 1 as ReturnCount includes return from this array.
SkipInfo.InnerReturnCount = ReferenceInfo.ReturnCount - 1;
Tokens[SkipIndexIndex] = SkipInfo;
}
/**
* Emit count
*
* @param Count count to emit
*/
int32 FGCReferenceTokenStream::EmitCount(uint32 Count)
{
int32 TokenIndex = Tokens.Add(Count);
#if ENABLE_GC_OBJECT_CHECKS
static const FName TokenName("CountToken");
check(TokenDebugInfo.Num() == TokenIndex);
TokenDebugInfo.Add(TokenName);
#endif
return TokenIndex;
}
int32 FGCReferenceTokenStream::EmitPointer(void const* Ptr)
{
const int32 StoreIndex = Tokens.Num();
Tokens.AddUninitialized(GNumTokensPerPointer);
StorePointer(&Tokens[StoreIndex], Ptr);
#if ENABLE_GC_OBJECT_CHECKS
static const FName TokenName("PointerToken");
check(TokenDebugInfo.Num() == StoreIndex);
for (int32 PointerTokenIndex = 0; PointerTokenIndex < GNumTokensPerPointer; ++PointerTokenIndex)
{
TokenDebugInfo.Add(TokenName);
}
#endif
// Now inser the end of pointer marker, this will mostly be used for storing ReturnCount value
// if the pointer was stored at the end of struct array stream.
static const FName EndOfPointerTokenName("EndOfPointerToken");
EmitReferenceInfo(FGCReferenceInfo(GCRT_EndOfPointer, 0), EndOfPointerTokenName);
return StoreIndex;
}
/**
* Emit stride
*
* @param Stride stride to emit
*/
int32 FGCReferenceTokenStream::EmitStride(uint32 Stride)
{
int32 TokenIndex = Tokens.Add(Stride);
#if ENABLE_GC_OBJECT_CHECKS
static const FName TokenName("StrideToken");
check(TokenDebugInfo.Num() == TokenIndex);
TokenDebugInfo.Add(TokenName);
#endif
return TokenIndex;
}
/**
* Increase return count on last token.
*
* @return index of next token
*/
uint32 FGCReferenceTokenStream::EmitReturn()
{
FGCReferenceInfo ReferenceInfo = Tokens.Last();
check(ReferenceInfo.Type != GCRT_None);
ReferenceInfo.ReturnCount++;
Tokens.Last() = ReferenceInfo;
return Tokens.Num();
}
#if ENABLE_GC_OBJECT_CHECKS
FTokenInfo FGCReferenceTokenStream::GetTokenInfo(int32 TokenIndex) const
{
FTokenInfo DebugInfo;
DebugInfo.Offset = FGCReferenceInfo(Tokens[TokenIndex]).Offset;
DebugInfo.Name = TokenDebugInfo[TokenIndex];
return DebugInfo;
}
#endif
FGCArrayPool* FGCArrayPool::GetGlobalSingleton()
{
static FAutoConsoleCommandWithOutputDevice GCDumpPoolCommand(
TEXT("gc.DumpPoolStats"),
TEXT("Dumps count and size of GC Pools"),
FConsoleCommandWithOutputDeviceDelegate::CreateStatic(&FGCArrayPool::DumpStats));
static FGCArrayPool* GlobalSingleton = nullptr;
if (!GlobalSingleton)
{
GlobalSingleton = new FGCArrayPool();
}
return GlobalSingleton;
}