doc 1 of 5

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#

PartYou buildRight when
01The Monitor read, the frame-budget arithmetic, and a measurement that can fail loudlyYou think something is slow, or you're about to "optimise" something
02The per-frame × per-entity model, the frame-skip, and the naive scan vs the hashYour scene has more than one of a thing, and the thing has a per-frame line
03The pool — the reset, the containment, the configure-before-parent — and queue_free vs freeSomething is created and destroyed often, and the creating spikes a frame
04The body-vs-area cost model, the mask as a performance control, monitoring offYour 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.