Summary
After stopping Play-In-Editor in a World Partition open-world project, the editor game
thread blocks for anywhere between 7 minutes and over an hour. During the stall exactly
one thread spins at 100% of a single core, with zero disk I/O and zero memory
allocation.
CPU sampling identifies a single hot function responsible for essentially all of the
stall time:
UnrealEditor-Chaos.dll!TArray<Chaos::FPBDRigidsSolver::FPendingDestroyInfo,
TSizedDefaultAllocator<32> >::AddUniqueImpl<...>
TArray::AddUnique performs a linear scan of the array before every insertion. Building
the pending-destroy list therefore costs O(N²) in the number of physics proxies being
unregistered. On a streamed open world with tens of thousands of physics bodies this
degenerates into a multi-minute to multi-hour single-threaded stall.
This is a regression: the same project on UE 5.4 exited PIE in a few seconds.
What Type of Bug are you experiencing?
Editor
Steps to Reproduce
- Open a large World Partition project containing many collision-enabled / simulating primitive components.
- Start PIE (Selected Viewport or New Editor Window — both reproduce).
- Move through the world so streaming loads a substantial amount of the map.
- Stop PIE.
Expected Result
Expected: editor returns to responsive state within a few seconds.
Observed Result
Actual: editor window stops responding. One thread spins at 100% of one core.
Recovery takes 7 minutes to over an hour. The stall duration scales super-linearly with how much of the world was streamed in during the session — consistent with O(N²).
Note: Play → Standalone Game does not reproduce, because teardown happens in the
child process rather than in the editor’s Chaos solver.
Affects Versions
5.8
Platform(s)
Windows
Additional Notes
Evidence
1. CPU sampling (xperf, -a profile -detail, symbols resolved)
Top samples inside UnrealEditor.exe during the stall:
Weight Module!Function
------------ -----------------------------------------------------------------
24,501,561 UnrealEditor-Chaos.dll!TArray<Chaos::FPBDRigidsSolver::FPendingDestroyInfo,
TSizedDefaultAllocator<32> >::AddUniqueImpl<...>
203,260 nvlddmkm.sys!"Unknown"
139,361 ntoskrnl.exe!RtlpLookupFunctionEntryForStackWalks
89,201 UnrealEditor-Core.dll!LowLevelTasks::Private::FWaitingQueue::Park
...
17,028 UnrealEditor-Chaos.dll!`Chaos::FPBDRigidsSolver::UnregisterObject'::`2'::<lambda_1>::operator()
17,007 UnrealEditor-Chaos.dll!Chaos::FEventManager::ClearEvents<Chaos::FCollisionEventData>
The hot function outweighs the next entry by ~120x. FPBDRigidsSolver::UnregisterObject
appears as the caller.
2. The stall is not garbage collection
With LogGarbage=Verbose, the GC itself completes normally and quickly. Reachability
analysis over 5.6M references / 359k objects takes 98 ms; the whole collection is ~370 ms:
LogGarbage: Verbose: 9.718798 ms for MarkObjectsAsUnreachable Phase (78953 Objects To Serialize)
LogGarbage: Verbose: 88.661697 ms for Reachability Analysis
LogGarbage: 98.45 ms for GC - 57018 refs/ms while processing 5613402 references from 359442 objects
LogGarbage: 0.893101 ms for Gather Unreachable Objects (16403 objects collected / 381946 scanned)
LogGarbage: 182.587501 ms for unhashing unreachable objects (16403 objects unhashed)
<-- log stops here; stall begins; no purge progress lines are emitted
For comparison, a mid-session GC in the same run purges incrementally without issue:
LogGarbage: GC purged 6819 objects (381791 -> 374972) in 8.995ms (5 iteration(s))
The stall therefore sits in the object destruction phase that follows unhashing, where
Chaos unregisters its proxies.
3. Resource counters during the stall (6-second sample)
CPU : 6.13 s over 6 s (102% of one core)
page faults : 306
IO operations: 0
IO bytes : 0.0 MB
commit delta : 0.0 MB (stable at 52,505 MB)
working set : +3.6 MB (stable at 11.10 GB)
Pure computation. Rules out paging, disk I/O, allocation pressure and lock contention.
4. Observed stall durations
| PIE stop | Recovery | Stall |
|---|---|---|
| 13:58:52 | 14:05:53 | ~7 min |
| 14:38:10 | 14:58:04 | ~20 min |
| 17:13:07 | 17:39:30 | ~26 min |
| later sessions | up to ~1 hour |
Unreachable object counts at PIE exit in these runs: 16,403 and 21,433 objects, out of
~380k total UObjects.
Analysis
FPBDRigidsSolver::UnregisterObject appends to a TArray<FPendingDestroyInfo> using
AddUnique. AddUnique is O(N) per call, so populating the list for N unregistering
proxies is O(N²). The constant factor is small enough that small levels appear fine,
which is likely why this passed testing, but on a streamed open world N reaches tens of
thousands and the quadratic term dominates completely.
Suggested fix
Replace the linear uniqueness check with a hashed container — e.g. keep a
TSet<FPhysicsProxyHandle> (or TMap) alongside the array for membership testing, or
drop the uniqueness requirement entirely if callers already guarantee it.