Skip to content

perf(server): reuse position/rotation arrays in network entity sync#32

Open
RZDESIGN wants to merge 1 commit intohytopiagg:mainfrom
RZDESIGN:perf/reuse-position-rotation-sync-arrays
Open

perf(server): reuse position/rotation arrays in network entity sync#32
RZDESIGN wants to merge 1 commit intohytopiagg:mainfrom
RZDESIGN:perf/reuse-position-rotation-sync-arrays

Conversation

@RZDESIGN
Copy link
Copy Markdown

@RZDESIGN RZDESIGN commented Mar 5, 2026

Summary

Physics runs at 60 Hz but network synchronization runs at 30 Hz (every 2 ticks, controlled by TICKS_PER_NETWORK_SYNC). Each tick, Entity.checkAndEmitUpdates() fires UPDATE_POSITION / UPDATE_ROTATION events for every moving entity, which the NetworkSynchronizer handles by allocating a new [x, y, z] or [x, y, z, w] array and assigning it to the entity sync object.

Since the sync object persists across the 2-tick window, the first tick's array is silently discarded when the second tick overwrites it — creating a throwaway array that immediately becomes garbage.

Before

entitySync.p = [ payload.position.x, payload.position.y, payload.position.z ];
entitySync.r = [ payload.rotation.x, payload.rotation.y, payload.rotation.z, payload.rotation.w ];

Every call allocates a new array, even when one already exists on the sync object.

After

const p = entitySync.p;
if (p) {
  p[0] = payload.position.x;
  p[1] = payload.position.y;
  p[2] = payload.position.z;
} else {
  entitySync.p = [ payload.position.x, payload.position.y, payload.position.z ];
}

Reuse the existing array when present (in-place index writes); only allocate on the first update in each sync cycle.

Impact

With 100 moving entities, this eliminates ~6,000 short-lived arrays per second (100 entities × 2 arrays × 30 wasted allocations/s). On busy worlds with 200+ entities the savings double. This directly reduces minor GC frequency and pause time on the server.

Changes

File Change
server/src/networking/NetworkSynchronizer.ts _onEntityUpdatePosition: reuse existing .p array via index writes
server/src/networking/NetworkSynchronizer.ts _onEntityUpdateRotation: reuse existing .r array via index writes

Risk Assessment

  • Correctness: The arrays still contain the exact same values — the serializer (msgpackr) reads indices [0], [1], [2] (and [3] for quaternions), which are identical whether the array is new or reused.
  • Sync lifecycle: The sync object (and its arrays) is created fresh each sync cycle via _createEntitySync{ i: entity.id! }. On first position update in a cycle, the else branch allocates. On subsequent ticks in the same cycle, the if branch reuses. After _clearSyncQueue, everything is discarded cleanly.
  • No observable difference: The only change is when the array object is allocated, not what it contains.

Test Plan

  • Run a world with 100+ moving entities — verify all entity positions/rotations sync correctly to connected clients
  • Monitor server GC (Bun --smol or V8 --trace-gc) — expect fewer minor GC cycles
  • Stress test with rapid position changes (physics-heavy scene) — verify no stale data leaks across sync cycles

Physics runs at 60 Hz but network sync at 30 Hz (every 2 ticks).
Each tick, _onEntityUpdatePosition and _onEntityUpdateRotation
allocated a fresh [x,y,z] / [x,y,z,w] array for every moving entity,
even though the previous tick's array was already sitting on the same
sync object.  The first tick's array became garbage on tick 2.

With 100 moving entities this produced ~6 000 throwaway arrays per
second.  Reuse the existing array when one is already present on the
sync object, falling back to allocation only on the first update in
each sync cycle.

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