How would you approach a 30 FPS regression after adding a new gameplay system?
After merging a new gameplay system (a reactive AI crowd), the build dropped from 60 to 30 FPS. You run stat unit before and after, then capture an Unreal Insights trace (-trace=cpu,frame) and expand a typical frame:
stat unit before: Frame 16.6 Game 8.2 Draw 4.1 GPU 9.0
stat unit after: Frame 33.3 Game 24.5 Draw 4.3 GPU 9.1
Trace, game-thread track:
GameThread
└─ TickActors
└─ CrowdAgent::Tick 0.4 ms x 50 agents = 20 ms
└─ FindPathSync synchronous pathfind every frame
Diagnose the cause.
Profile before and after with stat unit to see which thread regressed, then capture an Unreal Insights trace to locate the new system's cost. Bisect: disable the system to confirm it is the cause before optimizing.
- ✗Optimizing before measuring which thread actually regressed
- ✗Skipping the bisect step that confirms the new system is the cause
- ✗Assuming a gameplay system can only cost game-thread time, never render or GPU
- →How do you tell a render-thread regression from a game-thread one in the trace?
- →Why is a baseline capture before the change essential for this workflow?
Scenario
The team added a new system — say, a reactive AI crowd. Right after the merge the build dropped from 60 to 30 FPS. You need to find the cause, not guess.
Diagnosis
Step 1 — identify the regressed thread. Run stat unit on the branch before and after the change:
before: Frame 16.6 Game 8.2 Draw 4.1 GPU 9.0
after: Frame 33.3 Game 24.5 Draw 4.3 GPU 9.1
Game rose from 8 to 24 ms — a game-thread regression, not a GPU one. Lowering resolution would be pointless.
Step 2 — bisect. Disable the new system (a console variable, a flag, or removing the actor). If FPS returns to 60, the cause is confirmed. If not, the regression is elsewhere and the trace must be read differently.
Step 3 — trace. Capture Unreal Insights with -trace=cpu,frame. Expand a typical frame on the game-thread track and find the new system's blocks:
GameThread
└─ TickActors
└─ CrowdAgent::Tick ← 0.4 ms × 50 agents = 20 ms
└─ FindPathSync ← synchronous pathfinding every frame
The finding: every agent runs a synchronous pathfind every frame.
Fix
- Replace per-frame pathfinding with async queries and cache the route for several frames.
- Raise
TickIntervalfor the agents or spread their updates across frames (time-slicing). - Re-check
stat unit—Gameshould return to ~8 ms.
Only optimize after measurement has pointed to the exact cause.