nd_300/diagnostics/
bufferbloat.rs1use serde::Serialize;
2
3#[derive(Debug, Clone, Serialize)]
4pub struct BufferbloatResult {
5 pub unloaded_latency_ms: f64,
6 pub loaded_latency_ms: Option<f64>,
7 pub grade: String,
8 pub bloat_ms: Option<f64>,
9 pub description: String,
10}
11
12pub async fn collect() -> Option<BufferbloatResult> {
13 let unloaded = measure_latency("1.1.1.1", 3).await?;
15
16 let loaded = measure_loaded_latency(unloaded).await;
22
23 let (grade, description, bloat) = match loaded {
24 Some(loaded_ms) => {
25 let diff = loaded_ms - unloaded;
26 let grade = if diff < 5.0 {
27 "A+"
28 } else if diff < 30.0 {
29 "A"
30 } else if diff < 60.0 {
31 "B"
32 } else if diff < 200.0 {
33 "C"
34 } else if diff < 400.0 {
35 "D"
36 } else {
37 "F"
38 };
39
40 let desc = match grade {
41 "A+" | "A" => "Excellent - minimal bufferbloat".to_string(),
42 "B" => "Good - minor bufferbloat".to_string(),
43 "C" => "Fair - moderate bufferbloat".to_string(),
44 "D" => "Poor - significant bufferbloat".to_string(),
45 _ => "Severe bufferbloat detected".to_string(),
46 };
47
48 (grade.to_string(), desc, Some(diff))
49 }
50 None => {
51 let grade = if unloaded < 20.0 {
53 "A"
54 } else if unloaded < 50.0 {
55 "B"
56 } else {
57 "C"
58 };
59 (
60 grade.to_string(),
61 "Loaded latency not measured (run without --fast for full test)".to_string(),
62 None,
63 )
64 }
65 };
66
67 Some(BufferbloatResult {
68 unloaded_latency_ms: unloaded,
69 loaded_latency_ms: loaded,
70 grade,
71 bloat_ms: bloat,
72 description,
73 })
74}
75
76async fn measure_latency(host: &str, count: u32) -> Option<f64> {
77 #[cfg(windows)]
78 let output = {
79 let mut cmd = tokio::process::Command::new("ping");
80 cmd.args(["-n", &count.to_string(), "-w", "2000", host]);
81 super::util::run_with_timeout(cmd, super::util::SLOW).await
82 };
83
84 #[cfg(unix)]
85 let output = {
86 let mut cmd = tokio::process::Command::new("ping");
87 cmd.args(["-c", &count.to_string(), "-W", "2", host]);
88 super::util::run_with_timeout(cmd, super::util::SLOW).await
89 };
90
91 let output = output?;
92 if !output.status.success() {
93 return None;
94 }
95
96 let text = String::from_utf8_lossy(&output.stdout);
97 let mut times = Vec::new();
98
99 for line in text.lines() {
100 if let Some(pos) = line.find("time=") {
101 let after = &line[pos + 5..];
102 let num: String = after
103 .chars()
104 .take_while(|c| c.is_ascii_digit() || *c == '.')
105 .collect();
106 if let Ok(ms) = num.parse::<f64>() {
107 times.push(ms);
108 }
109 } else if let Some(pos) = line.find("time<") {
110 let after = &line[pos + 5..];
111 let num: String = after
112 .chars()
113 .take_while(|c| c.is_ascii_digit() || *c == '.')
114 .collect();
115 if let Ok(ms) = num.parse::<f64>() {
116 times.push(ms);
117 }
118 }
119 }
120
121 if times.is_empty() {
122 None
123 } else {
124 Some(times.iter().sum::<f64>() / times.len() as f64)
125 }
126}
127
128async fn measure_loaded_latency(_unloaded: f64) -> Option<f64> {
129 let client = reqwest::Client::builder()
131 .timeout(std::time::Duration::from_secs(10))
132 .build()
133 .ok()?;
134
135 let download = tokio::spawn(async move {
137 if let Ok(resp) = client
138 .get("https://speed.cloudflare.com/__down?bytes=25000000")
139 .send()
140 .await
141 {
142 let _ = resp.bytes().await;
143 }
144 });
145
146 tokio::time::sleep(std::time::Duration::from_millis(500)).await;
148 let loaded = measure_latency("1.1.1.1", 3).await;
149
150 let _ = download.await;
151
152 loaded
153}