perf: 100 awake idle NPCs cost ~2 ms of frame more than the bare swap (Oxide) #13

Open
opened 2026-10-07 02:33:35 +00:00 by wtclaude · 0 comments
Member

Found by stage 9's performance check (D306, rnt.cost in tools/RunicNpcTest.cs). The org lead's call on 2026-10-06: update the cost warning with today's numbers, log this as an optimisation fix, and keep going. Not a v1.0.0 blocker.

The numbers

How much 100 NPCs raise the median server frame over the same session's empty server. 6000 map (seed 981448696), the other rig stopped. Rust 25681086, Oxide 2.0.7801, Carbon 2.0.262. "Harness" is stage 1's bare swapped scientist (rnh.cost), measured in the same session.

100 NPCs Oxide: harness Oxide: RunicNPC Carbon: harness Carbon: RunicNPC
Idle, awake (a stand-in 85 m away, beyond their senses) +0.96 ms +2.65 / +2.92 ms (two runs) +0.97 ms +1.52 ms
Idle, asleep (nobody within 160 m) — — — +0.52 ms
Fighting 20 invulnerable stand-ins +7.53 ms +7.19 ms +7.96 ms +9.04 ms (687 hits vs the harness's 351)
  • Fighting costs what Rust's own AI costs. Stage 1's own NPC is over stage 1's +5.2 ms bar too: this rig and Rust build are slower than in September (empty Oxide frame 22–30 ms now, 16.4 ms then).
  • Awake idle on Oxide is about +2 ms per 100 over the bare swap. That overhead is RunicNPC's. With the bridge unloaded it was still +2.18 ms, so the bridge accounts for about 0.5 ms at most. Standing still (sentries) did not lower it, but that run was noisy (baselines 28.8 and 21.8 ms).
  • On Carbon the same overhead is about +0.5 ms.

Where to look

What RunicNPC adds to every think of an awake NPC, beyond Rust's own base.Think:

  • RunicNpcBrain.Think: every 2 s, PlayerWithin (the sleep check), Reground → OffMeshReason (a navmesh sample and more), CheckEscort and the tether check.
  • Fight(...) on every think, even with no faction settings: HuntsOf, brain.FightingAPlayer() (a loop over memory targets), KitTick when the profile uses kit items, and the Stopwatch around it.
  • The re-implemented IAISenses / IAIAttack members (IsTarget, GetBestTarget, …), which Rust calls on every sense.
  • The bridge's per-NPC work (≤ 0.5 ms).

Profile one think at a time (a Stopwatch around each part, reported by rnpc.status), then cut what dominates. Re-measure with rnt.cost 60 and rnt.cost 40 idle, and with stage 1's rnh.cost 60 in the same session as the reference.

Done when

Awake idle on Oxide is within 1 ms of the bare swap in the same session, and the cost warning (CostWarning, now 3 ms idle awake, 0.5 ms asleep, 9 ms fighting per 100) is lowered to match.

Found by stage 9's performance check (D306, `rnt.cost` in `tools/RunicNpcTest.cs`). The org lead's call on 2026-10-06: update the cost warning with today's numbers, log this as an optimisation fix, and keep going. Not a v1.0.0 blocker. ## The numbers How much 100 NPCs raise the median server frame over the same session's empty server. 6000 map (seed 981448696), the other rig stopped. Rust 25681086, Oxide 2.0.7801, Carbon 2.0.262. "Harness" is stage 1's bare swapped scientist (`rnh.cost`), measured in the same session. | 100 NPCs | Oxide: harness | Oxide: RunicNPC | Carbon: harness | Carbon: RunicNPC | |---|---|---|---|---| | Idle, awake (a stand-in 85 m away, beyond their senses) | +0.96 ms | **+2.65 / +2.92 ms** (two runs) | +0.97 ms | +1.52 ms | | Idle, asleep (nobody within 160 m) | — | — | — | +0.52 ms | | Fighting 20 invulnerable stand-ins | +7.53 ms | +7.19 ms | +7.96 ms | +9.04 ms (687 hits vs the harness's 351) | - **Fighting costs what Rust's own AI costs.** Stage 1's own NPC is over stage 1's +5.2 ms bar too: this rig and Rust build are slower than in September (empty Oxide frame 22–30 ms now, 16.4 ms then). - **Awake idle on Oxide is about +2 ms per 100 over the bare swap.** That overhead is RunicNPC's. With the bridge unloaded it was still +2.18 ms, so the bridge accounts for about 0.5 ms at most. Standing still (sentries) did not lower it, but that run was noisy (baselines 28.8 and 21.8 ms). - On Carbon the same overhead is about +0.5 ms. ## Where to look What RunicNPC adds to every think of an awake NPC, beyond Rust's own `base.Think`: - `RunicNpcBrain.Think`: every 2 s, `PlayerWithin` (the sleep check), `Reground` → `OffMeshReason` (a navmesh sample and more), `CheckEscort` and the tether check. - `Fight(...)` on every think, even with no faction settings: `HuntsOf`, `brain.FightingAPlayer()` (a loop over memory targets), `KitTick` when the profile uses kit items, and the Stopwatch around it. - The re-implemented `IAISenses` / `IAIAttack` members (`IsTarget`, `GetBestTarget`, …), which Rust calls on every sense. - The bridge's per-NPC work (≤ 0.5 ms). Profile one think at a time (a Stopwatch around each part, reported by `rnpc.status`), then cut what dominates. Re-measure with `rnt.cost 60` and `rnt.cost 40 idle`, and with stage 1's `rnh.cost 60` in the same session as the reference. ## Done when Awake idle on Oxide is within 1 ms of the bare swap in the same session, and the cost warning (`CostWarning`, now 3 ms idle awake, 0.5 ms asleep, 9 ms fighting per 100) is lowered to match.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: RunicGateway/runicnpc-rust#13
No description provided.