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
This commit is contained in:
chuan
2026-06-23 23:41:33 +08:00
Unverified
parent 125a3b342e
commit 0db5111267
+23 -7
View File
@@ -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<Duration> {
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<Duration> = 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;