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
This commit is contained in:
@@ -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]`
|
||||
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
|
||||
+4
-4
@@ -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()),
|
||||
};
|
||||
|
||||
+10
-10
@@ -96,14 +96,14 @@ impl LiveProgress {
|
||||
pub(crate) struct ExportRow {
|
||||
pub(crate) ip: IpAddr,
|
||||
latency_ms: Option<f64>,
|
||||
speed_mbps: Option<f64>,
|
||||
speed_mbs: Option<f64>,
|
||||
}
|
||||
|
||||
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<Vec<(IpAddr, Duration)>> {
|
||||
}
|
||||
|
||||
/// 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<Vec<ExportRow>> {
|
||||
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<Vec<ExportRow>> {
|
||||
rows.push(ExportRow {
|
||||
ip,
|
||||
latency_ms: fields.next().and_then(|s| s.trim().parse::<f64>().ok()),
|
||||
speed_mbps: fields.next().and_then(|s| s.trim().parse::<f64>().ok()),
|
||||
speed_mbs: fields.next().and_then(|s| s.trim().parse::<f64>().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<T: Ranked>(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)?;
|
||||
|
||||
+16
-16
@@ -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<f64>,
|
||||
pub speed_mbs: Option<f64>,
|
||||
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
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user