From 0db5111267e7fa7b2ef7fa2b3b8d9006e98fae1d Mon Sep 17 00:00:00 2001 From: chuan Date: Tue, 23 Jun 2026 23:41:33 +0800 Subject: [PATCH] feat: sample latency multiple times and tolerate keep-alive drops - time several round-trips on the warm connection and report the fastest, shedding transient network jitter - time the warm-up request too and fall back to it if the node won't honour keep-alive, instead of failing the IP outright - stop at the first reuse failure and keep the samples already gathered --- src/latency.rs | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/src/latency.rs b/src/latency.rs index 1e2b08e..dc2f7ad 100644 --- a/src/latency.rs +++ b/src/latency.rs @@ -73,13 +73,17 @@ pub async fn run( results } +/// Timed round-trips on the warm connection; the fastest is reported to shed +/// transient network jitter. +const SAMPLES: usize = 3; + /// Measure the steady-state round-trip latency through the node via `ip`. /// /// Building the tunnel and the inner TLS is a one-time cost a live proxy /// session pays only once, so we exclude it: set up the connection, send a -/// throwaway warm-up request, then time a second request on the warm -/// connection. That matches what clients like v2rayN report — the per-request -/// RTT, not the cold connect-and-handshake total. +/// throwaway warm-up request, then time several follow-up requests on the warm +/// connection and keep the fastest. That matches what clients like v2rayN +/// report — the per-request RTT, not the cold connect-and-handshake total. pub async fn measure(node: &VlessNode, ip: IpAddr, timeout: Duration) -> Result { let attempt = tokio::time::timeout(timeout, async { let tunnel = vless::connect(node, ip, TARGET_HOST, TARGET_PORT).await?; @@ -89,12 +93,24 @@ pub async fn measure(node: &VlessNode, ip: IpAddr, timeout: Duration) -> Result< let mut stream = vless::tls_connector().connect(sni, tunnel).await?; // Warm-up: absorbs the tunnel + TLS setup cost (and TCP slow start). + // Timed too, so it can stand in as the result if the node won't honour + // keep-alive for the follow-ups, rather than failing the IP outright. + let warmup_start = Instant::now(); request_once(&mut stream).await?; + let warmup_rtt = warmup_start.elapsed(); - // Timed: pure request round-trip on the established connection. - let started = Instant::now(); - request_once(&mut stream).await?; - Ok(started.elapsed()) + // Fastest of several round-trips on the warm connection. Stop at the + // first reuse failure and keep whatever samples we did gather. + let mut best: Option = None; + for _ in 0..SAMPLES { + let started = Instant::now(); + if request_once(&mut stream).await.is_err() { + break; + } + let rtt = started.elapsed(); + best = Some(best.map_or(rtt, |b| b.min(rtt))); + } + Ok(best.unwrap_or(warmup_rtt)) }) .await;