Skip to content

CI only for yas-run/yas#87 (receive budget, on #85) — do not merge - #41

Closed
pcarrier wants to merge 4 commits into
ci-basefrom
receive-budget
Closed

pcarrier wants to merge 4 commits into
ci-basefrom
receive-budget

Conversation

@pcarrier

Copy link
Copy Markdown

Fork CI for the receive-budget PR on yas-run/yas (stacked on yas-run#85). Do not merge.

@github-actions

github-actions Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Coverage

Crate Lines Functions Regions
alacritty-driver 85.6% (1081/1263) 89.4% (84/94) 89.3% (1746/1955)
browser 25.5% (309/1212) 30.4% (34/112) 27.7% (605/2183)
cli 31.8% (6517/20468) 32.8% (598/1824) 32.9% (9634/29263)
client 66.2% (4898/7399) 68.1% (624/916) 64.5% (6342/9832)
composite-transport 96.3% (526/546) 98.4% (60/61) 96.2% (884/919)
compositor 54.9% (11402/20763) 66.1% (834/1261) 54.7% (15789/28844)
desktop 78.4% (4460/5691) 71.6% (393/549) 75.1% (6211/8267)
edge 63.1% (800/1268) 50.9% (82/161) 58.0% (1043/1799)
fonts 77.3% (1257/1626) 82.7% (129/156) 78.9% (2424/3071)
fssync 86.0% (1609/1872) 85.8% (182/212) 87.0% (2833/3255)
git 70.7% (4476/6334) 68.5% (337/492) 67.5% (6334/9386)
guest 67.3% (6829/10148) 67.0% (488/728) 66.6% (8935/13416)
lsp 78.7% (3573/4542) 80.8% (336/416) 76.7% (5408/7054)
proxy 63.3% (1899/3000) 56.6% (184/325) 63.5% (2929/4613)
runtime-dir 93.6% (117/125) 100.0% (14/14) 94.8% (218/230)
sd-notify 73.9% (68/92) 100.0% (6/6) 83.2% (109/131)
server 70.6% (83897/118792) 73.2% (6088/8322) 68.6% (112285/163678)
ssh 67.2% (708/1054) 75.9% (85/112) 67.2% (1105/1645)
terminal-model 49.9% (314/629) 62.3% (38/61) 50.3% (505/1004)
uplink 94.2% (582/618) 92.6% (50/54) 93.1% (1062/1141)
webrtc-forwarder 33.6% (1321/3932) 44.9% (146/325) 36.0% (2279/6336)
webserver 80.7% (1490/1846) 81.1% (193/238) 83.2% (2551/3067)
website 35.1% (355/1012) 34.4% (53/154) 35.8% (607/1694)
xtask 0.0% (0/5149) 0.0% (0/131) 0.0% (0/9157)
yas 88.8% (27842/31363) 93.8% (2132/2272) 83.9% (43722/52130)
Total 66.3% (166330/250744) 69.3% (13170/18996) 64.7% (235560/364070)

@pcarrier
pcarrier force-pushed the receive-budget branch 2 times, most recently from b1f381c to 5f62df0 Compare September 30, 2026 09:22
…stalls over WebSocket (yas-run#84)

* Show that uplink WebSocket streams send small writes without Nagle stalls

A producer's stream WebSockets turn Nagle's algorithm off, so an echo
written a moment after another byte goes at once instead of waiting for
the relay's delayed acknowledgement. The new test answers each consumer
byte with two a millisecond apart: with Nagle on, every round takes
about 41 ms; with it off, well under 20.

* Start uplink WebTransport sessions with a window for a whole answer

quinn paces a congestion window over the round trip (1.25 windows per
RTT), and an uplink's answers leave its connection app-limited, which
keeps CUBIC's window from growing past them. From quinn's 14,720 bytes,
a 1 MiB answer settled at two round trips: 200 ms at 100 ms RTT, where
the same read over TCP (ssh, or the WebSocket carrier) takes one.

Sessions now start with a 16 MiB window, and the uplink asks for an
8 MiB UDP receive buffer (Linux's default of 208 KiB overflows under a
paced burst of up to 256 datagrams). Loss still shrinks the window, and
a stream's 1.25 MB receive window still bounds what one consumer has in
flight. Measured through a relay doing the same (read_file 1 MiB, p50):
100 ms RTT 200 -> 114 ms, 200 ms RTT 411 -> 222 ms.

* Say what UDP receive buffer an uplink WebTransport session got

The uplink asks for an 8 MiB UDP receive buffer, and a system may grant
less (Linux caps it at net.core.rmem_max, then doubles it). That never
fails a session, but it makes bursts lose packets, so the producer now
reports the size it got as it connects, a new `Event::ReceiveBuffer`:
`yas uplink` prints "UDP receive buffer: N bytes", and when N is under
what it asked for, says the system caps it.

* Fit the uplink's receive buffer to what macOS, the BSDs and Linux allow

Three issues from the review of yas-run#84:

- macOS and the BSDs refuse a UDP receive buffer over their cap
  (kern.ipc.maxsockbuf less mbuf overhead: 7,456,540 bytes of macOS's
  usual 8 MiB) with ENOBUFS rather than capping it, so asking for 8 MiB
  left the socket at its default, and the event blamed a cap. A refused
  ask now looks for the largest size the system takes, between its
  default and 8 MiB (23 tries at most).
- Linux reports double what it allows, so 8 MiB asked for reads as
  16 MiB, but the event compared that with 8 MiB: a net.core.rmem_max
  from 4 MiB up to 8 MiB was never noted (4 MiB, as here, reads as
  8388608). The event now compares with what all of it reports (16 MiB on
  Linux and Android) and says so: "under the 16777216 Linux reports for
  the 8388608 asked for (net.core.rmem_max caps it)", naming
  kern.ipc.maxsockbuf on macOS and FreeBSD. A test checks a real socket
  against the machine's rmem_max, and fails with the old comparison here.
- quinn's initial window is 12,000 bytes (14,720 clamped to ten 1,200-byte
  datagrams), not 14,720: fixed in the comment and docs/uplink.md.
…as-run#88)

* Keep a command's output head and tail, drop its middle at pipe speed

SPAWN_KEEP_OUTPUT (32), with REPORT_EXIT and SPAWN extension tag 4
[head_bytes, tail_bytes] (the tail at most 1 MiB), sends only the head and
the tail of each output stream. What comes between is dropped as the
server reads it, never held for the client's credit, so a command writing
far more than its client keeps runs at the speed of its pipe instead of
one window per round trip.

The cuts fall between characters as a WHATWG UTF-8 decoder with
replacement reads the whole stream (a JavaScript TextDecoder, Rust's
from_utf8_lossy), so decoding the head and the tail gives exactly the
characters they have within it. The EXIT event says what was dropped of
each stream (extension tags 1 and 2, OutputElision: offset, bytes, lines,
code points, UTF-16 units), so a client can say how much it did not get in
the units it counts.

The output reader waits for the owner only while the head goes out, keeps
the last tail_bytes in a ring, and sends them at the stream's end, or
before the exit is reported when the stream outlives it (a residue past
its grace, TERMINATE, a lost owner, a forced cleanup): flush_kept.

yas-client: Command::keep_output(head, tail), set where the server offers
it; Process::elided(stderr) once the exit came.

* Keep the elision in Process::output; gate mod process again

Review of yas-run#88: Output had no place for what KEEP_OUTPUT dropped, and
output() takes the process, so a kept command's head and tail came joined
with nothing to say a middle was missing. Output.elided now carries it
(the test's helper is output_limited again). And the new mod output_keep
line took mod process's #[cfg(any(unix, windows))].

* Check an output elision's UTF-16 bound without overflowing

protocol-fuzz found OutputElision::decode multiplying a decoded code point
count by two, which panics past u64::MAX / 2. Saturate instead: a count that
wide bounds nothing it could hold. Test the five counts' rules, prefixes, and
the widest values.
…m it

Every stdout and stderr stream holds its window of the session's receive
budget while it is open, whether it writes or not, and yas-client divides
three quarters of that budget between the server's per-session maximum of
processes. The budget was always 16 MiB (RECOMMENDED_BUFFERED): a client
that runs 256 processes a session got 24 KiB windows, and over a network a
stream carries about a window a round trip (20 MB took 88 s at 100 ms). A
wider window per command instead oversubscribes the budget: once it is all
held, a new stream gets no credit until another lets go.

HelloOptions::receive_budget (16 MiB by default, at most 1 GiB) is the
HELLO receive max_buffered this client declares; Client::receive_budget
says it, and default_process_window divides it instead of the constant.
At 256 processes in 256 MiB each stream gets 384 KiB, and they all fit.
Servers already honour a peer's budget above 16 MiB (their outbound credit
is the peer's max_buffered).

Test (client_host): a_wider_receive_budget_fits_every_process_window: at
256 processes a session, 16 MiB gives 24 KiB windows and 256 MiB 384 KiB;
with 255 quiet processes holding theirs (510 streams, 191 MiB), one more
gets all of three windows of output at once. With the same windows in
16 MiB, that command gets no credit (checked: it waited past 30 s).
Review of yas-run#87: NativeClient's queue of frames parked while a caller waits
for another stayed capped at 16 MiB and 1,024 frames, so a client that
declared a wider budget, and was sent within it, failed its session with
"native YAS peer exceeded the bounded pending-frame queue".

The cap is now the declared budget in bytes, and one frame per 16 KiB of
it in frames (at least 1,024, the default's). The new test parks 17 MiB
in 1,372 frames within a 64 MiB budget; capping either bound at the
default's makes it fail.
@pcarrier

pcarrier commented Oct 1, 2026

Copy link
Copy Markdown
Author

Upstream PR merged into yas-run/yas main (now dd33f04): this CI-only draft is done.

@pcarrier pcarrier closed this Oct 1, 2026
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