Repository navigation
Conversation
…k's resolution With timestamps negotiated, every RTT sample came from the echoed TSval, and TSval ticks once a millisecond, the finest RFC 7323 §5.4 allows. On the T14's 2 ms LAN path that quantum is half the round trip and a quarter of HyStart++'s 4 ms threshold. Without timestamps there was one timed segment per round trip. Each segment of new data now records when it left, up to 64 at once, and an ACK is timed from the newest segment it newly acknowledges (RFC 8985 §6.1), at the stack's own clock. That is also the segment the receiver did not hold waiting for a second, so a delayed ACK inflates the sample less than an echo of the earliest. Any retransmission clears the record (Karn, RFC 6298 §3): the ACK that repairs a hole covers what was held behind it. Such an ACK, and one whose segment was sent while the record was full, falls back to the echoed timestamp, which also times a retransmission (RFC 7323 §4.1). Every sample is weighted by RFC 7323 Appendix G's ExpectedSamples, with and without timestamps. An echo of a TSval never sent is still counted, and the ACK is still timed by its own departure. s_rt_008 now reads the newest segment's 15 ms where the single timer read the first one's 20 ms. s_rt_011 reads its own 30 ms sample. s_rt_017 holds Karn's rule after a fast retransmission, and s_rt_018 a 1.5 ms sample that an echo would read as 1 ms. Without timestamps the single Karn timer gave one sample a round trip, so a round never reached HyStart++'s N_RTT_SAMPLE of 8 and its delay exit was inert. Departures are timed with and without timestamps, so that case now samples once an ACK too (s_rt_019). An expiry also clears the record: a late ACK would time the original across the expiry and undo its backed-off timer (s_pl_002). Closes issues/tcp-samples-rtt-at-a-millisecond.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`tcp.hystart.css.rounds` counted rounds that entered CSS at least once, not RFC 9406's CSS_ROUNDS spent in CSS: rounds 2-5 without a new entry do not count. It is now `tcp.hystart.css.entry-rounds`. The send_limit test that still named the dropped acknowledged-bytes count is renamed for what it holds. Both were #1102's review r2 suggestions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RFC 9406 §4.3 caps a non-paced sender's growth at L = 8 segments an ACK and gives L = infinity to a paced one. On the T14's LAN path the cap is what held a new connection's ramp: with L = infinity and no pacing (a diagnostic arm in #1112's session) ToyOS reached Linux's marks at 100 and 300 ms, and nothing was resent. That arm had no pacing, so it did not land. [tcp] now paces each connection (RFC 9002 §7.7). A token bucket fills at N * cwnd / srtt. It holds the larger of the initial window, the burst §7.7 bounds a sender to, and 1 ms at the rate. Every data segment spends from it. New data that it cannot cover is held until half the bucket has come back, so a wake up to half a millisecond late loses no rate. The release instant is a connection deadline, so netstack wakes through the existing Tcp::next_deadline. No thread or timer is added. - N is 1.25 in avoidance, the value §7.7 gives. In slow start it is 2: cwnd doubles across the round trip, so the round's data is twice the window it began with, and 1.25 would hold the doubling back. - A pacing hold leaves `limited` as the last send set it. Clearing it would stop growth and CUBIC's clock while the pacer is spreading a window that is in use. - Retransmissions spend but are not held. Recovery's pipe and a timeout's one-segment window already bound them. - ACK-only segments are not paced (§7.7). - Without a smoothed RTT nothing is paced, and cwnd is at most the initial window then. - The time a pacing hold keeps data waiting is `tcp.send-limited.pacing.ns`. osbench reads it. Two net.rs downloads on a 15 ms gigabit path asserted that the receive window reaches the whole 4 MiB buffer. The window grows to twice what is read per round trip, 3.75 MB on that path. The unpaced sender carried it to the whole buffer; paced, it stops 3 KB short. They now assert the rule's bound. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Evidence branch pr-1112 was rewritten to f2909c1f without the commit that published an unmasked temp path; c990b1f2 no longer exists. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review r1 — #1112 at f1a230eArchitecture. The design fits. RTT sampling stays inside [tcp]'s Checked and correct:
BLOCKER
SUGGESTION
SEND BACK |
…s source The departure record held at most 64 segments. On the T14's path about 230-255 are in flight, so roughly three ACKs in four still took the 1 ms echo. An ACK that crossed from the last recorded segment into unrecorded data was also timed from the older entry, which overstated the round trip. The record now holds runs, not segments, and records every send. A send at the same instant as the last joins its run. A send shorter than SMSS also joins a run shorter than SMSS, and is timed from the run's start. So the record is bounded by the flight, at most two runs per SMSS plus one, with no cap that drops a send. An ACK is timed from the newest run it reaches, a partly acknowledged one included. The echo now times only data that a resend or an expiry took out of the record. - `tcp.rtt.departure` and `tcp.rtt.echo` count each sample's source. osbench reads them, so metal can show the share. - s_rt_020 (the review's case): with more than 64 segments in flight, an ACK of only the newest, 0.5 ms after an ACK of the first 64, is timed from its departure, and no sample is an echo. It fails under the old rule. - s_rt_021: a full segment after small sends starts its own run. - The expiry's clear now gives RFC 6298 §5's reason. A backed-off RTO stands until new data is sent and acknowledged. - rtt.rs tests are in order, and s_rt_019 checks set_options' outputs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RFC 9406 §4.3 caps a non-paced sender's growth at L = 8 segments an ACK and gives L = infinity to a paced one. On the T14's LAN path the cap is what held a new connection's ramp: with L = infinity and no pacing (a diagnostic arm in #1112's session) ToyOS reached Linux's marks at 100 and 300 ms, and nothing was resent. That arm had no pacing, so it did not land. [tcp] now paces each connection (RFC 9002 §7.7). A token bucket fills at N * cwnd / srtt. It holds the larger of the initial window, the burst §7.7 bounds a sender to, and 1 ms at the rate. Every data segment spends from it. New data that it cannot cover is held until half the bucket has come back, so a wake up to half a millisecond late loses no rate. The release instant is a connection deadline, so netstack wakes through the existing Tcp::next_deadline. No thread or timer is added. - N is 1.25 in avoidance, the value §7.7 gives. In slow start it is 2: cwnd doubles across the round trip, so the round's data is twice the window it began with, and 1.25 would hold the doubling back. - A pacing hold leaves `limited` as the last send set it. Clearing it would stop growth and CUBIC's clock while the pacer is spreading a window that is in use. - Retransmissions spend but are not held. Recovery's pipe and a timeout's one-segment window already bound them. - ACK-only segments are not paced (§7.7). - Without a smoothed RTT nothing is paced, and cwnd is at most the initial window then. - The time a pacing hold keeps data waiting is `tcp.send-limited.pacing.ns`. osbench reads it. Two net.rs downloads on a 15 ms gigabit path asserted that the receive window reaches the whole 4 MiB buffer. The window grows to twice what is read per round trip, 3.75 MB on that path. The unpaced sender carried it to the whole buffer; paced, it stops 3 KB short. They now assert the rule's bound. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Evidence branch pr-1112 was rewritten to f2909c1f without the commit that published an unmasked temp path; c990b1f2 no longer exists. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review r2 — #1112 at 15fdb0eBoth r1 blockers are resolved:
The rest of the run-record design is correct:
BLOCKER
SUGGESTION
SEND BACK |
Small writes joined a run until it reached SMSS, and each ACK inside the run was timed from its first send. A Nagle-off sender of 100 B every 10 ms on a 20 ms path kept one run growing for 140 ms. Its ACKs were then timed 20, 30, ... 160 ms, which inflated srtt, the RTO, the PTO and HyStart++'s round minimum: a regression against main for app-limited flows. A run now keeps its first and last send instants, and whether an ACK has landed in it. - Sub-SMSS joins stop at that ACK. Same-instant joins continue, since they don't blur the run's timing. - An ACK of a whole run is timed from its last send. - An ACK partway into a run that left at one instant is timed from that instant, so a burst's stretch ACKs are all exact. - In a run of several instants, the first partway ACK is timed from the first send. It overstates by less than the joins took, and they ended at that ACK. A later partway ACK is not timed at all. A send's own length no longer matters to the joins: since a whole run is timed from its last send, a full segment after small ones costs nothing. That tightens the bound. Only the oldest run can be both short and closed, so the record holds at most a run per SMSS in flight plus two. A run is 32 bytes. - s_rt_022 (the review's case): 100 B every 10 ms, each ACKed 20 ms later; srtt stays 20 ms. It fails at 15fdb0e (21.25 ms at the ACK at 30 ms). - s_rt_023: three stretch ACKs into one burst are each timed. - s_rt_024: a later partway ACK into a run of writes is not timed, and the ACK of the rest is timed from the last write. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RFC 9406 §4.3 caps a non-paced sender's growth at L = 8 segments an ACK and gives L = infinity to a paced one. On the T14's LAN path the cap is what held a new connection's ramp: with L = infinity and no pacing (a diagnostic arm in #1112's session) ToyOS reached Linux's marks at 100 and 300 ms, and nothing was resent. That arm had no pacing, so it did not land. [tcp] now paces each connection (RFC 9002 §7.7). A token bucket fills at N * cwnd / srtt. It holds the larger of the initial window, the burst §7.7 bounds a sender to, and 1 ms at the rate. Every data segment spends from it. New data that it cannot cover is held until half the bucket has come back, so a wake up to half a millisecond late loses no rate. The release instant is a connection deadline, so netstack wakes through the existing Tcp::next_deadline. No thread or timer is added. - N is 1.25 in avoidance, the value §7.7 gives. In slow start it is 2: cwnd doubles across the round trip, so the round's data is twice the window it began with, and 1.25 would hold the doubling back. - A pacing hold leaves `limited` as the last send set it. Clearing it would stop growth and CUBIC's clock while the pacer is spreading a window that is in use. - Retransmissions spend but are not held. Recovery's pipe and a timeout's one-segment window already bound them. - ACK-only segments are not paced (§7.7). - Without a smoothed RTT nothing is paced, and cwnd is at most the initial window then. - The time a pacing hold keeps data waiting is `tcp.send-limited.pacing.ns`. osbench reads it. Two net.rs downloads on a 15 ms gigabit path asserted that the receive window reaches the whole 4 MiB buffer. The window grows to twice what is read per round trip, 3.75 MB on that path. The unpaced sender carried it to the whole buffer; paced, it stops 3 KB short. They now assert the rule's bound. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Evidence branch pr-1112 was rewritten to f2909c1f without the commit that published an unmasked temp path; c990b1f2 no longer exists. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review r3 — #1112 at 417b74bThe r2 blocker is fixed.
The bound holds, at flight/SMSS + 2 runs of 32 B each.
The no-sample rule's effect on RTO.
Karn is unchanged from r2: Tests and evidence.
Comments. The field doc is long, but each sentence carries part of the contract: the join rule, the bound, or which instant times which ACK. It is accurate. SUGGESTION
LAND |
…goes On the T14's LAN path, #1114's A/B (evidence branch pr-1114, r4/t14 at 783d377ae1ee56592aae53465a74bf60c3beb839) read the paced sender with L = infinity (P) against #1112's head (C) and Linux in the same session: p50 MB/s by 100 ms by 300 ms Linux 112, 112 5.7 26.1-26.2 MB C 112 x3 2.0-3.1 23.8-25.4 P 114 x3 6.3-6.6 29.3-29.9 P beat C at 300 ms in every pair, resent nothing, and is at or above Linux at every mark. That meets the record's exit, so the record goes. The comparison record's pointer to it now names the PRs and the evidence. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RFC 9406 §4.3 caps a non-paced sender's growth at L = 8 segments an ACK and gives L = infinity to a paced one. On the T14's LAN path the cap is what held a new connection's ramp: with L = infinity and no pacing (a diagnostic arm in #1112's session) ToyOS reached Linux's marks at 100 and 300 ms, and nothing was resent. That arm had no pacing, so it did not land. [tcp] now paces each connection (RFC 9002 §7.7). A token bucket fills at N * cwnd / srtt. It holds the larger of the initial window, the burst §7.7 bounds a sender to, and 1 ms at the rate. Every data segment spends from it. New data that it cannot cover is held until half the bucket has come back, so a wake up to half a millisecond late loses no rate. The release instant is a connection deadline, so netstack wakes through the existing Tcp::next_deadline. No thread or timer is added. - N is 1.25 in avoidance, the value §7.7 gives. In slow start it is 2: cwnd doubles across the round trip, so the round's data is twice the window it began with, and 1.25 would hold the doubling back. - A pacing hold leaves `limited` as the last send set it. Clearing it would stop growth and CUBIC's clock while the pacer is spreading a window that is in use. - Retransmissions spend but are not held. Recovery's pipe and a timeout's one-segment window already bound them. - ACK-only segments are not paced (§7.7). - Without a smoothed RTT nothing is paced, and cwnd is at most the initial window then. - The time a pacing hold keeps data waiting is `tcp.send-limited.pacing.ns`. osbench reads it. Two net.rs downloads on a 15 ms gigabit path asserted that the receive window reaches the whole 4 MiB buffer. The window grows to twice what is read per round trip, 3.75 MB on that path. The unpaced sender carried it to the whole buffer; paced, it stops 3 KB short. They now assert the rule's bound. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Evidence branch pr-1112 was rewritten to f2909c1f without the commit that published an unmasked temp path; c990b1f2 no longer exists. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…goes On the T14's LAN path, #1114's A/B (evidence branch pr-1114, r4/t14 at 783d377ae1ee56592aae53465a74bf60c3beb839) read the paced sender with L = infinity (P) against #1112's head (C) and Linux in the same session: p50 MB/s by 100 ms by 300 ms Linux 112, 112 5.7 26.1-26.2 MB C 112 x3 2.0-3.1 23.8-25.4 P 114 x3 6.3-6.6 29.3-29.9 P beat C at 300 ms in every pair, resent nothing, and is at or above Linux at every mark. That meets the record's exit, so the record goes. The comparison record's pointer to it now names the PRs and the evidence. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With any send joining a run shorter than SMSS, a window of body sent after a short header joined the header's run. The run then spanned two instants, so only its first partway ACK was timed: HTTP's shape lost most of its samples. A send now joins a short run only if it is short too, as before #1112's review r2. The record's bound returns to two runs per SMSS in flight plus two, about 16 KB at a 250-segment flight. s_rt_025: a 200 B header, then four segments of body 1 ms later. Four ACKs give four samples. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RFC 9406 §4.3 caps a non-paced sender's growth at L = 8 segments an ACK and gives L = infinity to a paced one. On the T14's LAN path the cap is what held a new connection's ramp: with L = infinity and no pacing (a diagnostic arm in #1112's session) ToyOS reached Linux's marks at 100 and 300 ms, and nothing was resent. That arm had no pacing, so it did not land. [tcp] now paces each connection (RFC 9002 §7.7). A token bucket fills at N * cwnd / srtt. It holds the larger of the initial window, the burst §7.7 bounds a sender to, and 1 ms at the rate. Every data segment spends from it. New data that it cannot cover is held until half the bucket has come back, so a wake up to half a millisecond late loses no rate. The release instant is a connection deadline, so netstack wakes through the existing Tcp::next_deadline. No thread or timer is added. - N is 1.25 in avoidance, the value §7.7 gives. In slow start it is 2: cwnd doubles across the round trip, so the round's data is twice the window it began with, and 1.25 would hold the doubling back. - A pacing hold leaves `limited` as the last send set it. Clearing it would stop growth and CUBIC's clock while the pacer is spreading a window that is in use. - Retransmissions spend but are not held. Recovery's pipe and a timeout's one-segment window already bound them. - ACK-only segments are not paced (§7.7). - Without a smoothed RTT nothing is paced, and cwnd is at most the initial window then. - The time a pacing hold keeps data waiting is `tcp.send-limited.pacing.ns`. osbench reads it. Two net.rs downloads on a 15 ms gigabit path asserted that the receive window reaches the whole 4 MiB buffer. The window grows to twice what is read per round trip, 3.75 MB on that path. The unpaced sender carried it to the whole buffer; paced, it stops 3 KB short. They now assert the rule's bound. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Evidence branch pr-1112 was rewritten to f2909c1f without the commit that published an unmasked temp path; c990b1f2 no longer exists. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With any send joining a run shorter than SMSS, a window of body sent after a short header joined the header's run. The run then spanned two instants, so only its first partway ACK was timed: HTTP's shape lost most of its samples. A send now joins a short run only if it is short too, as before #1112's review r2. The record's bound returns to two runs per SMSS in flight plus two, about 16 KB at a 250-segment flight. s_rt_025: a 200 B header, then four segments of body 1 ms later. Four ACKs give four samples. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem
With timestamps, [tcp] took every RTT sample from the echoed TSval, which ticks at 1 ms, the finest RFC 7323 §5.4 allows. On the T14's 2 ms LAN path that quantum is half the round trip, and a quarter of HyStart++'s 4 ms threshold (#1102). Without timestamps there was one Karn sample a round trip, so a round never reached HyStart++'s 8 samples and its delay exit was inert.
Change
tcp.rtt.departureandtcp.rtt.echonow name each sample's source, and osbench reads them.issues/tcp-samples-rtt-at-a-millisecond.md, including its timestamps-off case.tcp.hystart.css.roundsbecomestcp.hystart.css.entry-rounds, since it counts rounds that entered CSS, not RFC 9406's CSS_ROUNDS. The send_limit test that still named the dropped acknowledged-bytes count is renamed.Tests
cargo test --no-fail-fast432 passed, exit 0;cargo run -- --ci host105 steps, all green, exit 0. Evidence: https://github.com/ToyOSOrg/evidence/tree/96c693b58d242d8ec236e0805b34b714306ec4c2/r3 (earlier rounds in r1, r2)Metal: T14,
osbench_lan, one session (all boots rc 0, all judges exit 0)This session ran the first version, with the 64-segment cap, so its C rows don't measure the fix. The pacing A/B (#1114) is restaged with C = 15fdb0e, and that session reads
tcp.rtt.departureandtcp.rtt.echo. Review r2's fix changes only how sub-SMSS sends join a run. The bulk upload sends full segments, and its runs are bursts left at one instant, which both versions time the same, so the staged images still measure this PR.The peer is the AMD bench's Linux over its Wi-Fi. Order: Linux L1, b1 c1 d1 b2 c2 d2 b3 c3 d3, Linux L2.
system/netstack), D = C with L = ∞ unpaced, a diagnostic arm that does not land.Under the rule set before the run:
Evidence (readbacks' test-runner lines, verdicts, judge logs, the Linux runs, image hashes, D's patch): https://github.com/ToyOSOrg/evidence/tree/110096df4b2c1ce75af25caa415677aa0985579e/r1
Risks
🤖 Generated with Claude Code