Skip to content

perf(server): reuse Entity.tick() event payload to eliminate per-tick allocations#29

Open
RZDESIGN wants to merge 1 commit intohytopiagg:mainfrom
RZDESIGN:perf/entity-tick-payload-reuse
Open

perf(server): reuse Entity.tick() event payload to eliminate per-tick allocations#29
RZDESIGN wants to merge 1 commit intohytopiagg:mainfrom
RZDESIGN:perf/entity-tick-payload-reuse

Conversation

@RZDESIGN
Copy link
Copy Markdown

@RZDESIGN RZDESIGN commented Mar 5, 2026

Summary

Entity.tick() is the server's hottest path — it fires for every entity on every tick (default 20 ticks/s). The current implementation allocates a fresh { entity, tickDeltaMs } object each time and immediately discards it after the event listeners return. With 100+ entities in a world this creates 2 000+ short-lived objects per second, all of which become garbage and pressure the GC into more frequent pauses.

This PR caches the payload as a private field on each Entity instance and only mutates the tickDeltaMs value before emitting. The object reference stays stable across ticks — no allocation, no GC pressure.

Changes

File Change
server/src/worlds/entities/Entity.ts Added _tickPayload field, reuse it in tick() instead of allocating a new object

Net diff: +1 field declaration, +1 assignment, −1 object literal (3 meaningful lines).

Risk Assessment

  • The event payload type { entity: Entity, tickDeltaMs: number } is unchanged — listeners receive the exact same shape.
  • The payload is mutated before emit() and only read during emit() — there is no window where stale data could be observed.
  • If a listener were to store a reference to the payload and read it later, they would see the latest tick's tickDeltaMs instead of the one from their tick. This is a theoretical concern only — all existing listeners and the documented pattern consume the value synchronously. If this ever becomes a concern, a Object.freeze() in dev mode could catch it.

Test Plan

  • Run a world with 100+ entities and verify EntityEvent.TICK listeners still receive correct tickDeltaMs values
  • Monitor server memory / GC pauses before and after — expect measurable reduction in minor GC frequency
  • Verify no behavioral change in entity controllers (BaseEntityController.tick still called correctly)

Entity.tick() previously allocated a new { entity, tickDeltaMs } object
on every tick for every entity.  At 20 ticks/s with 100+ entities this
produces 2 000+ short-lived objects per second, all of which pressure
the garbage collector.

Cache the payload as a private field on each Entity and only update the
tickDeltaMs value before emitting.  This reduces GC pressure on the
server hot path with zero behavioral change.

Made-with: Cursor
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant