From d1220f1a36732bc4314f09fef900de6d8fc72c11 Mon Sep 17 00:00:00 2001 From: Mygod Date: Fri, 20 Feb 2026 17:58:12 -0500 Subject: [PATCH 1/8] Add active polling to recursive mode --- crates/slipstream-client/src/main.rs | 59 ++++++++++++++++ crates/slipstream-client/src/runtime.rs | 91 ++++++++++++++++++++++++- crates/slipstream-ffi/src/lib.rs | 1 + 3 files changed, 150 insertions(+), 1 deletion(-) diff --git a/crates/slipstream-client/src/main.rs b/crates/slipstream-client/src/main.rs index 0bb320f6..2550fd74 100644 --- a/crates/slipstream-client/src/main.rs +++ b/crates/slipstream-client/src/main.rs @@ -54,6 +54,12 @@ struct Args { cert: Option, #[arg(long = "keep-alive-interval", short = 't', default_value_t = 400)] keep_alive_interval: u16, + #[arg( + long = "active-poll-cap-ms", + default_value_t = 10_000u64, + value_parser = clap::value_parser!(u64).range(1..) + )] + active_poll_cap_ms: u64, #[arg(long = "debug-poll")] debug_poll: bool, #[arg(long = "debug-streams")] @@ -177,6 +183,16 @@ fn main() { }); keep_alive_override.unwrap_or(args.keep_alive_interval) }; + let active_poll_cap_ms = if cli_provided(&matches, "active_poll_cap_ms") { + args.active_poll_cap_ms + } else { + let active_poll_cap_override = parse_active_poll_cap_ms(&sip003_env.plugin_options) + .unwrap_or_else(|err| { + tracing::error!("SIP003 env error: {}", err); + std::process::exit(2); + }); + active_poll_cap_override.unwrap_or(args.active_poll_cap_ms) + }; let config = ClientConfig { tcp_listen_host: &tcp_listen_host, @@ -187,6 +203,7 @@ fn main() { domain: &domain, cert: cert.as_deref(), keep_alive_interval: keep_alive_interval as usize, + active_poll_cap_ms, debug_poll: args.debug_poll, debug_streams: args.debug_streams, }; @@ -363,6 +380,23 @@ fn parse_keep_alive_interval(options: &[sip003::Sip003Option]) -> Result Result, String> { + let mut last = None; + for option in options { + if option.key == "active-poll-cap-ms" { + let value = option.value.trim(); + let parsed = value + .parse::() + .map_err(|_| format!("Invalid active-poll-cap-ms value: {}", value))?; + if parsed == 0 { + return Err("active-poll-cap-ms must be >= 1".to_string()); + } + last = Some(parsed); + } + } + Ok(last) +} + #[cfg(test)] mod tests { use super::*; @@ -488,4 +522,29 @@ mod tests { assert!(parsed.resolvers.is_empty()); assert!(parsed.authoritative_remote); } + + #[test] + fn active_poll_cap_uses_last_value() { + let options = vec![ + sip003::Sip003Option { + key: "active-poll-cap-ms".to_string(), + value: "5000".to_string(), + }, + sip003::Sip003Option { + key: "active-poll-cap-ms".to_string(), + value: "12000".to_string(), + }, + ]; + let parsed = parse_active_poll_cap_ms(&options).expect("options should parse"); + assert_eq!(parsed, Some(12_000)); + } + + #[test] + fn active_poll_cap_rejects_zero() { + let options = vec![sip003::Sip003Option { + key: "active-poll-cap-ms".to_string(), + value: "0".to_string(), + }]; + assert!(parse_active_poll_cap_ms(&options).is_err()); + } } diff --git a/crates/slipstream-client/src/runtime.rs b/crates/slipstream-client/src/runtime.rs index 05bc1f60..f248b2cc 100644 --- a/crates/slipstream-client/src/runtime.rs +++ b/crates/slipstream-client/src/runtime.rs @@ -9,7 +9,7 @@ use self::setup::{bind_tcp_listener, bind_udp_socket, compute_mtu, map_io}; use crate::dns::{ add_paths, expire_inflight_polls, handle_dns_response, maybe_report_debug, refresh_resolver_path, resolve_resolvers, resolver_mode_to_c, send_poll_queries, - sockaddr_storage_to_socket_addr, DnsResponseContext, + sockaddr_storage_to_socket_addr, DnsResponseContext, ResolverState, }; use crate::error::ClientError; use crate::pacing::{cwnd_target_polls, inflight_packet_estimate}; @@ -68,6 +68,49 @@ fn drain_disconnected_commands(command_rx: &mut mpsc::UnboundedReceiver dropped } +fn total_pending_polls(resolvers: &[ResolverState]) -> usize { + resolvers + .iter() + .map(|resolver| resolver.pending_polls) + .sum() +} + +fn total_inflight_polls(resolvers: &[ResolverState]) -> usize { + resolvers + .iter() + .map(|resolver| resolver.inflight_poll_ids.len()) + .sum() +} + +fn total_dns_responses(resolvers: &[ResolverState]) -> u64 { + resolvers + .iter() + .map(|resolver| resolver.debug.dns_responses) + .sum() +} + +fn select_active_poll_target( + cnx: *mut picoquic_cnx_t, + resolvers: &mut [ResolverState], +) -> Option { + let modes = [ResolverMode::Recursive, ResolverMode::Authoritative]; + for mode in modes { + for idx in 0..resolvers.len() { + if resolvers[idx].mode != mode { + continue; + } + let ready = { + let resolver = &mut resolvers[idx]; + refresh_resolver_path(cnx, resolver) + }; + if ready { + return Some(idx); + } + } + } + None +} + pub async fn run_client(config: &ClientConfig<'_>) -> Result { let domain_len = config.domain.len(); let mtu = compute_mtu(domain_len)?; @@ -232,6 +275,12 @@ pub async fn run_client(config: &ClientConfig<'_>) -> Result { let mut zero_send_loops = 0u64; let mut zero_send_with_streams = 0u64; let mut last_flow_block_log_at = 0u64; + let active_poll_cap_us = config.active_poll_cap_ms.saturating_mul(1_000).max(1); + let active_poll_base_us = DNS_POLL_SLICE_US.min(active_poll_cap_us); + let mut active_poll_backoff_us = active_poll_base_us; + let mut next_active_poll_at = current_time; + let mut last_dns_responses_total = 0u64; + let (mut last_enqueued_bytes_total, _) = unsafe { (*state_ptr).debug_snapshot() }; loop { let current_time = unsafe { picoquic_current_time() }; @@ -475,6 +524,43 @@ pub async fn run_client(config: &ClientConfig<'_>) -> Result { last_flow_block_log_at = now; } } + let mut force_authoritative_poll_path = None; + let now = unsafe { picoquic_current_time() }; + let pending_polls_sum = total_pending_polls(&resolvers); + let inflight_polls_sum = total_inflight_polls(&resolvers); + let dns_responses_total = total_dns_responses(&resolvers); + let (enqueued_bytes_total, _) = unsafe { (*state_ptr).debug_snapshot() }; + let has_useful_progress = dns_responses_total > last_dns_responses_total + || enqueued_bytes_total > last_enqueued_bytes_total; + if has_useful_progress { + active_poll_backoff_us = active_poll_base_us; + next_active_poll_at = now.saturating_add(active_poll_backoff_us); + } + + let active_streams = streams_len > 0; + let no_poll_work = pending_polls_sum == 0 && inflight_polls_sum == 0; + if active_streams && no_poll_work && now >= next_active_poll_at { + if let Some(target_idx) = select_active_poll_target(cnx, &mut resolvers) { + if resolvers[target_idx].mode == ResolverMode::Recursive { + resolvers[target_idx].pending_polls = + resolvers[target_idx].pending_polls.max(1); + } else { + force_authoritative_poll_path = Some(resolvers[target_idx].path_id); + } + active_poll_backoff_us = active_poll_backoff_us + .saturating_mul(2) + .min(active_poll_cap_us); + next_active_poll_at = now.saturating_add(active_poll_backoff_us); + } else { + next_active_poll_at = now.saturating_add(active_poll_base_us); + } + } + if !active_streams { + active_poll_backoff_us = active_poll_base_us; + next_active_poll_at = now; + } + last_dns_responses_total = dns_responses_total; + last_enqueued_bytes_total = enqueued_bytes_total; for resolver in resolvers.iter_mut() { if !refresh_resolver_path(cnx, resolver) { continue; @@ -492,6 +578,9 @@ pub async fn run_client(config: &ClientConfig<'_>) -> Result { if has_ready_stream && !flow_blocked { poll_deficit = 0; } + if force_authoritative_poll_path == Some(resolver.path_id) { + poll_deficit = poll_deficit.max(1); + } if poll_deficit > 0 && resolver.debug.enabled { debug!( "cc_state: {} cwnd={} in_transit={} rtt_us={} flow_blocked={} deficit={}", diff --git a/crates/slipstream-ffi/src/lib.rs b/crates/slipstream-ffi/src/lib.rs index a54df7b5..35731c61 100644 --- a/crates/slipstream-ffi/src/lib.rs +++ b/crates/slipstream-ffi/src/lib.rs @@ -32,6 +32,7 @@ pub struct ClientConfig<'a> { pub congestion_control: Option<&'a str>, pub gso: bool, pub keep_alive_interval: usize, + pub active_poll_cap_ms: u64, pub debug_poll: bool, pub debug_streams: bool, } From 15e6cfc4c862e3e4b886fb1348b9fcb04b32a54e Mon Sep 17 00:00:00 2001 From: Mygod Date: Fri, 20 Feb 2026 18:55:15 -0500 Subject: [PATCH 2/8] Exclude stale resolver queues from active-poll gate --- crates/slipstream-client/src/runtime.rs | 30 +++++++++++++------------ 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/crates/slipstream-client/src/runtime.rs b/crates/slipstream-client/src/runtime.rs index f248b2cc..a45d9c31 100644 --- a/crates/slipstream-client/src/runtime.rs +++ b/crates/slipstream-client/src/runtime.rs @@ -68,18 +68,21 @@ fn drain_disconnected_commands(command_rx: &mut mpsc::UnboundedReceiver dropped } -fn total_pending_polls(resolvers: &[ResolverState]) -> usize { - resolvers - .iter() - .map(|resolver| resolver.pending_polls) - .sum() -} - -fn total_inflight_polls(resolvers: &[ResolverState]) -> usize { - resolvers - .iter() - .map(|resolver| resolver.inflight_poll_ids.len()) - .sum() +fn active_poll_work(cnx: *mut picoquic_cnx_t, resolvers: &mut [ResolverState]) -> (usize, usize) { + let mut pending = 0usize; + let mut inflight = 0usize; + for resolver in resolvers.iter_mut() { + if !refresh_resolver_path(cnx, resolver) { + // Late responses can repopulate queue state after a path drop; keep them + // from blocking global active polling while the resolver is unreachable. + resolver.pending_polls = 0; + resolver.inflight_poll_ids.clear(); + continue; + } + pending = pending.saturating_add(resolver.pending_polls); + inflight = inflight.saturating_add(resolver.inflight_poll_ids.len()); + } + (pending, inflight) } fn total_dns_responses(resolvers: &[ResolverState]) -> u64 { @@ -526,8 +529,7 @@ pub async fn run_client(config: &ClientConfig<'_>) -> Result { } let mut force_authoritative_poll_path = None; let now = unsafe { picoquic_current_time() }; - let pending_polls_sum = total_pending_polls(&resolvers); - let inflight_polls_sum = total_inflight_polls(&resolvers); + let (pending_polls_sum, inflight_polls_sum) = active_poll_work(cnx, &mut resolvers); let dns_responses_total = total_dns_responses(&resolvers); let (enqueued_bytes_total, _) = unsafe { (*state_ptr).debug_snapshot() }; let has_useful_progress = dns_responses_total > last_dns_responses_total From 9b27235cecda8e28c7aa5ed9b85fe95bf7b4abc1 Mon Sep 17 00:00:00 2001 From: Mygod Date: Fri, 20 Feb 2026 19:43:53 -0500 Subject: [PATCH 3/8] Defer active-poll backoff until a poll is actually sent --- crates/slipstream-client/src/runtime.rs | 39 ++++++++++++++++++------- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/crates/slipstream-client/src/runtime.rs b/crates/slipstream-client/src/runtime.rs index a45d9c31..2779b29a 100644 --- a/crates/slipstream-client/src/runtime.rs +++ b/crates/slipstream-client/src/runtime.rs @@ -68,11 +68,12 @@ fn drain_disconnected_commands(command_rx: &mut mpsc::UnboundedReceiver dropped } -fn active_poll_work(cnx: *mut picoquic_cnx_t, resolvers: &mut [ResolverState]) -> (usize, usize) { +fn active_poll_work(resolvers: &mut [ResolverState]) -> (usize, usize) { let mut pending = 0usize; let mut inflight = 0usize; for resolver in resolvers.iter_mut() { - if !refresh_resolver_path(cnx, resolver) { + let reachable = resolver.added && resolver.path_id >= 0; + if !reachable { // Late responses can repopulate queue state after a path drop; keep them // from blocking global active polling while the resolver is unreachable. resolver.pending_polls = 0; @@ -92,6 +93,13 @@ fn total_dns_responses(resolvers: &[ResolverState]) -> u64 { .sum() } +fn total_polls_sent(resolvers: &[ResolverState]) -> u64 { + resolvers + .iter() + .map(|resolver| resolver.debug.polls_sent) + .sum() +} + fn select_active_poll_target( cnx: *mut picoquic_cnx_t, resolvers: &mut [ResolverState], @@ -529,7 +537,9 @@ pub async fn run_client(config: &ClientConfig<'_>) -> Result { } let mut force_authoritative_poll_path = None; let now = unsafe { picoquic_current_time() }; - let (pending_polls_sum, inflight_polls_sum) = active_poll_work(cnx, &mut resolvers); + let (pending_polls_sum, inflight_polls_sum) = active_poll_work(&mut resolvers); + let polls_sent_before = total_polls_sent(&resolvers); + let mut scheduled_active_poll = false; let dns_responses_total = total_dns_responses(&resolvers); let (enqueued_bytes_total, _) = unsafe { (*state_ptr).debug_snapshot() }; let has_useful_progress = dns_responses_total > last_dns_responses_total @@ -539,25 +549,22 @@ pub async fn run_client(config: &ClientConfig<'_>) -> Result { next_active_poll_at = now.saturating_add(active_poll_backoff_us); } - let active_streams = streams_len > 0; + let needs_active_polling = streams_len > 0 && !has_ready_stream; let no_poll_work = pending_polls_sum == 0 && inflight_polls_sum == 0; - if active_streams && no_poll_work && now >= next_active_poll_at { + if needs_active_polling && no_poll_work && now >= next_active_poll_at { if let Some(target_idx) = select_active_poll_target(cnx, &mut resolvers) { + scheduled_active_poll = true; if resolvers[target_idx].mode == ResolverMode::Recursive { resolvers[target_idx].pending_polls = resolvers[target_idx].pending_polls.max(1); } else { force_authoritative_poll_path = Some(resolvers[target_idx].path_id); } - active_poll_backoff_us = active_poll_backoff_us - .saturating_mul(2) - .min(active_poll_cap_us); - next_active_poll_at = now.saturating_add(active_poll_backoff_us); } else { next_active_poll_at = now.saturating_add(active_poll_base_us); } } - if !active_streams { + if !needs_active_polling { active_poll_backoff_us = active_poll_base_us; next_active_poll_at = now; } @@ -650,6 +657,18 @@ pub async fn run_client(config: &ClientConfig<'_>) -> Result { } } } + if needs_active_polling && scheduled_active_poll { + let poll_retry_now = unsafe { picoquic_current_time() }; + let polls_sent_after = total_polls_sent(&resolvers); + if polls_sent_after > polls_sent_before { + active_poll_backoff_us = active_poll_backoff_us + .saturating_mul(2) + .min(active_poll_cap_us); + } else { + active_poll_backoff_us = active_poll_base_us; + } + next_active_poll_at = poll_retry_now.saturating_add(active_poll_backoff_us); + } let report_time = unsafe { picoquic_current_time() }; let (enqueued_bytes, last_enqueue_at) = unsafe { (*state_ptr).debug_snapshot() }; From 3513efd69b3dac4b473e1a0b09e00065e27a57cd Mon Sep 17 00:00:00 2001 From: Mygod Date: Fri, 20 Feb 2026 20:12:10 -0500 Subject: [PATCH 4/8] Refresh resolver path before treating it as reachable --- crates/slipstream-client/src/dns/poll.rs | 2 +- crates/slipstream-client/src/runtime.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/slipstream-client/src/dns/poll.rs b/crates/slipstream-client/src/dns/poll.rs index 83e10cd4..3a6ed5b3 100644 --- a/crates/slipstream-client/src/dns/poll.rs +++ b/crates/slipstream-client/src/dns/poll.rs @@ -84,7 +84,6 @@ pub(crate) async fn send_poll_queries( resolver.local_addr_storage = Some(unsafe { std::ptr::read(local_addr_storage) }); resolver.debug.send_packets = resolver.debug.send_packets.saturating_add(1); resolver.debug.send_bytes = resolver.debug.send_bytes.saturating_add(send_length as u64); - resolver.debug.polls_sent = resolver.debug.polls_sent.saturating_add(1); let poll_id = *dns_id; let qname = build_qname(&send_buf[..send_length], config.domain) @@ -112,6 +111,7 @@ pub(crate) async fn send_poll_queries( } return Err(ClientError::new(err.to_string())); } + resolver.debug.polls_sent = resolver.debug.polls_sent.saturating_add(1); if resolver.mode == ResolverMode::Authoritative { resolver.inflight_poll_ids.insert(poll_id, current_time); } diff --git a/crates/slipstream-client/src/runtime.rs b/crates/slipstream-client/src/runtime.rs index 2779b29a..43c570ae 100644 --- a/crates/slipstream-client/src/runtime.rs +++ b/crates/slipstream-client/src/runtime.rs @@ -68,11 +68,11 @@ fn drain_disconnected_commands(command_rx: &mut mpsc::UnboundedReceiver dropped } -fn active_poll_work(resolvers: &mut [ResolverState]) -> (usize, usize) { +fn active_poll_work(cnx: *mut picoquic_cnx_t, resolvers: &mut [ResolverState]) -> (usize, usize) { let mut pending = 0usize; let mut inflight = 0usize; for resolver in resolvers.iter_mut() { - let reachable = resolver.added && resolver.path_id >= 0; + let reachable = refresh_resolver_path(cnx, resolver); if !reachable { // Late responses can repopulate queue state after a path drop; keep them // from blocking global active polling while the resolver is unreachable. @@ -537,7 +537,7 @@ pub async fn run_client(config: &ClientConfig<'_>) -> Result { } let mut force_authoritative_poll_path = None; let now = unsafe { picoquic_current_time() }; - let (pending_polls_sum, inflight_polls_sum) = active_poll_work(&mut resolvers); + let (pending_polls_sum, inflight_polls_sum) = active_poll_work(cnx, &mut resolvers); let polls_sent_before = total_polls_sent(&resolvers); let mut scheduled_active_poll = false; let dns_responses_total = total_dns_responses(&resolvers); From f4ff1ac689d5b63bda131fd212203132324fd09a Mon Sep 17 00:00:00 2001 From: Mygod Date: Fri, 20 Feb 2026 20:26:05 -0500 Subject: [PATCH 5/8] Stop treating queued-byte growth as active poll progress --- crates/slipstream-client/src/runtime.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/crates/slipstream-client/src/runtime.rs b/crates/slipstream-client/src/runtime.rs index 43c570ae..35ab0c06 100644 --- a/crates/slipstream-client/src/runtime.rs +++ b/crates/slipstream-client/src/runtime.rs @@ -291,7 +291,6 @@ pub async fn run_client(config: &ClientConfig<'_>) -> Result { let mut active_poll_backoff_us = active_poll_base_us; let mut next_active_poll_at = current_time; let mut last_dns_responses_total = 0u64; - let (mut last_enqueued_bytes_total, _) = unsafe { (*state_ptr).debug_snapshot() }; loop { let current_time = unsafe { picoquic_current_time() }; @@ -541,9 +540,7 @@ pub async fn run_client(config: &ClientConfig<'_>) -> Result { let polls_sent_before = total_polls_sent(&resolvers); let mut scheduled_active_poll = false; let dns_responses_total = total_dns_responses(&resolvers); - let (enqueued_bytes_total, _) = unsafe { (*state_ptr).debug_snapshot() }; - let has_useful_progress = dns_responses_total > last_dns_responses_total - || enqueued_bytes_total > last_enqueued_bytes_total; + let has_useful_progress = dns_responses_total > last_dns_responses_total; if has_useful_progress { active_poll_backoff_us = active_poll_base_us; next_active_poll_at = now.saturating_add(active_poll_backoff_us); @@ -569,7 +566,6 @@ pub async fn run_client(config: &ClientConfig<'_>) -> Result { next_active_poll_at = now; } last_dns_responses_total = dns_responses_total; - last_enqueued_bytes_total = enqueued_bytes_total; for resolver in resolvers.iter_mut() { if !refresh_resolver_path(cnx, resolver) { continue; From b9c8786294641ad5b14fbab0ccf1270a94567ef5 Mon Sep 17 00:00:00 2001 From: Mygod Date: Fri, 20 Feb 2026 20:48:09 -0500 Subject: [PATCH 6/8] Fix clippy --- crates/slipstream-client/src/runtime.rs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/crates/slipstream-client/src/runtime.rs b/crates/slipstream-client/src/runtime.rs index 35ab0c06..50b34762 100644 --- a/crates/slipstream-client/src/runtime.rs +++ b/crates/slipstream-client/src/runtime.rs @@ -106,15 +106,11 @@ fn select_active_poll_target( ) -> Option { let modes = [ResolverMode::Recursive, ResolverMode::Authoritative]; for mode in modes { - for idx in 0..resolvers.len() { - if resolvers[idx].mode != mode { + for (idx, resolver) in resolvers.iter_mut().enumerate() { + if resolver.mode != mode { continue; } - let ready = { - let resolver = &mut resolvers[idx]; - refresh_resolver_path(cnx, resolver) - }; - if ready { + if refresh_resolver_path(cnx, resolver) { return Some(idx); } } From 3c467da828edc0adbd7f0cc4c6f73dbb45c9a629 Mon Sep 17 00:00:00 2001 From: Mygod Date: Fri, 20 Feb 2026 21:03:41 -0500 Subject: [PATCH 7/8] Avoid resetting active-poll deadline on unrelated responses --- crates/slipstream-client/src/runtime.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/slipstream-client/src/runtime.rs b/crates/slipstream-client/src/runtime.rs index 50b34762..2bd7a6a4 100644 --- a/crates/slipstream-client/src/runtime.rs +++ b/crates/slipstream-client/src/runtime.rs @@ -536,14 +536,14 @@ pub async fn run_client(config: &ClientConfig<'_>) -> Result { let polls_sent_before = total_polls_sent(&resolvers); let mut scheduled_active_poll = false; let dns_responses_total = total_dns_responses(&resolvers); + let needs_active_polling = streams_len > 0 && !has_ready_stream; + let no_poll_work = pending_polls_sum == 0 && inflight_polls_sum == 0; let has_useful_progress = dns_responses_total > last_dns_responses_total; - if has_useful_progress { + if has_useful_progress && (!needs_active_polling || !no_poll_work) { active_poll_backoff_us = active_poll_base_us; next_active_poll_at = now.saturating_add(active_poll_backoff_us); } - let needs_active_polling = streams_len > 0 && !has_ready_stream; - let no_poll_work = pending_polls_sum == 0 && inflight_polls_sum == 0; if needs_active_polling && no_poll_work && now >= next_active_poll_at { if let Some(target_idx) = select_active_poll_target(cnx, &mut resolvers) { scheduled_active_poll = true; From ee689489d5da8b4db11aaf7565b069ea489d96ef Mon Sep 17 00:00:00 2001 From: Mygod Date: Fri, 20 Feb 2026 22:46:44 -0500 Subject: [PATCH 8/8] Reset backoff only on response-driven inflight drops --- crates/slipstream-client/src/dns/debug.rs | 2 ++ crates/slipstream-client/src/dns/response.rs | 13 +++++++++---- crates/slipstream-client/src/runtime.rs | 15 ++++++++++++++- 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/crates/slipstream-client/src/dns/debug.rs b/crates/slipstream-client/src/dns/debug.rs index 582b393a..a00876c8 100644 --- a/crates/slipstream-client/src/dns/debug.rs +++ b/crates/slipstream-client/src/dns/debug.rs @@ -9,6 +9,7 @@ pub(crate) struct DebugMetrics { pub(crate) enabled: bool, pub(crate) last_report_at: u64, pub(crate) dns_responses: u64, + pub(crate) poll_completions: u64, pub(crate) zero_send_loops: u64, pub(crate) zero_send_with_streams: u64, pub(crate) enqueued_bytes: u64, @@ -31,6 +32,7 @@ impl DebugMetrics { enabled, last_report_at: 0, dns_responses: 0, + poll_completions: 0, zero_send_loops: 0, zero_send_with_streams: 0, enqueued_bytes: 0, diff --git a/crates/slipstream-client/src/dns/response.rs b/crates/slipstream-client/src/dns/response.rs index a0f7152a..f40e1790 100644 --- a/crates/slipstream-client/src/dns/response.rs +++ b/crates/slipstream-client/src/dns/response.rs @@ -72,8 +72,11 @@ pub(crate) fn handle_dns_response( } resolver.debug.dns_responses = resolver.debug.dns_responses.saturating_add(1); if let Some(response_id) = response_id { - if resolver.mode == ResolverMode::Authoritative { - resolver.inflight_poll_ids.remove(&response_id); + if resolver.mode == ResolverMode::Authoritative + && resolver.inflight_poll_ids.remove(&response_id).is_some() + { + resolver.debug.poll_completions = + resolver.debug.poll_completions.saturating_add(1); } } if resolver.mode == ResolverMode::Recursive { @@ -84,8 +87,10 @@ pub(crate) fn handle_dns_response( } else if let Some(response_id) = response_id { if let Some(resolver) = find_resolver_by_addr(ctx.resolvers, peer) { resolver.debug.dns_responses = resolver.debug.dns_responses.saturating_add(1); - if resolver.mode == ResolverMode::Authoritative { - resolver.inflight_poll_ids.remove(&response_id); + if resolver.mode == ResolverMode::Authoritative + && resolver.inflight_poll_ids.remove(&response_id).is_some() + { + resolver.debug.poll_completions = resolver.debug.poll_completions.saturating_add(1); } } } diff --git a/crates/slipstream-client/src/runtime.rs b/crates/slipstream-client/src/runtime.rs index 2bd7a6a4..79c22ce8 100644 --- a/crates/slipstream-client/src/runtime.rs +++ b/crates/slipstream-client/src/runtime.rs @@ -93,6 +93,13 @@ fn total_dns_responses(resolvers: &[ResolverState]) -> u64 { .sum() } +fn total_poll_completions(resolvers: &[ResolverState]) -> u64 { + resolvers + .iter() + .map(|resolver| resolver.debug.poll_completions) + .sum() +} + fn total_polls_sent(resolvers: &[ResolverState]) -> u64 { resolvers .iter() @@ -287,6 +294,7 @@ pub async fn run_client(config: &ClientConfig<'_>) -> Result { let mut active_poll_backoff_us = active_poll_base_us; let mut next_active_poll_at = current_time; let mut last_dns_responses_total = 0u64; + let mut last_poll_completions_total = 0u64; loop { let current_time = unsafe { picoquic_current_time() }; @@ -536,10 +544,14 @@ pub async fn run_client(config: &ClientConfig<'_>) -> Result { let polls_sent_before = total_polls_sent(&resolvers); let mut scheduled_active_poll = false; let dns_responses_total = total_dns_responses(&resolvers); + let poll_completions_total = total_poll_completions(&resolvers); let needs_active_polling = streams_len > 0 && !has_ready_stream; let no_poll_work = pending_polls_sum == 0 && inflight_polls_sum == 0; let has_useful_progress = dns_responses_total > last_dns_responses_total; - if has_useful_progress && (!needs_active_polling || !no_poll_work) { + let poll_response_completed = poll_completions_total > last_poll_completions_total; + if has_useful_progress + && (!needs_active_polling || !no_poll_work || poll_response_completed) + { active_poll_backoff_us = active_poll_base_us; next_active_poll_at = now.saturating_add(active_poll_backoff_us); } @@ -562,6 +574,7 @@ pub async fn run_client(config: &ClientConfig<'_>) -> Result { next_active_poll_at = now; } last_dns_responses_total = dns_responses_total; + last_poll_completions_total = poll_completions_total; for resolver in resolvers.iter_mut() { if !refresh_resolver_path(cnx, resolver) { continue;