From 5b689ec5f41f5ca56e9567cbec79fb51f24c7f07 Mon Sep 17 00:00:00 2001 From: ddidderr Date: Wed, 20 May 2026 08:27:49 +0200 Subject: [PATCH] perf(peer): tune QUIC flow control for LAN downloads Raise the s2n-quic connection and stream data windows on both the client and server, increase the max send buffer, and use BBR with a larger initial congestion window. The download path was already able to pipeline multiple chunk streams, but those streams still shared small default connection-level budgets that limited sustained LAN throughput. The tuning keeps one current wire protocol and does not add fallback behavior. It is deliberately centralized in the peer networking module so later transport changes can use the same limits on both sides of the connection. S37 single-source throughput: - Before: 733.22 MiB/s, 6150.72 Mbit/s, 2.793s - After: 824.94 MiB/s, 6920.09 Mbit/s, 2.483s - Delta: +91.72 MiB/s, +769.37 Mbit/s, about +12.5% Test Plan: - just fmt - python3 crates/lanspread-peer-cli/scripts/run_extended_scenarios.py S37 --build-image Refs: local LAN download performance investigation on 2026-05-20. Depends-on: 14e772c5c71a (peer-cli S37 throughput measurement). --- crates/lanspread-peer/src/config.rs | 12 +++++++ crates/lanspread-peer/src/network.rs | 33 ++++++++++++++++++-- crates/lanspread-peer/src/services/server.rs | 6 ++-- 3 files changed, 46 insertions(+), 5 deletions(-) diff --git a/crates/lanspread-peer/src/config.rs b/crates/lanspread-peer/src/config.rs index 39a81f5..e20ff5c 100644 --- a/crates/lanspread-peer/src/config.rs +++ b/crates/lanspread-peer/src/config.rs @@ -23,6 +23,18 @@ pub const PEER_DOWNLOAD_STREAM_WINDOW: usize = 4; /// Maximum number of retry attempts for failed chunk downloads. pub const MAX_RETRY_COUNT: usize = 3; +/// QUIC connection-level receive window for bulk LAN transfers (64 MiB). +pub const QUIC_CONNECTION_DATA_WINDOW: u64 = 64 * 1024 * 1024; + +/// QUIC per-stream receive window for bulk LAN transfers (32 MiB). +pub const QUIC_STREAM_DATA_WINDOW: u64 = 32 * 1024 * 1024; + +/// Maximum queued send data per QUIC stream (32 MiB). +pub const QUIC_MAX_SEND_BUFFER_SIZE: u32 = 32 * 1024 * 1024; + +/// Initial congestion window for LAN-oriented BBR transfers (1 MiB). +pub const QUIC_INITIAL_CONGESTION_WINDOW: u32 = 1024 * 1024; + /// Fallback interval for reconciling missed filesystem watcher events (seconds). pub const LOCAL_GAME_FALLBACK_SCAN_SECS: u64 = 300; diff --git a/crates/lanspread-peer/src/network.rs b/crates/lanspread-peer/src/network.rs index e30bc5b..c96b066 100644 --- a/crates/lanspread-peer/src/network.rs +++ b/crates/lanspread-peer/src/network.rs @@ -10,19 +10,46 @@ use futures::{SinkExt, StreamExt}; use if_addrs::{IfAddr, Interface, get_if_addrs}; use lanspread_db::db::GameFileDescription; use lanspread_proto::{Hello, HelloAck, LibraryDelta, Message, Request, Response}; -use s2n_quic::{Client as QuicClient, Connection, client::Connect, provider::limits::Limits}; +use s2n_quic::{ + Client as QuicClient, + Connection, + client::Connect, + provider::{congestion_controller, limits::Limits}, +}; use tokio_util::codec::{FramedRead, FramedWrite, LengthDelimitedCodec}; -use crate::config::CERT_PEM; +use crate::config::{ + CERT_PEM, + QUIC_CONNECTION_DATA_WINDOW, + QUIC_INITIAL_CONGESTION_WINDOW, + QUIC_MAX_SEND_BUFFER_SIZE, + QUIC_STREAM_DATA_WINDOW, +}; + +pub(crate) fn quic_limits() -> eyre::Result { + Ok(Limits::default() + .with_data_window(QUIC_CONNECTION_DATA_WINDOW)? + .with_bidirectional_local_data_window(QUIC_STREAM_DATA_WINDOW)? + .with_bidirectional_remote_data_window(QUIC_STREAM_DATA_WINDOW)? + .with_unidirectional_data_window(QUIC_STREAM_DATA_WINDOW)? + .with_max_send_buffer_size(QUIC_MAX_SEND_BUFFER_SIZE)?) +} + +pub(crate) fn quic_congestion_controller() -> congestion_controller::Bbr { + congestion_controller::bbr::Builder::default() + .with_initial_congestion_window(QUIC_INITIAL_CONGESTION_WINDOW) + .build() +} /// Establishes a QUIC connection to a peer. pub async fn connect_to_peer(addr: SocketAddr) -> eyre::Result { - let limits = Limits::default().with_max_handshake_duration(Duration::from_secs(3))?; + let limits = quic_limits()?.with_max_handshake_duration(Duration::from_secs(3))?; let client = QuicClient::builder() .with_tls(CERT_PEM)? .with_io("0.0.0.0:0")? .with_limits(limits)? + .with_congestion_controller(quic_congestion_controller())? .start()?; let conn = Connect::new(addr).with_server_name("localhost"); diff --git a/crates/lanspread-peer/src/services/server.rs b/crates/lanspread-peer/src/services/server.rs index 4b20795..c9e636d 100644 --- a/crates/lanspread-peer/src/services/server.rs +++ b/crates/lanspread-peer/src/services/server.rs @@ -2,7 +2,7 @@ use std::{net::SocketAddr, time::Duration}; -use s2n_quic::{Connection, Server, provider::limits::Limits}; +use s2n_quic::{Connection, Server}; use tokio::sync::mpsc::UnboundedSender; use crate::{ @@ -10,6 +10,7 @@ use crate::{ config::{CERT_PEM, KEY_PEM}, context::PeerCtx, events, + network::{quic_congestion_controller, quic_limits}, services::{ advertise::{monitor_mdns_events, start_mdns_advertiser}, stream::handle_peer_stream, @@ -22,7 +23,7 @@ pub async fn run_server_component( ctx: PeerCtx, tx_notify_ui: UnboundedSender, ) -> eyre::Result<()> { - let limits = Limits::default() + let limits = quic_limits()? .with_max_handshake_duration(Duration::from_secs(3))? .with_max_idle_timeout(Duration::from_secs(3))?; @@ -30,6 +31,7 @@ pub async fn run_server_component( .with_tls((CERT_PEM, KEY_PEM))? .with_io(addr)? .with_limits(limits)? + .with_congestion_controller(quic_congestion_controller())? .start()?; let server_addr = server.local_addr()?;