EM_Task/CoreUObject/Private/UObject/GarbageCollectionVerification.cpp

356 lines
18 KiB
C++
Raw Permalink Normal View History

2026-02-13 16:18:33 +08:00
// Copyright Epic Games, Inc. All Rights Reserved.
/*=============================================================================
UnObjGC.cpp: Unreal object garbage collection code.
=============================================================================*/
#include "UObject/GarbageCollectionVerification.h"
#include "UObject/GarbageCollection.h"
#include "HAL/ThreadSafeBool.h"
#include "Misc/TimeGuard.h"
#include "HAL/IConsoleManager.h"
#include "Misc/App.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/GCObject.h"
#include "UObject/GCScopeLock.h"
#include "HAL/ExceptionHandling.h"
#include "UObject/UObjectClusters.h"
#include "Async/ParallelFor.h"
#include "UObject/ReferenceChainSearch.h"
#include "UObject/FastReferenceCollector.h"
/*-----------------------------------------------------------------------------
Garbage collection verification code.
-----------------------------------------------------------------------------*/
/**
* If set and VERIFY_DISREGARD_GC_ASSUMPTIONS is true, we verify GC assumptions about "Disregard For GC" objects.
*/
COREUOBJECT_API bool GShouldVerifyGCAssumptions = !(UE_BUILD_SHIPPING != 0 && WITH_EDITOR != 0);
#if VERIFY_DISREGARD_GC_ASSUMPTIONS
/**
* Finds only direct references of objects passed to the TFastReferenceCollector and verifies if they meet Disregard for GC assumptions
*/
class FDisregardSetReferenceProcessor: public FSimpleReferenceProcessorBase
{
FThreadSafeCounter NumErrors;
public:
FDisregardSetReferenceProcessor()
: NumErrors(0)
{
}
int32 GetErrorCount() const
{
return NumErrors.GetValue();
}
FORCEINLINE_DEBUGGABLE void HandleTokenStreamObjectReference(TArray<UObject*>& ObjectsToSerialize, UObject* ReferencingObject, UObject*& Object, const int32 TokenIndex, bool bAllowReferenceElimination)
{
if (Object)
{
#if ENABLE_GC_OBJECT_CHECKS
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 while verifying Disregard for GC assumptions: 0x%016llx, ReferencingObject: %s, %s, TokenIndex: %d"),
(int64)(PTRINT)Object,
ReferencingObject ? *ReferencingObject->GetFullName() : TEXT("NULL"),
*TokenDebugInfo, TokenIndex);
}
#endif // ENABLE_GC_OBJECT_CHECKS
if (!(Object->IsRooted() ||
GUObjectArray.IsDisregardForGC(Object) ||
GUObjectArray.ObjectToObjectItem(Object)->GetOwnerIndex() > 0 ||
GUObjectArray.ObjectToObjectItem(Object)->HasAnyFlags(EInternalObjectFlags::ClusterRoot)))
{
UE_LOG(LogGarbage, Warning, TEXT("Disregard for GC object %s referencing %s which is not part of root set"),
*ReferencingObject->GetFullName(),
*Object->GetFullName());
NumErrors.Increment();
}
}
}
};
typedef TDefaultReferenceCollector<FDisregardSetReferenceProcessor> FDisregardSetReferenceCollector;
void VerifyGCAssumptions()
{
int32 MaxNumberOfObjects = GUObjectArray.GetObjectArrayNumPermanent();
FDisregardSetReferenceProcessor Processor;
TFastReferenceCollector<
FDisregardSetReferenceProcessor,
FDisregardSetReferenceCollector,
FGCArrayPool,
EFastReferenceCollectorOptions::AutogenerateTokenStream | EFastReferenceCollectorOptions::ProcessNoOpTokens>
ReferenceCollector(Processor, FGCArrayPool::Get());
int32 NumThreads = FMath::Max(1, FTaskGraphInterface::Get().GetNumWorkerThreads());
int32 NumberOfObjectsPerThread = (MaxNumberOfObjects / NumThreads) + 1;
FGCArrayStruct* ArrayStructs = new FGCArrayStruct[NumThreads];
ParallelFor(NumThreads, [&ReferenceCollector, ArrayStructs, NumberOfObjectsPerThread, NumThreads, MaxNumberOfObjects](int32 ThreadIndex)
{
int32 FirstObjectIndex = ThreadIndex * NumberOfObjectsPerThread;
int32 NumObjects = (ThreadIndex < (NumThreads - 1)) ? NumberOfObjectsPerThread : (MaxNumberOfObjects - (NumThreads - 1) * NumberOfObjectsPerThread);
FGCArrayStruct& ArrayStruct = ArrayStructs[ThreadIndex];
ArrayStruct.ObjectsToSerialize.Reserve(NumberOfObjectsPerThread);
for (int32 ObjectIndex = 0; ObjectIndex < NumObjects && (FirstObjectIndex + ObjectIndex) < MaxNumberOfObjects; ++ObjectIndex)
{
FUObjectItem& ObjectItem = GUObjectArray.GetObjectItemArrayUnsafe()[FirstObjectIndex + ObjectIndex];
if (ObjectItem.Object && ObjectItem.Object != FGCObject::GGCObjectReferencer)
{
ArrayStruct.ObjectsToSerialize.Add(static_cast<UObject*>(ObjectItem.Object));
}
}
ReferenceCollector.CollectReferences(ArrayStruct);
});
delete[] ArrayStructs;
UE_CLOG(Processor.GetErrorCount() > 0, LogGarbage, Fatal, TEXT("Encountered %d object(s) breaking Disregard for GC assumptions. Please check log for details."), Processor.GetErrorCount());
}
/**
* Finds only direct references of objects passed to the TFastReferenceCollector and verifies if they meet GC Cluster assumptions
*/
class FClusterVerifyReferenceProcessor: public FSimpleReferenceProcessorBase
{
FThreadSafeCounter NumErrors;
UObject* CurrentObject;
FUObjectCluster* Cluster;
UObject* ClusterRootObject;
public:
FClusterVerifyReferenceProcessor()
: NumErrors(0), CurrentObject(nullptr), Cluster(nullptr), ClusterRootObject(nullptr)
{
}
int32 GetErrorCount() const
{
return NumErrors.GetValue();
}
void SetCurrentObject(UObject* InRootOrClusterObject)
{
check(InRootOrClusterObject);
CurrentObject = InRootOrClusterObject;
Cluster = GUObjectClusters.GetObjectCluster(CurrentObject);
check(Cluster);
FUObjectItem* RootItem = GUObjectArray.IndexToObject(Cluster->RootIndex);
check(RootItem && RootItem->Object);
ClusterRootObject = static_cast<UObject*>(RootItem->Object);
}
/**
* Handles UObject reference from the token stream. Performance is critical here so we're FORCEINLINING this function.
*
* @param ObjectsToSerialize An array of remaining objects to serialize (Obj must be added to it if Obj can be added to cluster)
* @param ReferencingObject Object referencing the object to process.
* @param TokenIndex Index to the token stream where the reference was found.
* @param bAllowReferenceElimination True if reference elimination is allowed (ignored when constructing clusters).
*/
FORCEINLINE_DEBUGGABLE void HandleTokenStreamObjectReference(TArray<UObject*>& ObjectsToSerialize, UObject* ReferencingObject, UObject*& Object, const int32 TokenIndex, bool bAllowReferenceElimination)
{
if (Object)
{
check(CurrentObject);
#if ENABLE_GC_OBJECT_CHECKS
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");
}
#if UE_GCCLUSTER_VERBOSE_LOGGING
DumpClusterToLog(*Cluster, true, true);
#endif
UE_LOG(LogGarbage, Fatal, TEXT("Invalid object while verifying cluster assumptions: 0x%016llx, ReferencingObject: %s, %s, TokenIndex: %d"),
(int64)(PTRINT)Object,
ReferencingObject ? *ReferencingObject->GetFullName() : TEXT("NULL"),
*TokenDebugInfo, TokenIndex);
}
#endif // ENABLE_GC_OBJECT_CHECKS
FUObjectItem* ObjectItem = GUObjectArray.ObjectToObjectItem(Object);
if (ObjectItem->GetOwnerIndex() <= 0)
{
// We are allowed to reference other clusters, root set objects and objects from diregard for GC pool
if (!ObjectItem->HasAnyFlags(EInternalObjectFlags::ClusterRoot | EInternalObjectFlags::RootSet) && !GUObjectArray.IsDisregardForGC(Object) && Object->CanBeInCluster() &&
!Cluster->MutableObjects.Contains(GUObjectArray.ObjectToIndex(Object))) // This is for objects that had RF_NeedLoad|RF_NeedPostLoad set when creating the cluster
{
UE_LOG(LogGarbage, Warning, TEXT("Object %s (0x%016llx) from cluster %s (0x%016llx / 0x%016llx) is referencing 0x%016llx %s which is not part of root set or cluster."),
*CurrentObject->GetFullName(),
(int64)(PTRINT)CurrentObject,
*ClusterRootObject->GetFullName(),
(int64)(PTRINT)ClusterRootObject,
(int64)(PTRINT)Cluster,
(int64)(PTRINT)Object,
*Object->GetFullName());
NumErrors.Increment();
#if UE_BUILD_DEBUG
FReferenceChainSearch RefChainSearch(Object, EReferenceChainSearchMode::Shortest | EReferenceChainSearchMode::PrintResults);
#endif
}
else if (ObjectItem->HasAnyFlags(EInternalObjectFlags::ClusterRoot))
{
// However, clusters need to be referenced by the current cluster otherwise they can also get GC'd too early.
const FUObjectItem* ClusterRootObjectItem = GUObjectArray.ObjectToObjectItem(ClusterRootObject);
const int32 OtherClusterRootIndex = GUObjectArray.ObjectToIndex(Object);
const FUObjectItem* OtherClusterRootItem = GUObjectArray.IndexToObjectUnsafeForGC(OtherClusterRootIndex);
check(OtherClusterRootItem && OtherClusterRootItem->Object);
UObject* OtherClusterRootObject = static_cast<UObject*>(OtherClusterRootItem->Object);
UE_CLOG(OtherClusterRootIndex != Cluster->RootIndex &&
!Cluster->ReferencedClusters.Contains(OtherClusterRootIndex) &&
!Cluster->MutableObjects.Contains(OtherClusterRootIndex),
LogGarbage, Warning,
TEXT("Object %s from source cluster %s (%d) is referencing cluster root object %s (0x%016llx) (%d) which is not referenced by the source cluster."),
*GetFullNameSafe(ReferencingObject),
*ClusterRootObject->GetFullName(),
ClusterRootObjectItem->GetClusterIndex(),
*Object->GetFullName(),
(int64)(PTRINT)Object,
OtherClusterRootItem->GetClusterIndex());
}
}
else if (ObjectItem->GetOwnerIndex() != Cluster->RootIndex)
{
// If we're referencing an object from another cluster, make sure the other cluster is actually referenced by this cluster
const FUObjectItem* ClusterRootObjectItem = GUObjectArray.ObjectToObjectItem(ClusterRootObject);
const int32 OtherClusterRootIndex = ObjectItem->GetOwnerIndex();
check(OtherClusterRootIndex > 0);
const FUObjectItem* OtherClusterRootItem = GUObjectArray.IndexToObjectUnsafeForGC(OtherClusterRootIndex);
check(OtherClusterRootItem && OtherClusterRootItem->Object);
UObject* OtherClusterRootObject = static_cast<UObject*>(OtherClusterRootItem->Object);
UE_CLOG(OtherClusterRootIndex != Cluster->RootIndex &&
!Cluster->ReferencedClusters.Contains(OtherClusterRootIndex) &&
!Cluster->MutableObjects.Contains(GUObjectArray.ObjectToIndex(Object)),
LogGarbage, Warning,
TEXT("Object %s from source cluster %s (%d) is referencing object %s (0x%016llx) from cluster %s (%d) which is not referenced by the source cluster."),
*GetFullNameSafe(ReferencingObject),
*ClusterRootObject->GetFullName(),
ClusterRootObjectItem->GetClusterIndex(),
*Object->GetFullName(),
(int64)(PTRINT)Object,
*OtherClusterRootObject->GetFullName(),
OtherClusterRootItem->GetClusterIndex());
}
}
}
};
typedef TDefaultReferenceCollector<FClusterVerifyReferenceProcessor> FClusterVerifyReferenceCollector;
void VerifyClustersAssumptions()
{
int32 MaxNumberOfClusters = GUObjectClusters.GetClustersUnsafe().Num();
int32 NumThreads = FMath::Max(1, FTaskGraphInterface::Get().GetNumWorkerThreads());
int32 NumberOfClustersPerThread = (MaxNumberOfClusters / NumThreads) + 1;
FGCArrayStruct* ArrayStructs = new FGCArrayStruct[NumThreads];
FThreadSafeCounter NumErrors(0);
ParallelFor(NumThreads, [&NumErrors, ArrayStructs, NumberOfClustersPerThread, NumThreads, MaxNumberOfClusters](int32 ThreadIndex)
{
int32 FirstClusterIndex = ThreadIndex * NumberOfClustersPerThread;
int32 NumClusters = (ThreadIndex < (NumThreads - 1)) ? NumberOfClustersPerThread : (MaxNumberOfClusters - (NumThreads - 1) * NumberOfClustersPerThread);
FGCArrayStruct& ArrayStruct = ArrayStructs[ThreadIndex];
FClusterVerifyReferenceProcessor Processor;
TFastReferenceCollector<
FClusterVerifyReferenceProcessor,
FClusterVerifyReferenceCollector,
FGCArrayPool,
EFastReferenceCollectorOptions::AutogenerateTokenStream | EFastReferenceCollectorOptions::ProcessNoOpTokens>
ReferenceCollector(Processor, FGCArrayPool::Get());
for (int32 ClusterIndex = 0; ClusterIndex < NumClusters && (FirstClusterIndex + ClusterIndex) < MaxNumberOfClusters; ++ClusterIndex)
{
FUObjectCluster& Cluster = GUObjectClusters.GetClustersUnsafe()[FirstClusterIndex + ClusterIndex];
if (Cluster.RootIndex >= 0 && Cluster.Objects.Num())
{
ArrayStruct.ObjectsToSerialize.Reset();
ArrayStruct.ObjectsToSerialize.Reserve(Cluster.Objects.Num() + 1);
{
FUObjectItem* RootItem = GUObjectArray.IndexToObject(Cluster.RootIndex);
check(RootItem);
check(RootItem->Object);
ArrayStruct.ObjectsToSerialize.Add(static_cast<UObject*>(RootItem->Object));
}
for (int32 ObjectIndex: Cluster.Objects)
{
FUObjectItem* ObjectItem = GUObjectArray.IndexToObject(ObjectIndex);
check(ObjectItem);
check(ObjectItem->Object);
ArrayStruct.ObjectsToSerialize.Add(static_cast<UObject*>(ObjectItem->Object));
}
ReferenceCollector.CollectReferences(ArrayStruct);
NumErrors.Add(Processor.GetErrorCount());
}
}
});
delete[] ArrayStructs;
UE_CLOG(NumErrors.GetValue() > 0, LogGarbage, Fatal, TEXT("Encountered %d object(s) breaking GC Clusters assumptions. Please check log for details."), NumErrors.GetValue());
}
#endif // VERIFY_DISREGARD_GC_ASSUMPTIONS
#if PROFILE_GCConditionalBeginDestroy
TMap<FName, FCBDTime> CBDTimings;
TMap<UObject*, FName> CBDNameLookup;
void FScopedCBDProfile::DumpProfile()
{
CBDTimings.ValueSort(TLess<FCBDTime>());
int32 NumPrint = 0;
for (auto& Item: CBDTimings)
{
UE_LOG(LogGarbage, Log, TEXT(" %6d cnt %6.2fus per %6.2fms total %s"), Item.Value.Items, 1000.0f * 1000.0f * Item.Value.TotalTime / float(Item.Value.Items), 1000.0f * Item.Value.TotalTime, *Item.Key.ToString());
if (NumPrint++ > 3000000000)
{
break;
}
}
CBDTimings.Empty();
CBDNameLookup.Empty();
}
#endif // PROFILE_GCConditionalBeginDestroy