perf(net): near-real-time sandbox for non-host viewers

Two causes made a shared shell's PTY stream lag for non-host viewers and
then arrive "all at once":

1. Nagle's algorithm — PTY echo/keystrokes/chat are tiny frames; Nagle +
   delayed-ACK (amplified by Tailscale RTT) coalesced them into bursts.
   Set TCP_NODELAY on the client websocket (Plain/--no-tls path) and on
   each server connection's socket (via ws.io_proto.transport).

2. Head-of-line blocking — ConnectionManager.broadcast awaited each
   send() serially under a global lock, so the slowest viewer gated
   delivery to everyone AND stalled the server's read of the broker's
   next frame, backing the stream up. Give each connection its own
   bounded outbound queue + writer task; broadcast now only enqueues and
   never waits on a socket. A peer that overflows its backlog (4096) is
   evicted (close 1013) and resyncs on reconnect rather than buffering
   without bound or being fed a gapped terminal stream. Init snapshot is
   enqueued as the guaranteed-first frame (send_state -> state_frame).

Adds tests/test_manager.py (FIFO, slow-client isolation, wedged
eviction, reconnect replacement). Also fmt-clean sbx.rs/theme.rs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
leetcrypt
2026-06-06 17:11:51 -07:00
parent c552ece6b1
commit 3a54578f0a
7 changed files with 318 additions and 42 deletions
+12 -11
View File
@@ -24,19 +24,20 @@ def get_client_ip(request: Request) -> str:
return request.ip
async def send_state(ws: Websocket, app: Sanic) -> None:
def state_frame(app: Sanic) -> str:
"""Build the per-client `init` snapshot (message backlog + roster) as a JSON
string. Returned (not sent) so it can be enqueued as the connection's first
outbound frame, guaranteeing it precedes any later broadcast."""
messages = app.ctx.message_store.get_all()
users = app.ctx.session_store.get_all()
await ws.send(
json.dumps(
{
"type": "init",
"messages": [asdict(m) for m in messages],
"users": [
{"user_id": u.user_id, "username": u.username} for u in users
],
}
)
return json.dumps(
{
"type": "init",
"messages": [asdict(m) for m in messages],
"users": [
{"user_id": u.user_id, "username": u.username} for u in users
],
}
)