Skip to content

improve: coalesce playerUpdate frames to cut websocket spam - #242

Closed
snakkeeh wants to merge 1 commit into
PerformanC:devfrom
snakkeeh:feat/player-update-coalescing
Closed

snakkeeh wants to merge 1 commit into
PerformanC:devfrom
snakkeeh:feat/player-update-coalescing

Conversation

@snakkeeh

Copy link
Copy Markdown
Contributor

Changes

  • Added playerUpdate coalescing in Player._sendUpdate: leading-edge send for the first frame in an idle window, trailing debounce (newest-only) for later frames in the same window, and duplicate suppression when position/connected/ping match the last delivered frame.
  • Lifecycle transitions bypass the debounce and send immediately (seek, pause, track, connection, recover), so clients always converge on state changes without delay.
  • Added Player.flushPlayerUpdate() for explicit convergence (track end, resume flush paths) and _clearPlayerUpdateCoalescer() on destroy so dead players never flush stale frames.
  • Added queuePlayerUpdate helper in utils.ts: paused, resumable sessions keep only the newest playerUpdate frame (newest-wins eviction) instead of appending every position; non-resumable sessions drop them.
  • Restructured the per-tick loops in serverMonitor.ts and the cluster playerUpdateTimer in workers/main.ts into two phases (zombie checks, then one update per player), so N players produce at most N frames per tick.
  • Extended the playback Session, worker player, and cluster snapshot types with the coalescer state and flushPlayerUpdate.

Why

  • Every player emitted a playerUpdate frame on each state transition plus once per tick, so flappy connections or sponsorblock-heavy tracks produced bursts of near-identical { position, connected, ping } payloads per player — pure websocket spam.
  • Paused sessions queued every position frame for resume replay, where only the newest position is useful; stale frames wasted queue space and replay bandwidth.

Checkmarks

  • I have tested the changes locally (burst coalescing, duplicate suppression, force bypass, destroy cleanup, and paused-session routing verified programmatically).
  • I have run the linter/formatter (code follows the existing style conventions).
  • I have tested the changes on a real bot (manual playback with pause/seek/track-change flow).

Additional info

  • The trailing-flush delay is derived from the configured playerUpdateInterval (a quarter of it, clamped to 50-500ms), so operator tuning is respected.
  • Behavior for state transitions is unchanged: they still send immediately, only steady-state position updates are coalesced.

@1Lucas1apk

Copy link
Copy Markdown
Member

I'm going to hold off on this for now because the player is currently being refactored in #250.

This PR changes the same player update logic that is being moved into PlayerPlaybackMonitor as part of that refactor. I'd rather wait until the new structure is settled and revisit this optimization there, instead of integrating it into the current implementation and potentially having to rework it afterward.

I think the idea is worth considering, but I'd prefer to revisit it once the refactor is in a better place.

@1Lucas1apk 1Lucas1apk closed this Oct 20, 2026
@github-actions github-actions Bot locked and limited conversation to collaborators Oct 20, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants