Skip to content

Commit ade217c

Browse files
committed
feat(proto): expose max_transmit_segments and max_transmit_datagrams in TransportConfig
1 parent 6ee7cf2 commit ade217c

4 files changed

Lines changed: 114 additions & 17 deletions

File tree

noq-proto/src/config/transport.rs

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use std::path::Path;
33
use std::{
44
fmt,
55
net::SocketAddr,
6-
num::{NonZeroU8, NonZeroU32},
6+
num::{NonZeroU8, NonZeroU32, NonZeroUsize},
77
sync::Arc,
88
};
99

@@ -59,6 +59,9 @@ pub struct TransportConfig {
5959

6060
pub(crate) enable_segmentation_offload: bool,
6161

62+
pub(crate) max_transmit_segments: NonZeroUsize,
63+
pub(crate) max_transmit_datagrams: NonZeroUsize,
64+
6265
pub(crate) address_discovery_role: address_discovery::Role,
6366

6467
pub(crate) max_concurrent_multipath_paths: Option<NonZeroU32>,
@@ -364,6 +367,39 @@ impl TransportConfig {
364367
self
365368
}
366369

370+
/// Maximum number of UDP segments encoded into a single GSO `sendmsg` batch.
371+
///
372+
/// Effective batch size is also bounded by the platform's GSO capabilities (Linux with
373+
/// kernel ≥ 4.18 reports up to 64 via `UDP_MAX_SEGMENTS`; macOS reports `BATCH_SIZE`;
374+
/// Windows reports up to 512). When GSO is unavailable or disabled this setting has
375+
/// no effect, every `Transmit` carries a single datagram.
376+
///
377+
/// Defaults to **10**, a conservative compromise tuned for multi-connection workloads.
378+
/// Single-connection bulk-transfer workloads on Linux+GSO benefit from raising this
379+
/// to 40 (≈ 51 KB per `sendmsg` at MTU 1280, 60 KB at MTU 1500), at the cost of a
380+
/// proportional increase in per-`Connection` send-buffer pre-allocation.
381+
///
382+
/// Worst-case memory cost: `max_transmit_segments * current_mtu` bytes of pre-allocated
383+
/// buffer per active `Connection`.
384+
pub fn max_transmit_segments(&mut self, value: NonZeroUsize) -> &mut Self {
385+
self.max_transmit_segments = value;
386+
self
387+
}
388+
389+
/// Maximum number of datagrams produced in a single `drive_transmit` call.
390+
///
391+
/// This caps the amount of CPU spent generating datagrams in one go, allowing other
392+
/// tasks (notably ACK reception and timer wakeups) to run. It is intended to be kept
393+
/// in lockstep with [`TransportConfig::max_transmit_segments`] (recommended ratio: 2×)
394+
/// so a single GSO super-segment can hold a couple of transmit drives back-to-back
395+
/// without bouncing to the scheduler.
396+
///
397+
/// Defaults to **20**.
398+
pub fn max_transmit_datagrams(&mut self, value: NonZeroUsize) -> &mut Self {
399+
self.max_transmit_datagrams = value;
400+
self
401+
}
402+
367403
/// Whether to send observed address reports to peers.
368404
///
369405
/// This will aid peers in inferring their reachable address, which in most NATd networks
@@ -563,6 +599,9 @@ impl Default for TransportConfig {
563599

564600
enable_segmentation_offload: true,
565601

602+
max_transmit_segments: NonZeroUsize::new(10).expect("nonzero"),
603+
max_transmit_datagrams: NonZeroUsize::new(20).expect("nonzero"),
604+
566605
address_discovery_role: address_discovery::Role::default(),
567606

568607
// disabled multipath by default
@@ -608,6 +647,8 @@ impl fmt::Debug for TransportConfig {
608647
deterministic_packet_numbers: _,
609648
congestion_controller_factory: _,
610649
enable_segmentation_offload,
650+
max_transmit_segments,
651+
max_transmit_datagrams,
611652
address_discovery_role,
612653
max_concurrent_multipath_paths,
613654
default_path_max_idle_timeout,
@@ -648,6 +689,8 @@ impl fmt::Debug for TransportConfig {
648689
.field("datagram_send_buffer_size", datagram_send_buffer_size)
649690
// congestion_controller_factory not debug
650691
.field("enable_segmentation_offload", enable_segmentation_offload)
692+
.field("max_transmit_segments", max_transmit_segments)
693+
.field("max_transmit_datagrams", max_transmit_datagrams)
651694
.field("address_discovery_role", address_discovery_role)
652695
.field(
653696
"max_concurrent_multipath_paths",

noq-proto/src/connection/mod.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6966,6 +6966,20 @@ impl Connection {
69666966
.unwrap_or(INITIAL_MTU)
69676967
}
69686968

6969+
/// Maximum number of UDP segments allowed in a single GSO `sendmsg` batch.
6970+
///
6971+
/// See [`TransportConfig::max_transmit_segments`].
6972+
pub fn max_transmit_segments(&self) -> std::num::NonZeroUsize {
6973+
self.config.max_transmit_segments
6974+
}
6975+
6976+
/// Maximum number of datagrams produced in a single `poll_transmit` driver iteration.
6977+
///
6978+
/// See [`TransportConfig::max_transmit_datagrams`].
6979+
pub fn max_transmit_datagrams(&self) -> std::num::NonZeroUsize {
6980+
self.config.max_transmit_datagrams
6981+
}
6982+
69696983
/// Size of non-frame data for a 1-RTT packet
69706984
///
69716985
/// Quantifies space consumed by the QUIC header and AEAD tag. All other bytes in a packet are

noq-proto/src/tests/mod.rs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2191,6 +2191,59 @@ fn tail_loss_respect_max_datagrams() {
21912191
assert_eq!(client_stats.udp_tx.ios, client_stats.udp_tx.datagrams);
21922192
}
21932193

2194+
// Default max_transmit_segments / max_transmit_datagrams must remain backward
2195+
// compatible. The historical compromise was 10 / 20.
2196+
#[test]
2197+
fn default_max_transmit_settings() {
2198+
let cfg = TransportConfig::default();
2199+
assert_eq!(
2200+
cfg.max_transmit_segments.get(),
2201+
10,
2202+
"default max_transmit_segments must remain 10 to preserve historical behavior",
2203+
);
2204+
assert_eq!(
2205+
cfg.max_transmit_datagrams.get(),
2206+
20,
2207+
"default max_transmit_datagrams must remain 20 to preserve historical behavior",
2208+
);
2209+
}
2210+
2211+
// max_transmit_segments setter caps the number of GSO segments emitted in a
2212+
// single Transmit. With a cap of 4, no Transmit returned by poll_transmit may
2213+
// carry more than 4 datagrams.
2214+
#[test]
2215+
fn max_transmit_segments_setter_caps_batch_size() {
2216+
use std::num::NonZeroUsize;
2217+
let _guard = subscribe();
2218+
let client_config = {
2219+
let mut c_config = client_config();
2220+
let mut t_config = TransportConfig::default();
2221+
t_config.max_transmit_segments(NonZeroUsize::new(4).expect("nonzero"));
2222+
c_config.transport_config(t_config.into());
2223+
c_config
2224+
};
2225+
let mut pair = Pair::default();
2226+
let (client_ch, _) = pair.connect_with(client_config);
2227+
2228+
// Send enough data to require GSO batching.
2229+
let s = pair.client_streams(client_ch).open(Dir::Uni).unwrap();
2230+
pair.client_send(client_ch, s)
2231+
.write(&[42u8; 64 * 1024])
2232+
.unwrap();
2233+
pair.drive();
2234+
2235+
let stats = pair.client_conn_mut(client_ch).stats();
2236+
assert!(
2237+
stats.udp_tx.ios > 0,
2238+
"expected at least one UDP TX I/O during the transfer",
2239+
);
2240+
let segments_per_io = stats.udp_tx.datagrams as f64 / stats.udp_tx.ios as f64;
2241+
assert!(
2242+
segments_per_io <= 4.0,
2243+
"max_transmit_segments(4) should cap GSO batch size, observed average = {segments_per_io}",
2244+
);
2245+
}
2246+
21942247
#[test]
21952248
fn datagram_send_recv() {
21962249
let _guard = subscribe();

noq/src/connection.rs

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ use std::{
44
future::Future,
55
io,
66
net::{IpAddr, SocketAddr},
7-
num::NonZeroUsize,
87
pin::Pin,
98
sync::{
109
Arc, Weak,
@@ -1487,7 +1486,8 @@ impl State {
14871486
let max_datagrams = self
14881487
.sender
14891488
.max_transmit_segments()
1490-
.min(MAX_TRANSMIT_SEGMENTS);
1489+
.min(self.inner.max_transmit_segments());
1490+
let max_transmit_datagrams = self.inner.max_transmit_datagrams().get();
14911491

14921492
loop {
14931493
// Retry the last transmit, or get a new one.
@@ -1525,7 +1525,7 @@ impl State {
15251525
Poll::Ready(Ok(())) => {}
15261526
}
15271527

1528-
if transmits >= MAX_TRANSMIT_DATAGRAMS {
1528+
if transmits >= max_transmit_datagrams {
15291529
// TODO: What isn't ideal here yet is that if we don't poll all
15301530
// datagrams that could be sent we don't go into the `app_limited`
15311531
// state and CWND continues to grow until we get here the next time.
@@ -1875,16 +1875,3 @@ pub enum SendDatagramError {
18751875
#[error("connection lost")]
18761876
ConnectionLost(#[from] ConnectionError),
18771877
}
1878-
1879-
/// The maximum amount of datagrams which will be produced in a single `drive_transmit` call
1880-
///
1881-
/// This limits the amount of CPU resources consumed by datagram generation,
1882-
/// and allows other tasks (like receiving ACKs) to run in between.
1883-
const MAX_TRANSMIT_DATAGRAMS: usize = 20;
1884-
1885-
/// The maximum amount of datagrams that are sent in a single transmit
1886-
///
1887-
/// This can be lower than the maximum platform capabilities, to avoid excessive
1888-
/// memory allocations when calling `poll_transmit()`. Benchmarks have shown
1889-
/// that numbers around 10 are a good compromise.
1890-
const MAX_TRANSMIT_SEGMENTS: NonZeroUsize = NonZeroUsize::new(10).expect("known");

0 commit comments

Comments
 (0)