guest sdk: large-room scale — 1024 lazy frame baselines + cross-callback roster cache#25
Merged
Conversation
…ack roster cache Two changes that take the SDK from 16-player rooms to 1024: rosterCap 16 -> 1024, with lazy per-slot baselines. The SDK silently drops Send for a roster index >= rosterCap, so the old cap was a hard invisible wall for any room beyond 16 players. Each ~45 KiB baseline slot is now allocated on first commit (guest memory tracks the active roster, not the cap), and Identical reconciles only allocated slots — a never-sent-to slot recovers via its first send opening with a keyframe, the same path as a roster change. No wire/ABI change. Cross-callback roster cache. The host re-sends the full member list in every callback payload, but rosters change only on join/leave/index- shift. decodeCtx now skims the member section's raw bytes (additive wire.(*Rd).SkipStr) and memcmps them against the previous callback's: on a match the previously decoded []Player is reused with zero allocation; only a real change re-decodes. Replaces the roster fingerprint hash (the byte compare is strictly stronger). Under -gc=leaking the old per-callback decode leaked the roster at callback rate (~100 KB/callback at 1000 players — guest OOM within seconds of large-room play); steady-state callbacks are now allocation-free in the member path. Members() slices are valid for the duration of the callback — copy retained Players (long-lived state keys by AccountID, as before). Load-benchmarked at 50..1000 players (200 ticks/size): mean input delivery halved at every size, guest heap growth halved under -gc=leaking, hibernation snapshots ~6x smaller at 1000 players. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
BCook98
marked this pull request as ready for review
June 7, 2026 00:01
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Takes the guest SDK from 16-player rooms to 1024, found while load-testing large rooms (50→1000 players, 200 measured 50ms ticks per size, roguelike-shaped guest with per-player scrolling viewports).
1.
rosterCap16 → 1024, lazy per-slot baselinesThe SDK silently drops
Sendfor a roster index ≥rosterCap— the old 16 was a hard invisible wall for any larger room. Raising it naively would cost a ~47 MiB static baseline table, so per-slot baselines (~45 KiB each) are now allocated on first commit: guest linear memory tracks the active roster, not the cap.Identicalreconciles only allocated slots; a never-sent-to slot stays not-present and recovers via its first per-playerSendopening with a keyframe (unconditionally accepted) — the same recovery path as a roster change. No wire/ABI change (the send index was already u32-framed).2. Cross-callback roster cache
The host re-sends the full member list in every callback payload, but rosters only change on join/leave/index-shift.
decodeCtxnow skims the member section's raw bytes (additivewire.(*Rd).SkipStr) and memcmps against the previous callback's region:[]Playeris reused, zero allocationsrosterFingerprinthash; the byte compare is strictly stronger)This matters because under
-gc=leaking(the recommended profile while the TinyGo GC issue is open) the old per-callback decode leaked the roster at callback rate — measured ~100 KB/callback at 1000 players, OOMing the guest within seconds of large-room play.Lifetime contract change: the slice from
Room.Members()is valid for the duration of the callback; copy anyPlayeryou retain (long-lived state keys byAccountID, as before — no example game is affected).Measured (Apple M4 Pro, 200 ticks/size, conservative-GC guest)
-gc=leaking)Tick-time scaling (wake compose+send path) is unchanged by design — this PR removes the per-callback roster tax, not the render cost.
Known follow-up (out of scope here)
The remaining O(members)-per-callback cost is the payload itself: the host re-encodes and the guest re-copies the full roster bytes every callback (
inputBytes()alloc — still leaked under-gc=leaking). Removing it means not resending an unchanged roster — a protocol-level change (e.g. roster epoch + on-change resend) that needs host-side coordination and an ABI design pass.Tests
TestRosterCache— cache hit reuses the backing slice; join/leave re-decode; first-callback semanticsTestLazyBaselineHighSlot— slot 900: keyframe-then-delta with lazy allocation (regression guard for the silent-drop wall)TestIdenticalThenSendReconciles-adjacent host conformance updated separately in the enginego test ./...green🤖 Generated with Claude Code