Diagnosing "it stutters" tickets ① — the four causes of frame drops

CPU-bound, GPU-bound, GC spikes, loading hitches. Frame drops need classification before measurement, because the four have opposite fixes.

  • unity
  • performance
  • qa

Diagnosing "it stutters", part 1 of 3 — ① The four causes · ② Measuring in practice · ③ Capturing the moment (③ coming next)

Performance tickets are the hardest bug tickets to reproduce. A developer picks up "the game stutters when I open the inventory," opens the inventory, and it's smooth — their machine is faster than the QA box. The ticket closes, and after launch the reviews say "needs optimization."

This series is about making those tickets diagnosable. The first step isn't measurement, and it isn't optimization. It's classification.

Why classification comes first

"It stutters" is one symptom, but the cause is usually one of four — and the four fixes point in opposite directions. Optimizing scripts in a GPU-bound game changes nothing. Lowering graphics quality in a GC-spike game leaves the hitch exactly where it was. Optimizing before classifying is digging in a random direction.

1. CPU-bound — the main thread can't keep up

Scripts (Update), physics, animation, or UI layout exceed the frame budget (16.6ms at 60fps).

Signature: frame times are high across the board and get proportionally worse in specific situations — many enemies on screen, complex UI. Lowering the resolution changes nothing, which is the fastest way to tell this apart from GPU-bound.

2. GPU-bound — the drawing side can't keep up

Too many draw calls, overdraw, heavy shaders, high resolution.

Signature: dropping the resolution or quality preset improves it immediately. CPU frame time has headroom while the screen still stutters — the time the CPU spends waiting on the GPU (Gfx.WaitForPresent and friends) shows up long.

We hit exactly this. Stuttering was reported only on Windows, and we suspected our own SDK's always-on capture. Measurement said otherwise: the stutter was identical with the SDK fully disabled, and the game itself was GPU-bound. Intuition and circumstantial evidence name the wrong culprit surprisingly often. How that verdict was reached is part 2.

3. GC spikes — a big stall every few seconds

Every frame is fine, then every few seconds a hitch of tens of milliseconds lands. Managed heap allocations made every frame — string concatenation, closures, LINQ, freshly new'd collections — pile up until GC runs.

Signature: the frame time graph is flat with periodic needle-shaped spikes. The interval between spikes is inversely proportional to allocation rate, so if the gap shortens during allocation-heavy moments like combat, it's all but confirmed.

4. Loading hitches — a single stall at a specific moment

Scene transitions, asset loads, an object appearing for the first time, a shader compiling for the first time — a one-off stall bound to an event.

Signature: no periodicity, and clear reproduction conditions. "Only when this boss first appears." "Only the first time I enter this map." If it doesn't stutter from the second time on (caches warmed, compilation done), it's almost certainly this. Shader compilation hitches often occur only in builds and never in the editor, which makes them especially easy to close as "cannot reproduce."

Reading classification hints out of the ticket

What the ticket saysLikely class
"It's heavy overall once combat starts"CPU-bound
"It got better when I lowered the settings"GPU-bound
"It catches every few seconds"GC spike
"Only stalls the first time I enter this map"Loading hitch

This table is circumstantial evidence, of course. Confirming it takes measurement, and turning QA's felt descriptions into measured data is the subject of part 2.


Performance tickets die as "cannot reproduce" for the same structural reason ordinary bugs do — no evidence survived the moment. What a ticket has to carry is covered in the bug reporting series.