Argus
← All posts

The boss wave that quietly ate 8ms

Romanperformancegcunitycase-study

The boss wave that quietly ate 8ms

Here's a regression that would've shipped if I'd been profiling the way I used to.

I'd spent a week on a boss encounter — three escalating waves, each spawning more enemies than the last. On my dev machine it ran at a locked 60. On the iPhone I keep on my desk, also fine. I would have called it done.

But the capture from a mid-range Android — the kind of phone most of my players actually own — told a different story. Waves one and two: smooth, ~16ms frames. Wave three: a cluster of 24ms spikes, every couple of seconds, like clockwork.

The diff pointed before I did

I pulled up the same encounter from the build a week earlier and diffed them. Frame time up 8ms on that device, and — this is the part that saved me an afternoon — the spike rows were all tagged with the same region and the same dominant subsystem: gc. Not render. Not physics. Garbage collection.

That immediately killed a bunch of theories. It wasn't the extra enemies' draw calls (I'd have seen render). It wasn't their AI (I'd have seen scripts). Something was allocating on wave three specifically, and the collector was catching up mid-combat.

I wrapped the wave-spawn path in a region — one line:

using (Argus.Region("Wave.Spawn")) {
    SpawnEnemiesForWave(wave);
}

Next capture, the spikes landed inside Wave.Spawn. From there it was a two-minute find: the third wave passed a different difficulty config, and that path built a fresh array of modifiers per enemy, per frame while they streamed in. Waves one and two used a cached list; wave three had been written later and never got the cache. Classic.

The fix was boring. The finding wasn't.

Cache the modifier list, allocate once. The 24ms spikes flattened back to 16ms. Total code change: about four lines.

The point isn't the fix — GC spikes are a beginner-to-veteran rite of passage and you've all fixed a dozen. The point is how I found it. I didn't get a one-star review. I didn't spend an evening bisecting builds with the profiler attached. I looked at a colored diff, saw gc on the spike, and knew within a minute it was an allocation on a code path that only ran on wave three, on devices without the memory headroom to hide it.

That's the whole reason Argus exists: the regression was real, it was device-specific, it was subtle, and it announced itself in a diff instead of in the reviews.

Wrap your hot paths in a region, capture on a real mid-range device, diff against last week. The 8ms was always there — I just used to find out about it much later. 👁️


Catch your own regressions before players do.

Free tier, no card — first capture in about ten minutes.