Performance — first make it work, then make it survive
The survivors game's stage 08 is this shelf in a single sitting: a measurement stage that builds "nothing new, mostly," puts numbers on every per-frame cost in a 300-enemy scene, and installs exactly one real fix — because it's the one cost that spikes. This shelf is stage 08 without the game: the instruments, the arithmetic, and the two shapes of fix (the pool, the physics contract) in their general form.
The identity is the repo's own testing discipline, applied to frames: measure it, then state
the number. A performance claim without a measurement is a claim, not a fact — the
almanac's architecture doc records a content-visibility optimisation that was added with a
comment claiming it stopped off-screen work, and the measurement showed identical frame
times. The rule stayed; the claim did not. That's this shelf in one paragraph.
What this shelf teaches#
| Part | You build | Right when |
|---|---|---|
| 01 | The Monitor read, the frame-budget arithmetic, and a measurement that can fail loudly | You think something is slow, or you're about to "optimise" something |
| 02 | The per-frame × per-entity model, the frame-skip, and the naive scan vs the hash | Your scene has more than one of a thing, and the thing has a per-frame line |
| 03 | The pool — the reset, the containment, the configure-before-parent — and queue_free vs free | Something is created and destroyed often, and the creating spikes a frame |
| 04 | The body-vs-area cost model, the mask as a performance control, monitoring off | Your entities have physics, and the count is the variable |
The rule the shelf keeps returning to#
Before optimising any line, write down how often it runs. Per frame, per second, per
event — because a line that runs once a second can be absurdly slow and still cost you
nothing, and a line that runs 300 × 60 times a second must be absurdly fast to cost you
anything. The survivors stage 08 nearest-enemy scan is the worked example: a 300-entry loop
that everyone assumes is the bottleneck, measured at tens of microseconds, running a few
times a second (once per attack, not once per frame), and declared — with the number — not
the problem. The cost of a line is per-run cost × run rate, and both factors are
measurements, not vibes.
What performance work actually is#
Not "make it faster" — that's the claim, and the claim is what gets measured. The work is:
- Find the frame's budget and what's in it. 60 fps is 16.6 ms; the Monitor says how much of the 16.6 is physics, how much is process, how much is render. The budget is the denominator of every optimisation that follows.
- Name the cost's shape. Is it steady (scales with the count, always there) or a spike (a burst of allocations, one frame, every clump-death)? The steady cost wants part 02's arithmetic; the spike wants part 03's pool. A fix aimed at the wrong shape is a fix that doesn't land — the pool that doesn't remove the dip, the frame-skip that doesn't move the number.
- Fix the one thing the number justifies. Stage 08's honest finding: the frame-skip was cheap insurance — commenting it out moved the frame time "not visibly." The fix that earned its keep was the one cost that spiked (the float text). One fix, measured, against a number. The temptation is to fix the table; the discipline is to fix the row.
What it isn't#
It isn't premature. A scene that runs at 60 fps with headroom has no performance problem, and
"optimising" it is work spent on a number that isn't there — the frame-skip you add before
the count that needs it is insurance, and stage 08 is the part that decides, with the
count, whether the insurance paid. It isn't general either: the survivors game's 300
Area2D enemies are a cost model the 3D farm's 200 crops aren't (no chase, no overlap
processing at scale), and the steering shelf's thousand agents
is a cost model neither is. The shelf gives you the instruments and the shapes; the numbers
are yours to measure, on your scene, on your machine.
Prerequisites#
- The Monitor panel — Debugger → Monitor. Part 01 is the reading; everything after is the acting on it.
Time.get_ticks_usec— the measurement instrument part 01 installs. A microsecond timer is the difference between "it feels slow" and "it's 3 ms, here's the trace."- A scene with a count in it — the survivors game at stage 08 is the reference, but any project with "more than one of a thing" works. The count is the variable; without it there's no performance question, only a frame time.
Where this shows up in the projects#
The survivors game is the shelf's test case across all four
parts: stage 08 is part 01 (the Monitor, the budget) and part 02 (the per-entity table, the
scan measurement) in a real game; stage 08's pool is part 03 worked (the float text, the
reset, the containment); and stage 02's decision 1 (fake velocity, Area2D not
CharacterBody2D) is part 04's cost model, guessed in stage 02 and measured in stage 08.
The 3D farm is the shelf's negative case — a scene
with a count (the crops) but no scale problem (a farm doesn't have 300 things chasing you),
which is why its performance surface is part 04's (the physics contract: the ground's
collision, the player's body) and not part 02's. A project that doesn't need the shelf is a
project that's measured as not needing it; the 3D farm's stage 06 central tick (the
new_stage != plot.stage guard that re-swaps meshes only on a boundary crossing) is part
02's "pay only as often as the answer changes" applied to a count that's small.
The steering shelf is part 02's stress case — a thousand agents each running a neighbour scan is the naive-scan cost at the count where it stops being microseconds, and the spatial hash is the fix part 02's grid-field diagram previews. Read part 02, then steering part 05, and the diagram's "two blocks, not two panels" comparison is the measurement the hash earns.