Deleting shared ALevelInstance actors leads to extreme performance problems

On our project we use a large number of level instances which themselves are shared between a large number of levels. I investigated a performance issue today which will occur when you delete a level instance from a level. Render thread time went from 14.6ms to 60.58ms, RHI time went from 8.27ms to 48.39ms.

It took a while to uncover what was happening, but it turns out that when you delete an actor it does a reference search for soft object pointers for things which could be referencing that actor (see callstack). In doing this, it will load all other UWorlds that reference the LI. If those UWorlds themselves have ALevelInstance, it would then in turn load those UWorlds as well. This leads to an explosion of UWorlds which allocate FScenes, each of which are iterated for rendering in FEngineLoop::Tick();

I tried forcing GC to try to cleanup the old UWorlds, however that does not reduce the allocated FScene objects, so I’m not actually sure how we can help our content team fix this problem.

 	UnrealEditor-Renderer.dll!FRendererModule::AllocateScene(UWorld * World, bool bInRequiresHitProxies, bool bCreateFXSystem, ERHIFeatureLevel::Type InFeatureLevel) Line 6754	C++
 	UnrealEditor-Engine.dll!UWorld::InitWorld(const FWorldInitializationValues IVS) Line 2338	C++
 	UnrealEditor-UnrealEd.dll!UEditorEngine::InitializeNewlyCreatedInactiveWorld(UWorld * World) Line 6084	C++
>	UnrealEditor-UnrealEd.dll!UEditorEngine::OnAssetLoaded(UObject * Asset) Line 5997	C++
 	[External Code]	
 	[Inline Frame] UnrealEditor-CoreUObject.dll!TMulticastDelegateBase<FDefaultDelegateUserPolicy>::Broadcast(UObject *) Line 258	C++
 	[Inline Frame] UnrealEditor-CoreUObject.dll!TMulticastDelegate<void __cdecl(UObject *),FDefaultDelegateUserPolicy>::Broadcast(UObject *) Line 1080	C++
 	UnrealEditor-CoreUObject.dll!FAsyncLoadingThread2::ConditionalProcessEditorCallbacks() Line 10868	C++
 	UnrealEditor-CoreUObject.dll!FAsyncLoadingThread2::FlushLoading(TArrayView<int const ,int> RequestIDs) Line 11376	C++
 	UnrealEditor-CoreUObject.dll!FlushAsyncLoading(TArrayView<int const ,int> RequestIds) Line 362	C++
 	UnrealEditor-CoreUObject.dll!FlushAsyncLoading(int RequestId) Line 331	C++
 	UnrealEditor-CoreUObject.dll!LoadPackageInternal(UPackage * InOuter, const FPackagePath & PackagePath, unsigned int LoadFlags, FLinkerLoad * ImportLinker, FArchive * InReaderOverride, const FLinkerInstancingContext * InstancingContext, const FPackagePath * DiffPackagePath) Line 1771	C++
 	UnrealEditor-CoreUObject.dll!LoadPackage(UPackage * InOuter, const FPackagePath & PackagePath, unsigned int LoadFlags, FArchive * InReaderOverride, const FLinkerInstancingContext * InstancingContext, const FPackagePath * DiffPackagePath) Line 2135	C++
 	UnrealEditor-CoreUObject.dll!LoadPackage(UPackage * InOuter, const wchar_t * InLongPackageNameOrFilename, unsigned int LoadFlags, FArchive * InReaderOverride, const FLinkerInstancingContext * InstancingContext) Line 2111	C++
 	UnrealEditor-AssetTools.dll!FAssetRenameManager::GatherReferencingObjects(TArray<FAssetRenameDataWithReferencers,TSizedDefaultAllocator<32>> & AssetsToRename, TMap<FSoftObjectPath,TArray<UObject *,TSizedDefaultAllocator<32>>,FDefaultSetAllocator,TDefaultMapHashableKeyFuncs<FSoftObjectPath,TArray<UObject *,TSizedDefaultAllocator<32>>,0>> & OutSoftReferencingObjects) Line 1118	C++
 	UnrealEditor-AssetTools.dll!FAssetRenameManager::FindSoftReferencesToObjects(const TArray<FSoftObjectPath,TSizedDefaultAllocator<32>> & TargetObjects, TMap<FSoftObjectPath,TArray<UObject *,TSizedDefaultAllocator<32>>,FDefaultSetAllocator,TDefaultMapHashableKeyFuncs<FSoftObjectPath,TArray<UObject *,TSizedDefaultAllocator<32>>,0>> & ReferencingObjects) Line 213	C++
 	UnrealEditor-UnrealEd.dll!UUnrealEdEngine::DeleteActors(const TArray<AActor *,TSizedDefaultAllocator<32>> & InActorsToDelete, UWorld * InWorld, UTypedElementSelectionSet * InSelectionSet, const bool bVerifyDeletionCanHappen, const bool bWarnAboutReferences, const bool bWarnAboutSoftReferences) Line 923	C++

2026-04-23_14h51_50.png(228 KB)

Steps to Reproduce

Hi,

Have you tried reloading the level after deleting the LevelInstance? That should get rid of all the extra world\fscene instances.

Martin

I did some digging and found out this behavior is related to the BlueprintEditorProjectSettings::bValidateUnloadedSoftActorReferences . The default in 5.6 is true and forces loading soft references in FAssetRenameManager::GatherReferencingObjects

The default was recently changed to false to address the speed of deletion in LevelInstance. You can add the following in DefaultEditor.ini to turn the validation off:

[/Script/UnrealEd.BlueprintEditorProjectSettings]
bValidateUnloadedSoftActorReferences=false

There is a risk of breaking soft references without being notified with this setting turned off.

The main issue here is that the loaded worlds are flagged as RF_Standalone which prevents them from being GC’ed even as they are not referenced. Destroying the World would also lead to its FScene being destroyed.

I will try to get more information from the engine team.

I discussed with the engine team and the default for that setting is changed in 5.8. We ran into issues with following the soft references (editor stalls, OOMs, broken levels…) which prompted this decision.

I’m not sure I see an easy way to identify the Worlds that got loaded through references to force them to unload. That would be key in improving the current system.

Yes, this will remove the unecessary FScene instances, however I’m trying to find a better solution than asking our level designers to reload the map whenever they delete an actor.

This is what I read as well. To me the code reads as a lot heavier than simply blueprints being validated, and would generally validate any kind of references, so I got scared away from trying out this approach.

Really, what would be great is if loading a world via this pathway would automatically set InitializationValues::bInitializeScenes to false. I could probably hack something together here that just puts a scoped value somewhere when we flush the loads, however this feels a bit inelegant to me.