// Copyright Epic Games, Inc. All Rights Reserved. #include "PackageTracker.h" #include "CookOnTheFlyServerInterface.h" #include "CookPackageData.h" #include "CookPlatformManager.h" #include "CookProfiling.h" #include "Misc/ScopeRWLock.h" #include "UObject/Package.h" #include "UObject/UObjectGlobals.h" #include "UObject/UObjectIterator.h" namespace UE::Cook { void FThreadSafeUnsolicitedPackagesList::AddCookedPackage(const FFilePlatformRequest& PlatformRequest) { FScopeLock S(&SyncObject); CookedPackages.Add(PlatformRequest); } void FThreadSafeUnsolicitedPackagesList::GetPackagesForPlatformAndRemove(const ITargetPlatform* Platform, TArray& PackageNames) { FScopeLock _(&SyncObject); for (int I = CookedPackages.Num() - 1; I >= 0; --I) { FFilePlatformRequest& Request = CookedPackages[I]; if (Request.GetPlatforms().Contains(Platform)) { // remove the platform Request.RemovePlatform(Platform); PackageNames.Emplace(Request.GetFilename()); if (Request.GetPlatforms().Num() == 0) { CookedPackages.RemoveAt(I); } } } } void FThreadSafeUnsolicitedPackagesList::Empty() { FScopeLock _(&SyncObject); CookedPackages.Empty(); } FPackageTracker::FPackageTracker(UCookOnTheFlyServer& InCOTFS) :COTFS(InCOTFS) { ActiveInstances = new FPackageStreamInstancedPackageContainer(); // RefCounted } FPackageTracker::~FPackageTracker() { Unsubscribe(); } void FPackageTracker::Unsubscribe() { if (!bSubscribed) { return; } bSubscribed = false; GUObjectArray.RemoveUObjectDeleteListener(this); GUObjectArray.RemoveUObjectCreateListener(this); FCoreUObjectDelegates::OnEndLoadPackage.RemoveAll(this); } void FPackageTracker::InitializeTracking(TSet& OutStartupPackages) { check(!bTrackingInitialized); LLM_SCOPE_BYTAG(Cooker); FWriteScopeLock ScopeLock(Lock); OutStartupPackages.Empty(); for (TObjectIterator It; It; ++It) { UPackage* Package = *It; if (Package->GetOuter() == nullptr && Package != GetTransientPackage()) { LoadedPackages.Add(Package); OutStartupPackages.Add(Package->GetFName()); } } TMap MapOfNewPackages; MapOfNewPackages.Reserve(LoadedPackages.Num()); for (UPackage* Package : LoadedPackages) { MapOfNewPackages.Add(Package->GetFName(), FInstigator(EInstigator::StartupPackage)); } GUObjectArray.AddUObjectDeleteListener(this); GUObjectArray.AddUObjectCreateListener(this); FCoreUObjectDelegates::OnEndLoadPackage.AddRaw(this, &FPackageTracker::OnEndLoadPackage); bSubscribed = true; TArray> StartupPackageLoadTypes; FCookLoadScope::SetCookerStartupComplete(StartupPackageLoadTypes); for (TPair& Pair : StartupPackageLoadTypes) { switch (Pair.Value) { case ECookLoadType::EditorOnly: { OutStartupPackages.Remove(Pair.Key); FInstigator* Instigator = MapOfNewPackages.Find(Pair.Key); if (Instigator) { *Instigator = FInstigator(EInstigator::EditorOnlyLoad); } break; } case ECookLoadType::UsedInGame: { FInstigator* Instigator = MapOfNewPackages.Find(Pair.Key); if (Instigator) { *Instigator = FInstigator(EInstigator::StartupPackageCookLoadScope); } break; } default: break; } } bTrackingInitialized = true; PackageStream.Reserve(MapOfNewPackages.Num()); for (TPair& Pair : MapOfNewPackages) { PackageStream.Add({ Pair.Key, MoveTemp(Pair.Value), EPackageStreamEvent::PackageLoad }); } } TArray FPackageTracker::GetPackageStream() { check(bTrackingInitialized); FWriteScopeLock ScopeLock(Lock); TArray Result = MoveTemp(PackageStream); PackageStream.Reset(); return Result; } void FPackageTracker::NotifyUObjectCreated(const class UObjectBase* Object, int32 Index) { if (Object->GetClass() == UPackage::StaticClass()) { UPackage* Package = const_cast(static_cast(Object)); if (Package->GetOuter() == nullptr) // Nested packages are no longer created, but can still exist in old data. { OnCreatePackage(Package->GetFName()); LoadedPackages.Add(Package); } } } FInstigator FPackageTracker::GetPackageCreationInstigator() const { #if UE_WITH_PACKAGE_ACCESS_TRACKING PackageAccessTracking_Private::FTrackedData* AccumulatedScopeData = PackageAccessTracking_Private::FPackageAccessRefScope::GetCurrentThreadAccumulatedData(); FName ReferencerName(AccumulatedScopeData ? AccumulatedScopeData->PackageName : NAME_None); #else FName ReferencerName(NAME_None); #endif EInstigator InstigatorType; switch (FCookLoadScope::GetCurrentValue()) { case ECookLoadType::EditorOnly: InstigatorType = EInstigator::EditorOnlyLoad; break; case ECookLoadType::UsedInGame: InstigatorType = EInstigator::SaveTimeSoftDependency; break; default: InstigatorType = EInstigator::Unsolicited; break; } return FInstigator(InstigatorType, ReferencerName); } void FPackageTracker::OnCreatePackage(FName PackageName) { LLM_SCOPE_BYTAG(Cooker); #if ENABLE_COOK_STATS ++DetailedCookStats::NumDetectedLoads; #endif FInstigator Instigator = GetPackageCreationInstigator(); if (Instigator.Category == EInstigator::Unsolicited && COTFS.bHiddenDependenciesDebug) { COTFS.OnDiscoveredPackageDebug(PackageName, Instigator); } FWriteScopeLock ScopeLock(Lock); if (ExpectedNeverLoadPackages.Contains(PackageName)) { UE_LOG(LogCook, Verbose, TEXT("SoftGC PoorPerformance: Reloaded package %s."), *WriteToString<256>(PackageName)); } // We store packages by name rather than by pointer, because they might have their name changed. When // external actors are moved out of their external package, we rename the package to _Trash. // We want to report a load dependency on the package as it was originally loaded; we don't want to report // the renamed packagename if it gets renamed after load. ActivePackageInstigators.FindOrAdd(PackageName, Instigator); PackageStream.Add({ PackageName, MoveTemp(Instigator), EPackageStreamEvent::PackageLoad }); } void FPackageTracker::NotifyUObjectDeleted(const class UObjectBase* ObjectBase, int32 Index) { if (ObjectBase->GetClass() == UPackage::StaticClass()) { UPackage* Package = const_cast(static_cast(ObjectBase)); FWriteScopeLock ScopeLock(Lock); LoadedPackages.Remove(Package); } if (!bCollectingGarbage) { const UObject* Object = static_cast(ObjectBase); COTFS.PackageDatas->CachedCookedPlatformDataObjectsOnDestroyedOutsideOfGC(Object); } } void FPackageTracker::OnUObjectArrayShutdown() { Unsubscribe(); } void FPackageTracker::RemapTargetPlatforms(const TMap& Remap) { RemapMapKeys(PlatformSpecificNeverCookPackages, Remap); } void FPackageTracker::OnEndLoadPackage(const FEndLoadPackageContext& Context) { using namespace UE::AssetRegistry; // OnEndLoadPackage is the hook we use to test for whether a load is instanced; the CreatePackage hook is too early // (LoadPath is unknown in the case of instanced loads), and the OnSyncLoadPackage and OnAsyncLoadPackage hooks are // called too frequently - they are called even for packages that have already loaded, before LoadPackage checks // for whether they can early exit. OnEndLoadPackage has the information we need and is only called for packages // when they transition from unloaded to loaded. // // Use this hook to record instanced loads in our lookup map; ProcessUnsolicitedPackages will respond to the // creation event for the packages by looking them up in the map and using the information we provide about their // AssetRegistry dependencies non-instanced referencers. TArray, TInlineAllocator<10>> LoadedInstances; for (UPackage* Package : Context.LoadedPackages) { FName PackageName = Package->GetFName(); FName LoadedName = Package->GetLoadedPath().GetPackageFName(); if (PackageName == LoadedName || LoadedName.IsNone()) { continue; } FInstigator Instigator(EInstigator::Unsolicited); { FWriteScopeLock ScopeLock(Lock); FInstigator* ActiveInstigator = ActivePackageInstigators.Find(PackageName); if (ActiveInstigator) { Instigator = *ActiveInstigator; } } TArray PackageDependencies; COTFS.AssetRegistry->GetDependencies(FAssetIdentifier(LoadedName), PackageDependencies, EDependencyCategory::Package, EDependencyQuery::Hard); FWriteScopeLock ActiveInstancesScopeLock(ActiveInstances->Lock); FPackageStreamInstancedPackage*& Existing = ActiveInstances->Map.FindOrAdd(PackageName, nullptr); if (Existing) { if (Existing->LoadedName != LoadedName) { UE_LOG(LogCook, Error, TEXT("OnBeginLoadPackage was called twice for the same package with two different LoadedPaths. Ignoring the second call. PackageName: %s. LoadedPath1: %s. LoadedPath2: %s."), *PackageName.ToString(), *Existing->LoadedName.ToString(), *LoadedName.ToString()); continue; } LoadedInstances.Emplace(Existing); continue; } TRefCountPtr InstancedPackage = new FPackageStreamInstancedPackage(*ActiveInstances); Existing = InstancedPackage.GetReference(); InstancedPackage->PackageName = PackageName; InstancedPackage->LoadedName = LoadedName; InstancedPackage->Instigator = MoveTemp(Instigator); InstancedPackage->Dependencies.Reserve(PackageDependencies.Num()); for (FAssetDependency& PackageDependency : PackageDependencies) { InstancedPackage->Dependencies.Add(PackageDependency.AssetId.PackageName, PackageDependency.Properties); } LoadedInstances.Emplace(MoveTemp(InstancedPackage)); } if (LoadedInstances.IsEmpty()) { // The usual path through this function is that there were no loaded instances. Clear the // ActivePackageInstigators to fulfill our design of removing that memory when we no longer need it, and then // return without further work. FWriteScopeLock ScopeLock(Lock); ActivePackageInstigators.Reset(); } else { // Now that FPackageStreamInstancedPackage have been registered for all of the instanced loads that occurred // during the top-most LoadPackage call, calculate the ancestor non-instanced package referencer for each of // the instanced package loads. { FReadScopeLock ActiveInstancesScopeLock(ActiveInstances->Lock); for (TRefCountPtr& InstancedPackage : LoadedInstances) { TSet Visited; InstancedPackage->FlattenReferencer(ActiveInstancesScopeLock, Visited); } } // The FPackageStreamInstancedPackage we created need to remain available until all of the package creation // records created during their load and that we have added into the PackageStream have been processed by // ProcessUnsolicitedPackages. Add end-of-data-lifetime markers for the FPackageStreamInstancedPackages // into the PackageStream, after all of those creation records. FWriteScopeLock ScopeLock(Lock); for (TRefCountPtr& Instance : LoadedInstances) { PackageStream.Add({ Instance->PackageName, FInstigator(), EPackageStreamEvent::InstancedPackageEndLoad, TRefCountPtr(Instance.GetReference()) }); } // Clear ActivePackageInstigators to fulfill our design of removing that memory when we no longer need it. ActivePackageInstigators.Reset(); } } TRefCountPtr FPackageTracker::FindInstancedPackage(FName PackageName) { FWriteScopeLock ActiveInstancesScopeLock(ActiveInstances->Lock); FPackageStreamInstancedPackage** Existing = ActiveInstances->Map.Find(PackageName); return TRefCountPtr(Existing ? *Existing : nullptr); } EInstigator FPackageTracker::MergeReferenceCategories(EInstigator Parent, EInstigator Child) { // EditorOnly -> 0, Unsolicited -> 1, UsedInGame -> 2. Return Min(Child, Parent) switch (Parent) { case EInstigator::EditorOnlyLoad: [[fallthrough]]; case EInstigator::HardEditorOnlyDependency: return Parent; case EInstigator::Unsolicited: switch (Child) { case EInstigator::EditorOnlyLoad: [[fallthrough]]; case EInstigator::HardEditorOnlyDependency: return Child; default: // Child is Unsolicited or UsedInGame, return Parent; } default: return Child; } } FPackageStreamInstancedPackage::FPackageStreamInstancedPackage(FPackageStreamInstancedPackageContainer& InContainer) : Container(&InContainer) { } FPackageStreamInstancedPackage::~FPackageStreamInstancedPackage() { { FWriteScopeLock ActiveInstancesScopeLock(Container->Lock); Container->Map.Remove(PackageName); } Container.SafeRelease(); } void FPackageStreamInstancedPackage::FlattenReferencer(FReadScopeLock& CalledInsideActiveInstancesLock, TSet& Visited) { bool bAlreadyVisited; Visited.Add(this, &bAlreadyVisited); if (bAlreadyVisited) { UE_LOG(LogCook, Error, TEXT("Cycle detected in InstancedPackage referencers. PackageName == %s"), *PackageName.ToString()); return; } if (Instigator.Referencer.IsNone()) { return; } FPackageStreamInstancedPackage** ReferencerPtr = Container->Map.Find(Instigator.Referencer); if (!ReferencerPtr) { return; } FPackageStreamInstancedPackage* Referencer = *ReferencerPtr; Referencer->FlattenReferencer(CalledInsideActiveInstancesLock, Visited); Instigator.Referencer = Referencer->Instigator.Referencer; Instigator.Category = FPackageTracker::MergeReferenceCategories( Referencer->Instigator.Category, Instigator.Category); } } // namespace UE::Cook