From 02b33a3e6dc595eb3d843a467375ce782858fd54 Mon Sep 17 00:00:00 2001 From: chuan Date: Wed, 24 Jun 2026 01:45:33 +0800 Subject: [PATCH] feat: report speed in MB/s instead of Mbps - drop the *8 bit conversion so throughput is MB/s, matching what clients like v2rayN display - rename speed_mbps -> speed_mbs across speed/report/commands - narrow the ranking bucket to 0.1 MB/s so slow (~0.1-0.3 MB/s) nodes still separate by speed instead of collapsing into one bucket - update table/CSV/alias labels, color thresholds, --min-speed help, and README --- README.md | 6 +++--- src/cli.rs | 4 ++-- src/commands.rs | 8 ++++---- src/report.rs | 20 ++++++++++---------- src/speed.rs | 32 ++++++++++++++++---------------- 5 files changed, 35 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index fb37fd9..dcb741d 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,7 @@ flowchart LR | `-6, --ipv6` | off | 同时测 IPv6 | | `--ping-max` | 300 | 第一次 ping(TCP)延迟上限 ms | | `--lat-max` | 0 | 第二次 ping(真实延迟)上限 ms(0 关闭) | -| `--min-speed` | 0 | 最低速度 Mbps(0 关闭) | +| `--min-speed` | 0 | 最低速度 MB/s(0 关闭) | | `-o, --output` | result | 输出目录 | | `-v, --verbose` | off | 打印各阶段结果表 | @@ -140,11 +140,11 @@ auto 使用各阶段默认的超时、并发、丢包等参数;需精调时单 | `-c, --concurrency` | 1 | 并发下载数 | | `-t, --timeout` | 10 | 单 IP 下载上限(秒) | | `--bytes` | 10000000 | 每 IP 下载字节数 | -| `--min` | 0 | 最低速度 Mbps(0 关闭) | +| `--min` | 0 | 最低速度 MB/s(0 关闭) | | `-p, --top` | 0 | 保留最优 N(0 全部) | | `-o` / `-v` | result | 输出目录 / 结果表 | -排序:速度每 1 Mbps 一档,先比档位,同档按延迟升序。 +排序:速度每 0.1 MB/s 一档,先比档位,同档按延迟升序。 ### export — `fast-xray export [NODE] [OPTIONS]` diff --git a/src/cli.rs b/src/cli.rs index a777654..835572c 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -71,7 +71,7 @@ pub(crate) struct AutoArgs { #[arg(long, default_value_t = 0.0)] pub(crate) lat_max: f64, - /// Quality gate: drop IPs slower than this (Mbps). 0 = off. + /// Quality gate: drop IPs slower than this (MB/s). 0 = off. #[arg(long, default_value_t = 0.0)] pub(crate) min_speed: f64, @@ -199,7 +199,7 @@ pub(crate) struct SpeedArgs { #[arg(long, default_value_t = 10_000_000)] pub(crate) bytes: u64, - /// Reject IPs slower than this (Mbps). 0 = keep all. + /// Reject IPs slower than this (MB/s). 0 = keep all. #[arg(long = "min", default_value_t = 0.0)] pub(crate) min_speed: f64, diff --git a/src/commands.rs b/src/commands.rs index 56f52e9..e08c900 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -211,7 +211,7 @@ pub(crate) async fn run_auto(args: AutoArgs) -> Result<()> { let mut body = String::new(); for r in &speed_results { let alias = - format!("{:.2}M-{:.0}ms-{}", r.speed_mbps, r.latency.as_secs_f64() * 1000.0, r.ip); + format!("{:.2}MB-{:.0}ms-{}", r.speed_mbs, r.latency.as_secs_f64() * 1000.0, r.ip); body.push_str(&node.to_url(r.ip, &alias)); body.push('\n'); } @@ -324,10 +324,10 @@ async fn stage_speed( let mut results = speed::run(node, inputs, concurrency, timeout, bytes, min_speed, |p| { let addr = style(format!("{:<15}", p.ip.to_string())).dim(); - let line = match p.speed_mbps { - Some(mbps) => { + let line = match p.speed_mbs { + Some(mbs) => { kept += 1; - format!("{} {addr} {}", style("✓").green().dim(), style(format!("{mbps:.2}Mbps")).green().dim()) + format!("{} {addr} {}", style("✓").green().dim(), style(format!("{mbs:.2}MB/s")).green().dim()) } None => format!("{} {addr} {}", style("x").red().dim(), style("failed").dim()), }; diff --git a/src/report.rs b/src/report.rs index 2497920..1f3c5ba 100644 --- a/src/report.rs +++ b/src/report.rs @@ -96,14 +96,14 @@ impl LiveProgress { pub(crate) struct ExportRow { pub(crate) ip: IpAddr, latency_ms: Option, - speed_mbps: Option, + speed_mbs: Option, } impl ExportRow { /// Richest alias the available columns allow. pub(crate) fn alias(&self) -> String { - match (self.speed_mbps, self.latency_ms) { - (Some(s), Some(l)) => format!("{s:.2}M-{l:.0}ms-{}", self.ip), + match (self.speed_mbs, self.latency_ms) { + (Some(s), Some(l)) => format!("{s:.2}MB-{l:.0}ms-{}", self.ip), (_, Some(l)) => format!("{l:.0}ms-{}", self.ip), _ => self.ip.to_string(), } @@ -156,7 +156,7 @@ pub(crate) fn read_latency_csv(path: &Path) -> Result> { } /// Parse export input: first comma field is the IP; optional 2nd = latency(ms), -/// 3rd = speed(Mbps). Header/blank/unparseable lines are skipped. +/// 3rd = speed(MB/s). Header/blank/unparseable lines are skipped. pub(crate) fn read_export_rows(path: &Path) -> Result> { let text = std::fs::read_to_string(path).with_context(|| format!("read input {}", path.display()))?; @@ -169,7 +169,7 @@ pub(crate) fn read_export_rows(path: &Path) -> Result> { rows.push(ExportRow { ip, latency_ms: fields.next().and_then(|s| s.trim().parse::().ok()), - speed_mbps: fields.next().and_then(|s| s.trim().parse::().ok()), + speed_mbs: fields.next().and_then(|s| s.trim().parse::().ok()), }); } Ok(rows) @@ -202,10 +202,10 @@ pub(crate) fn print_speed_table(results: &[speed::SpeedResult]) { eprintln!(" {:>3} {:<39} {:>9} {:>10}", "#", "IP", "Latency", "Speed"); for (i, r) in results.iter().enumerate() { let ms = r.latency.as_secs_f64() * 1000.0; - let speed = format!("{:.2} Mbps", r.speed_mbps); - let colored = if r.speed_mbps >= 20.0 { + let speed = format!("{:.2} MB/s", r.speed_mbs); + let colored = if r.speed_mbs >= 2.5 { style(speed).green() - } else if r.speed_mbps >= 5.0 { + } else if r.speed_mbs >= 0.6 { style(speed).yellow() } else { style(speed).red() @@ -245,13 +245,13 @@ pub(crate) fn write_csv(results: &[T], path: &Path) -> Result<()> { /// Speed CSV: IP, latency, speed, best first. pub(crate) fn write_speed_csv(results: &[speed::SpeedResult], path: &Path) -> Result<()> { - let mut body = String::from("IP,Latency(ms),Speed(Mbps)\n"); + let mut body = String::from("IP,Latency(ms),Speed(MB/s)\n"); for r in results { body.push_str(&format!( "{},{:.2},{:.2}\n", r.ip, r.latency.as_secs_f64() * 1000.0, - r.speed_mbps + r.speed_mbs )); } write_file(path, &body)?; diff --git a/src/speed.rs b/src/speed.rs index 97ecc48..c667b5a 100644 --- a/src/speed.rs +++ b/src/speed.rs @@ -15,30 +15,30 @@ const TARGET_HOST: &str = "cachefly.cachefly.net"; const TARGET_PORT: u16 = 443; const TARGET_PATH: &str = "/50mb.test"; -/// One IP with its (carried-over) latency and measured download speed. +/// One IP with its (carried-over) latency and measured download speed (MB/s). #[derive(Clone, Debug)] pub struct SpeedResult { pub ip: IpAddr, pub latency: Duration, - pub speed_mbps: f64, + pub speed_mbs: f64, } /// Progress event, emitted once per finished measurement. pub struct Probed { pub ip: IpAddr, - pub speed_mbps: Option, + pub speed_mbs: Option, pub done: usize, #[allow(dead_code)] // available to callers that want a denominator. pub total: usize, } -/// 1 Mbps-wide bucket used for ranking (higher bucket wins, ties by latency). -pub fn bucket(speed_mbps: f64) -> i64 { - speed_mbps.floor() as i64 +/// 0.1 MB/s-wide bucket used for ranking (higher bucket wins, ties by latency). +pub fn bucket(speed_mbs: f64) -> i64 { + (speed_mbs * 10.0).floor() as i64 } /// Speed-test every input IP, carrying its latency through. Keeps results at -/// or above `min_speed` Mbps (zero = disabled), ranked best-first (speed +/// or above `min_speed` MB/s (zero = disabled), ranked best-first (speed /// bucket desc, then latency asc). pub async fn run( node: &VlessNode, @@ -62,24 +62,24 @@ pub async fn run( done += 1; // Kept only if it succeeded and meets the optional min-speed floor. let kept = match outcome { - Ok(mbps) if min_speed <= 0.0 || mbps >= min_speed => { - results.push(SpeedResult { ip, latency, speed_mbps: mbps }); - Some(mbps) + Ok(mbs) if min_speed <= 0.0 || mbs >= min_speed => { + results.push(SpeedResult { ip, latency, speed_mbs: mbs }); + Some(mbs) } _ => None, }; - on_progress(Probed { ip, speed_mbps: kept, done, total }); + on_progress(Probed { ip, speed_mbs: kept, done, total }); } results.sort_by(|a, b| { - bucket(b.speed_mbps) - .cmp(&bucket(a.speed_mbps)) + bucket(b.speed_mbs) + .cmp(&bucket(a.speed_mbs)) .then(a.latency.cmp(&b.latency)) }); results } -/// Download through the tunnel and return throughput in Mbps. Stops as soon as +/// Download through the tunnel and return throughput in MB/s. Stops as soon as /// throughput settles out of TCP slow start (the steady-state speed), or when /// `limit_bytes` / `timeout` is hit — whichever comes first. pub async fn measure_download( @@ -157,7 +157,7 @@ pub async fn measure_download( // Evaluate a window once it fills (only after the body has started). let win_elapsed = win_start.elapsed(); if header_done && win_elapsed >= WINDOW { - let speed = win_bytes as f64 * 8.0 / win_elapsed.as_secs_f64() / 1_000_000.0; + let speed = win_bytes as f64 / win_elapsed.as_secs_f64() / 1_000_000.0; if let Some(prev) = prev_speed { if speed <= prev * SETTLE_TOLERANCE { steady = Some(speed.max(prev)); @@ -181,7 +181,7 @@ pub async fn measure_download( // transfer ended — EOF, byte cap, or time cap — before it settled). Ok(steady.unwrap_or_else(|| { let elapsed = started.elapsed().as_secs_f64().max(0.001); - body_bytes as f64 * 8.0 / elapsed / 1_000_000.0 + body_bytes as f64 / elapsed / 1_000_000.0 })) }