# Script performance: writing behaviours that stay cheap

Behaviours run on QuickJS, an interpreter. A call into the engine (`ctx.entity.position`, a
`ctx.world.query` result's `.position`, `ctx.events.emit`) costs far more than the arithmetic around
it, and every `[x, y, z]` a helper returns is an allocation. A behaviour that does a little work for
each of many things can cost milliseconds a frame without doing anything expensive. This page is
what to write instead. **Every pattern here uses the API as it is; none changes what a game does.**

**In one line:** in a hot `update`, ask the engine for the answer (`query({ near, sort: "distance",
limit: 1 })`, `ctx.world.positions(list)`, `ctx.events.emitTo`) rather than reading every handle, and
keep vector maths in number locals.

## Find what costs first

`GET /profile/frames` (`crates/query/API.md`) names the biggest behaviour call of each frame
(`top_behaviour`) and each frame's `script_ms`; `GET /profile/trace` opens the frames in Perfetto,
one span per behaviour call. Measure before rewriting: a behaviour that runs once a second is not
worth touching.

## The patterns, biggest saving first

| Instead of | Write | Why |
|---|---|---|
| `query({ near, radius })`, then reading `.position` of every result to find the nearest | `query({ near, radius, sort: "distance", limit: 1 })[0]` | The engine sorts; the script reads one handle, not every one. Each result handle and each `.position` read is an engine call |
| `for (const f of foes) f.position` | `const ps = ctx.world.positions(foes)` (one `Float32Array`, 3 numbers each) | One call for the whole list |
| `ctx.events.emit("hit", { to })` with every receiver checking `to` | `ctx.events.emitTo(target, "hit", data)` | `emit` reaches every behaviour: N senders and N receivers is N × N handler calls |
| one `update` per instance of a behaviour placed on hundreds of entities | `updateAll(ctx, dt, batch)` (`docs/API.md`, "One call for every instance") | One host call a tick instead of one per instance |
| `add(scale(sub(b, a), t), a)` in a per-frame loop | `const x = a[0] + (b[0] - a[0]) * t` (and y, z) in number locals | Each helper returns a new array; locals allocate nothing |
| reading `ctx.entity.position` several times in one call | read it once into a local | Every read is an engine call and a new array |

## Recipe: a behaviour on many entities

Enemies, a crowd, pickups, coins, drones: one behaviour placed on dozens or hundreds of entities. Write it this way
from the start; the starter templates' pickups and spinners do (`crates/editor/templates`), and the
`many-entities` recipe (`fixtures/recipes/many-entities`, 200 drones) is a whole worked project.

1. **One call a tick for all of them:** export `updateAll(ctx, dt, batch)` instead of `update`. `batch.entities[k]`
   and `batch.states[k]` are instance `k`'s handle and its own `ctx.state` (docs/API.md, "One call for every
   instance").
2. **Copy props into state in `init`.** A batch has each instance's state, not its props, so `init` copies what the
   batch needs (`radius`, `speed`, a home position) into `ctx.state`.
3. **Read positions in bulk:** `ctx.world.positions(batch.entities)`: one `Float32Array`, 3 numbers each.
4. **Ask the engine for the nearest:** `ctx.world.nearestMany(points, targets, radius)` for every instance at once; a
   behaviour on one entity uses `ctx.world.query({ near, radius, sort: "distance", limit: 1 })[0]`. Never read every
   result's `.position` to find the nearest yourself.
5. **Tell one entity, not everyone:** `ctx.events.emitTo(target, name, data)`; `emit` reaches every behaviour in the
   scene, N senders by N listeners.
6. **Number locals in the loop,** not vector helpers: each helper returns a new array.

`ctx` in `updateAll` is the first instance's, so per-instance work goes through `batch.entities[k]` and
`batch.states[k]`. The budgets are per call and a throw disables the whole batch: keep the loop linear.

Measured, the collection-course pickup at 200 pickups (no player near, the common case; `crates/app/tests/recipes.rs`,
`many_entities_pickup_cost_per_entity_against_update_all`, Mars dev build under load, 600 ticks):

| Pickup written as | script ms a tick, median / p95 |
|---|---|
| per-entity `update`, one `query({ near, radius })` each (the template before) | 0.205 / 0.246 |
| `updateAll`, `positions` + `nearestMany` (the template now) | 0.105 / 0.123 |

About 1.9 times cheaper with the same outcome; the saving grows with what each instance does (the 200-bot case below
is five times).

## What it costs (measured)

Release build, one CPU core of the Triton workstation (wave117), `crates/app/tests/script_cost.rs`.
Microseconds per behaviour call; lower is better. These are this machine's numbers, for comparing
patterns, not a budget.

| Behaviour | Idiomatic (allocating helpers) | Number locals | Number locals, no engine calls | `updateAll` |
|---|---|---|---|---|
| sway, 24 objects (4 engine calls each) | 3.8 | 2.6 | 1.0 | 2.0 per object |
| spring follow, 8 followers | 4.2 | 2.7 | | |
| kart-like controller, 8 karts (~32 vector operations) | 9.6 | 4.9 | | |

One engine call costs about 0.4-0.7 µs; one allocating vector operation about 0.15 µs.

### A worked case: 200 bots, each finding its nearest enemy

`fixtures/netcode/arena-200`'s `combat.js` queries the enemies within 30 m of each bot every tick and
reads every result's position to find the nearest (about 75 results a query). The same game, rewritten
one pattern at a time (median of three runs; ms of `combat.update` a tick for all 200 bots):

| `combat.js` | ms a tick | engine calls a tick |
|---|---|---|
| as checked in: `query({ near, radius })`, `.position` of each result | 10.9 | 16,883 |
| the results' positions in one `ctx.world.positions(foes)` | 8.1 | 2,165 |
| `query({ near, radius, sort: "distance", limit: 1 })` | 2.1 | 2,166 |
| that, and `emitTo(best, …)` instead of `emit` | 2.1 | 839 |

Asking the engine for the nearest one is five times cheaper than finding it in the script: each
result of a query is a handle the engine builds for the script, so a query that answers 75 handles
costs 75 builds even when the script only wants one.

