The hitch at the edge of the map
The hitch at the edge of the map
Some regressions don't show up in an average. This one hid in the p95 and would've stayed hidden if I'd only looked at "avg FPS: 59."
I was building an open-ish world that streams terrain chunks as you move. Fly around the middle of a chunk and everything's silky. Cross a boundary at full sprint and — hitch. A single dropped frame, maybe two. Just enough to feel cheap. The kind of thing a player can't quite describe but definitely notices.
My average frame time said nothing was wrong, because one bad frame every ten seconds barely moves an average. What flagged it was the frame timeline — scrubbing the capture, I could see a lone 40ms spike sitting in an otherwise flat 16ms sea, and it lined up exactly with a chunk-boundary crossing.
"It's the loading" — except it wasn't
Obvious first guess: disk I/O, or mesh instantiation, blocking the main thread. So I opened the spike. The per-frame breakdown said otherwise — the render thread was calm, the "scripts" bucket owned almost the whole 40ms. The async load itself was already off-thread and behaving. Whatever was stalling happened in my code, right after the chunk arrived.
I wrapped the two suspects and re-captured:
using (Argus.Region("Chunk.Activate")) {
ActivateChunk(chunk); // enable renderers, register colliders
}
using (Argus.Region("Chunk.Bake")) {
RebuildNavMeshTile(chunk); // ...this one
}
The spike landed squarely in Chunk.Bake. I was rebuilding a navmesh tile synchronously the instant a chunk activated — on the main thread, in one frame, right as the player crossed the seam. Fine on my desktop, where it cost 3ms. On a real device under thermal load, 40.
The fix, and the part that matters
I spread the bake across a few frames and gated it behind a "player isn't standing on this tile yet" check. The 40ms spike dropped to a couple of 6ms ones nobody will ever feel.
What I want to flag isn't the navmesh gotcha — it's that three things had to line up for me to catch this at all: a per-frame timeline I could scrub (not just an average), a per-spike subsystem breakdown that told me scripts, not I/O, so I didn't waste an hour optimizing the loader, and a region that pinned it to one function. Any one of those missing and I'm guessing.
Averages lie by omission. The frame that ruins the feel is often a single outlier, on a single device, at a single moment — the boundary crossing, the boss spawn, the inventory open. Capture the timeline, read the breakdown, wrap the suspect. The hitch at the edge of the map was one function on one thread on one frame. Now it's gone, and I found it before anyone crossed that boundary but me. 👁️
Catch your own regressions before players do.
Free tier, no card — first capture in about ten minutes.