Repository navigation
1.4.0: interaction preview flow control behind proxies, preview JPEG quality setting - #19
Merged
Merged
Conversation
…oxy. A reverse proxy between the function and the browser reads the previews at once and holds them in its own buffers, so TCP_NOTSENT_LOWAT on our socket does not help. Lab (50 KB previews, 25 fps asked, 40 ms round trip, a buffering userspace proxy with a shaped link to the client): 1.3.4 delivered 10-11 s old previews at 2 Mbit/s; at 500 kbit/s the delay grew to 14 s and the session was closed as stuck (outbox_age) after 20 s, ending control. - Preview flow control: a client that answers each preview with preview_ack has at most 2 previews unanswered; the server skips it until an answer comes (counted as dropped) and then sends the newest picture. No answer for 5 s closes it as preview_not_taken. Clients that never ack are unchanged. The web UI and the widgets ack every frame. Acks are not counted against the input limit. - TCP_NOTSENT_LOWAT 32 KiB instead of 64 KiB: 16/32/64/128 KiB at 2 Mbit/s 40 ms gave 0.65/0.64/0.80/1.07 s median age; no fps loss at 6 and 16 Mbit/s. - websocket_stuck_close logs the interaction session and oldest_ms. - BROWSER_PREVIEW_QUALITY (75, 10-100, restart) for the preview JPEG. Median preview age after the change: behind the proxy 0.22 s at 2 Mbit/s and 1.1 s at 500 kbit/s (no close, control kept for 60 s); direct at 2 Mbit/s 0.36 s (1.3.4: 0.79 s). Cost: 17 instead of 22 fps on a 16 Mbit/s path with 40 ms round trip; none on a LAN. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ybuzgBaGxuB5UiaE8dRa4
Review of the preview flow control: - An ack now resets the stuck timer (WebSocket::binaryTaken). Before, only a sent frame did, so a client that answered every frame while its window was shut by a backlog from before its first ack could be closed as preview_not_taken. The counter keeps counting every send from the first frame on, so that backlog is drained before new previews go out (clamping it on the first ack would leave the backlog in flight for good: each answer for a buffered frame would let a new one in). - A shut window skips a session (and counts a drop towards the stuck close) only when there is a new picture; a static page closes no one. - preview_ack is recognised with a size and substring check before the input limit; other messages are parsed after it, as before. - SPEC: an extra ack is ignored when nothing is unanswered. Lab, 500 kbit/s behind the buffering proxy, 40 ms, fresh connect: median age 1.17 s, 60 s without a close, control kept; two reconnects on the same instance: no close (the first seconds up to 3.9 and 7.6 s while the backlog drained). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ybuzgBaGxuB5UiaE8dRa4
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ybuzgBaGxuB5UiaE8dRa4
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.
Problem. On a slow path the interaction preview fell behind and control was lost. 1.3.4 limits what the kernel holds unsent (
TCP_NOTSENT_LOWAT), but a reverse proxy between the function and the browser reads the previews at once and keeps them in its own buffers. Then the server sees no back-pressure, the previews get seconds old, and in the end the session is closed as stuck, which ends its control.Changes
docs/interact.schema.json). A client that answers each preview frame with{"type":"preview_ack"}has at most 2 previews unanswered. Until an answer comes, the server skips the session (counted inpreview_frames_dropped_total), then it sends the newest picture. A session that answers nothing for 5 s is closed aspreview_not_taken. Clients that never sendpreview_ackwork as before. The web UI and the widgets answer every frame. The input limit does not count acks.TCP_NOTSENT_LOWATis 32 KiB instead of 64 KiB (measured, below).websocket_stuck_closealso logs the interactionsession(0 on/api/v1/events) andoldest_ms, the age of the oldest waiting message.websocket_stuck_closes_total{reason}is unchanged.BROWSER_PREVIEW_QUALITY(default 75, range 10–100, needs a restart): the JPEG quality ofpreview.jpgand of the interaction preview.docs/api.md, README (slow-path behaviour in plain words, the new setting), CHANGELOG "Unreleased".Lab. Setup: plain docker without privileges,
tc(netem 40 ms + tbf) in a sidecar that shares the container's network namespace, or in a userspace TCP proxy container. The proxy reads 64 KiB chunks and leaves its kernel send buffer to autotuning, as nginx does. The page sends random pixels, so each preview is a 50 KB JPEG. The client asks for 25 fps. Age = arrival time − TAI of the grain the preview was encoded at (median / p95):outbox_ageafter 20.6 s (control lost)The frame rate was the same everywhere (the link limit), except on 16 Mbit/s with a 40 ms round trip. There, 2 frames in flight give 17 instead of 22 fps.
TCP_NOTSENT_LOWATsweep (1.3.3 image with the sysctl, 2 Mbit/s, 40 ms): 16 KiB 0.65 s, 32 KiB 0.64 s, 64 KiB 0.80 s, 128 KiB 1.07 s. At 6 and 16 Mbit/s every value gave the same frame rate.Log lines from the integration test against this image:
Tests. Unit tests: the window (per session, an ack before the next send, a frame that was not sent, acks over the input limit), a held-back preview counts towards the stuck close, and the new setting.
tests/unit/web/interact.test.mjs: the UI answers every frame, andpreview_ackis in the schema.tests/integration/interact_test.pychecks that 2 previews stay unanswered, 1 more comes per answer, and the session is closed and counted 5 s after the last answer. Locally: 114/114 unit tests, 14/14 web tests, andinteract_test.pyagainst the built image all passed.Notes
-codecsvariant builds from the same sources and needs nothing extra.🤖 Generated with Claude Code
https://claude.ai/code/session_017ybuzgBaGxuB5UiaE8dRa4