How do you debug intermittent frame hitching in a packaged build?
A packaged Development build holds 60 FPS but stutters every ~30-60 seconds. stat unit shows Frame periodically jumping from 16 ms to 45 ms. You capture an Unreal Insights trace (-trace=cpu,gpu,frame,loadtime,memory) and expand one spiking frame on the Frames track:
stat unit (spiking frame): Frame 45.2 Game 44.0 Draw 4.1 GPU 9.0
Frames track @ spike, even ~45s cadence:
GameThread 44 ms
└─ CollectGarbage 38 ms (blocks the frame)
GPU 9 ms
loadtime 0 ms
Diagnose the cause.
Capture an Unreal Insights trace and find the spiking frame, then read which track widened — game thread, GPU, GC, or asset streaming. The spike's call stack names the culprit; stat unit only confirms a hitch happened.
- ✗Trusting average FPS, which hides a single-frame spike
- ✗Assuming hitches are always GPU-bound and ignoring GC or streaming spikes
- ✗Debugging only in the editor where streaming and GC behave differently
- →How would you confirm a hitch is caused by a garbage-collection pause?
- →What settings reduce hitching from synchronous asset loading?
Scenario
A Development packaged build holds a steady 60 FPS, but every few seconds a frame "stutters" — a visible jolt. stat unit shows Frame periodically jumping from 16 ms to 45 ms, but which thread is at fault is unclear.
Diagnosis
Run the build with tracing enabled and open the trace in Unreal Insights:
MyGame.exe -trace=cpu,gpu,frame,loadtime,memory
In the Timing Insights window, find the spike on the Frames track and expand that frame:
- The game thread widened — look for a long block: a heavy
Tick, synchronous load, or logic spike. - A
CollectGarbagebar appears — a garbage-collection pause; the frame is blocked while GC scans theUObjectgraph. - The
GPUtrack widened — a render spike: a heavy effect or shadow update. - A
loadtime/streaming event shows — a synchronous asset or level load inside the frame.
A typical finding: even spikes every ~30–60 seconds with a CollectGarbage bar — that is garbage collection.
Fix
For garbage-collection hitches:
- Cut runtime
UObjectallocation — recycle actors through a pool instead ofSpawnActor/Destroy. - Tune
gc.TimeBetweenPurgingPendingKillObjectsand enable incremental purge (gc.IncrementalBeginDestroyEnabled) to spread the pause out.
For synchronous loading, switch to async streaming (StreamableManager, Async Load Asset). For Tick spikes, move heavy logic into timers or spread it across frames.