Repository navigation
Description
Is your feature request related to a problem? Please describe
The current rwnd trace design describes a step with four fields, two of which
(app_read_bytes and rwnd_remaining) are mutually exclusive actions. That
shape has turned out to be harder to reason about than the thing it models.
-
Two fields say overlapping things.
set_rcv_bufstates the buffer, and
rwnd_remainingstates the window — but the window is what is left of the
buffer, so a reader has to know which of the two a given step used before
they know what the receiver is doing. Nothing in the step says which
convention is in force. -
The mutual-exclusion invariant is pure overhead. "At most one of
app_read_bytes/rwnd_remaining, and possibly neither" cannot be
expressed by a derivedDeserialize. It needs a hand-written one that
rejects the both-set case, plus a tri-stateaction: Option<RwndAction>that
every consumer must match exhaustively — including aNonearm that only
exists to represent "a step that reconfigures the buffer and nothing else". -
The names hide the part that actually matters. In practice the two
variants differ by how the application reads, not by which quantity is
asserted. Arwnd_remainingstep only reproduces the window it names if the
application drains continuously, so that the window's right edge slides
forward withrcv_nxtthe way a real receiver's does. Anapp_read_bytes
step is the opposite: the application reads a fixed budget and then stops, and
the window falls out of what is left unread. That reading behaviour is the
whole difference, and neither field name mentions it. -
The two names are not the same kind of thing.
rwnd_remainingis an
observation from the wire;app_read_bytesis an action by the application.
Presenting them as two variants of one enum makes them look like alternative
ways to say the same thing, when they describe two different physical
situations — a receive buffer that stopped growing, versus an application that
reads too slowly.
Description of the new feature/enhancement
Drop rwnd_remaining, and give set_rcv_buf the meaning it was standing in for.
set_rcv_buf = N — the receive buffer is N bytes, the advertised window is
held at N, and the application drains continuously so that the buffer stays
empty and the window's right edge tracks rcv_nxt. This is one statement about
the receiver rather than two, and it is exactly what a rwnd_remaining = N step
means today.
app_read_bytes = M — over this step the application reads exactly M bytes
and then stops. The advertised window is whatever is left of the standing
set_rcv_buf after the unread backlog, so it shrinks as data arrives and reaches
zero when the buffer fills.
The two fields stop being alternatives and become orthogonal, which removes the
invariant entirely. Four step shapes, all meaningful, none of them special cases:
set_rcv_buf |
app_read_bytes |
meaning |
|---|---|---|
| N | — | buffer N, window pinned at N, application drains continuously |
| — | M | application reads M bytes against the standing buffer; window = buffer − unread |
| N | M | resize the buffer to N, then read M bytes from it |
| — | — | carry the previous configuration forward for this step's duration |
The receiver model becomes a small state machine with one rule: a step that names
app_read_bytes reads that budget and stops; a step that does not read
continuously. Nothing else has to be inferred from the field set.
Proposed technical implementation details (optional)
-
Remove the
RwndActionenum and theOption<RwndAction>action field.
RwndDecisionbecomes a plain struct of two independent options:pub struct RwndDecision { pub set_rcv_buf: Option<u64>, pub app_read_bytes: Option<u64>, }
-
Delete the hand-written
DeserializeforStaticRwndConfigand the
"cannot set both" error. With the fields orthogonal, a derived
Serialize/Deserializeis enough. The serialized form stays flat and drops
one key:{"duration": "1s", "set_rcv_buf": 65536} {"duration": "1s", "app_read_bytes": 1024} {"duration": "1s", "set_rcv_buf": 131072, "app_read_bytes": 1024} -
Builder: drop
StaticRwndConfig::remaining(); keepset_rcv_buf()and
app_read(), which may now be combined. -
This is a breaking change to the trace format, so it wants a version bump and
a CHANGELOG entry. Traces written under the old schema are not readable —
worth an explicit deserialization error namingrwnd_remainingrather than
silently ignoring an unknown key, so an old trace fails loudly instead of
replaying as a series of no-op steps.
Additional context
The consumer side already works this way. The replay application distinguishes
exactly these two behaviours — a continuously draining reader versus a reader
that spends a byte budget and stops — and the kernel-side window control has two
corresponding modes: advertise a value outright, or compute it as
buf_size - unread. Today the mapping from trace field to behaviour is a
convention held in the replayer rather than something the trace states, which is
what makes a trace hard to read on its own. Folding rwnd_remaining into
set_rcv_buf moves that convention into the format.
The generator side benefits for the same reason: it decides per step whether the
capture shows a receiver whose buffer stopped growing or an application that read
slowly, and it currently has to encode that decision by choosing between two
field names that do not say so.
No behaviour change is intended for either kind of step — this is a rename and a
merge that makes the existing two behaviours legible, plus the removal of an
invariant that only existed because two fields overlapped.
Activity