This commit is contained in:
Andrew Pimenov 2026-08-26 17:08:49 +03:00 • committed by GitHub
commit da0755e238
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -1,4 +1,15 @@
from __future__ import annotations from __future__ import annotations
import asyncio
import time
from collections.abc import Awaitable, Callable
from netsleuth.models import SpeedResult, TierAttempt
import json
import httpx
from netsleuth.config import Speedtest
from netsleuth.config import BufferbloatBands
from netsleuth.interpret import grade_bufferbloat
from netsleuth.stats import rtt_stats
import re import re
from urllib.parse import parse_qs from urllib.parse import parse_qs
@ -39,7 +50,10 @@ def parse_server_timing_cfl4(header: str) -> CfL4Stats | None:
match = _CFL4_RE.search(header or "") match = _CFL4_RE.search(header or "")
if not match: if not match:
return None return None
fields = {k: v[0] for k, v in parse_qs(match.group("query"), keep_blank_values=True).items()} fields = {
k: v[0]
for k, v in parse_qs(match.group("query"), keep_blank_values=True).items()
}
return CfL4Stats( return CfL4Stats(
rtt_ms=_us_to_ms(fields.get("rtt")), rtt_ms=_us_to_ms(fields.get("rtt")),
min_rtt_ms=_us_to_ms(fields.get("min_rtt")), min_rtt_ms=_us_to_ms(fields.get("min_rtt")),
@ -51,18 +65,16 @@ def parse_server_timing_cfl4(header: str) -> CfL4Stats | None:
) )
def bufferbloat_delta(idle_rtt_ms: float | None, loaded_rtts_ms: list[float]) -> float | None: def bufferbloat_delta(
idle_rtt_ms: float | None, loaded_rtts_ms: list[float]
) -> float | None:
if idle_rtt_ms is None or not loaded_rtts_ms: if idle_rtt_ms is None or not loaded_rtts_ms:
return None return None
loaded = percentile(sorted(loaded_rtts_ms), 95.0) loaded = percentile(sorted(loaded_rtts_ms), 95.0)
return round(max(0.0, loaded - idle_rtt_ms), 3) return round(max(0.0, loaded - idle_rtt_ms), 3)
import asyncio
import time
from collections.abc import Awaitable, Callable
from netsleuth.models import SpeedResult, TierAttempt
NDT7_CONSENT_NOTICE = ( NDT7_CONSENT_NOTICE = (
"M-Lab NDT7 publishes every measurement as public CC0 open data, including your " "M-Lab NDT7 publishes every measurement as public CC0 open data, including your "
@ -93,20 +105,24 @@ async def run_speed_cascade(
duration_ms = int((time.perf_counter() - began) * 1000) duration_ms = int((time.perf_counter() - began) * 1000)
if not result.download_mbps: if not result.download_mbps:
attempts.append( attempts.append(
TierAttempt(tier=name, ok=False, reason="no throughput measured", duration_ms=duration_ms) TierAttempt(
tier=name,
ok=False,
reason="no throughput measured",
duration_ms=duration_ms,
)
) )
continue continue
attempts.append(TierAttempt(tier=name, ok=True, reason=None, duration_ms=duration_ms)) attempts.append(
TierAttempt(tier=name, ok=True, reason=None, duration_ms=duration_ms)
)
result.tier_attempts = attempts result.tier_attempts = attempts
return result return result
return SpeedResult(method="none", tier_attempts=attempts) return SpeedResult(method="none", tier_attempts=attempts)
import json
import httpx
from netsleuth.config import Speedtest
def ookla_interface_args(iface_name: str | None, ipv4: str | None) -> list[str]: def ookla_interface_args(iface_name: str | None, ipv4: str | None) -> list[str]:
@ -138,32 +154,58 @@ async def tier_ookla(
server_info = payload.get("server") or {} server_info = payload.get("server") or {}
return SpeedResult( return SpeedResult(
method="ookla_bin", method="ookla_bin",
download_mbps=round(mbps(download.get("bytes", 0), (download.get("elapsed", 0) or 0) / 1000), 3), download_mbps=round(
upload_mbps=round(mbps(upload.get("bytes", 0), (upload.get("elapsed", 0) or 0) / 1000), 3), mbps(download.get("bytes", 0), (download.get("elapsed", 0) or 0) / 1000), 3
),
upload_mbps=round(
mbps(upload.get("bytes", 0), (upload.get("elapsed", 0) or 0) / 1000), 3
),
server=f"{server_info.get('name', '')} ({server_info.get('location', '')})".strip(), server=f"{server_info.get('name', '')} ({server_info.get('location', '')})".strip(),
idle_rtt_ms=(payload.get("ping") or {}).get("latency"), idle_rtt_ms=(payload.get("ping") or {}).get("latency"),
server_country=server_info.get("country") or None, server_country=server_info.get("country") or None,
) )
async def tier_cloudflare(client: httpx.AsyncClient, cfg: Speedtest, timeout: float) -> SpeedResult: async def tier_cloudflare(
down_samples: list[tuple[int, float]] = [] client: httpx.AsyncClient, cfg: Speedtest, timeout: float
cfl4: CfL4Stats | None = None ) -> SpeedResult:
for size in cfg.download_sizes_bytes: async def _download(size: int) -> tuple[int, float, str]:
began = time.perf_counter() began = time.perf_counter()
response = await client.get( response = await client.get(
f"{cfg.cloudflare_base_url}/__down", params={"bytes": size}, timeout=timeout f"{cfg.cloudflare_base_url}/__down", params={"bytes": size}, timeout=timeout
) )
response.raise_for_status() response.raise_for_status()
down_samples.append((len(response.content), time.perf_counter() - began)) return (
cfl4 = parse_server_timing_cfl4(response.headers.get("server-timing", "")) or cfl4 len(response.content),
up_samples: list[tuple[int, float]] = [] time.perf_counter() - began,
for size in cfg.upload_sizes_bytes: response.headers.get("server-timing", ""),
)
down_samples: list[tuple[int, float]] = []
cfl4: CfL4Stats | None = None
down_results = await asyncio.gather(
*[_download(size) for size in cfg.download_sizes_bytes]
)
for length, duration, timing in down_results:
down_samples.append((length, duration))
cfl4 = parse_server_timing_cfl4(timing) or cfl4
async def _upload(size: int) -> tuple[int, float]:
payload = b"\x00" * size payload = b"\x00" * size
began = time.perf_counter() began = time.perf_counter()
response = await client.post(f"{cfg.cloudflare_base_url}/__up", content=payload, timeout=timeout) response = await client.post(
f"{cfg.cloudflare_base_url}/__up", content=payload, timeout=timeout
)
response.raise_for_status() response.raise_for_status()
up_samples.append((size, time.perf_counter() - began)) return size, time.perf_counter() - began
up_samples: list[tuple[int, float]] = []
up_results = await asyncio.gather(
*[_upload(size) for size in cfg.upload_sizes_bytes]
)
for size_out, duration in up_results:
up_samples.append((size_out, duration))
return SpeedResult( return SpeedResult(
method="cloudflare", method="cloudflare",
download_mbps=throughput_from_samples(down_samples), download_mbps=throughput_from_samples(down_samples),
@ -173,7 +215,9 @@ async def tier_cloudflare(client: httpx.AsyncClient, cfg: Speedtest, timeout: fl
) )
async def tier_fastcom(client: httpx.AsyncClient, cfg: Speedtest, timeout: float) -> SpeedResult: async def tier_fastcom(
client: httpx.AsyncClient, cfg: Speedtest, timeout: float
) -> SpeedResult:
response = await client.get( response = await client.get(
cfg.fastcom_api_url, cfg.fastcom_api_url,
params={"https": "true", "token": cfg.fastcom_api_token, "urlCount": "3"}, params={"https": "true", "token": cfg.fastcom_api_token, "urlCount": "3"},
@ -197,14 +241,21 @@ async def tier_fastcom(client: httpx.AsyncClient, cfg: Speedtest, timeout: float
method="fastcom", method="fastcom",
download_mbps=throughput_from_samples(samples), download_mbps=throughput_from_samples(samples),
upload_mbps=None, upload_mbps=None,
server=targets[0].get("url").split("/")[2] if targets and targets[0].get("url") else None, server=targets[0].get("url").split("/")[2]
if targets and targets[0].get("url")
else None,
netflix_oca_onnet=on_net, netflix_oca_onnet=on_net,
server_country=(targets[0].get("location") or {}).get("country") if targets else None, server_country=(targets[0].get("location") or {}).get("country")
if targets
else None,
) )
async def tier_ndt7( async def tier_ndt7(
client: httpx.AsyncClient, cfg: Speedtest, timeout: float, source_ip: str | None = None client: httpx.AsyncClient,
cfg: Speedtest,
timeout: float,
source_ip: str | None = None,
) -> SpeedResult: ) -> SpeedResult:
import websockets import websockets
@ -225,7 +276,11 @@ async def tier_ndt7(
message = await asyncio.wait_for(socket.recv(), timeout=timeout) message = await asyncio.wait_for(socket.recv(), timeout=timeout)
except (TimeoutError, Exception): except (TimeoutError, Exception):
break break
total += len(message) if isinstance(message, (bytes, bytearray)) else len(message.encode()) total += (
len(message)
if isinstance(message, (bytes, bytearray))
else len(message.encode())
)
elapsed = time.perf_counter() - began elapsed = time.perf_counter() - began
return SpeedResult( return SpeedResult(
method="ndt7", method="ndt7",
@ -236,9 +291,6 @@ async def tier_ndt7(
) )
from netsleuth.config import BufferbloatBands
from netsleuth.interpret import grade_bufferbloat
from netsleuth.stats import rtt_stats
async def probe_while( async def probe_while(
@ -276,7 +328,11 @@ async def measure_with_bufferbloat(
result.bufferbloat_down_ms = bufferbloat_delta(idle_rtt_ms, down_samples) result.bufferbloat_down_ms = bufferbloat_delta(idle_rtt_ms, down_samples)
result.bufferbloat_up_ms = bufferbloat_delta(idle_rtt_ms, up_samples) result.bufferbloat_up_ms = bufferbloat_delta(idle_rtt_ms, up_samples)
worst_delta = max( worst_delta = max(
[d for d in (result.bufferbloat_down_ms, result.bufferbloat_up_ms) if d is not None], [
d
for d in (result.bufferbloat_down_ms, result.bufferbloat_up_ms)
if d is not None
],
default=None, default=None,
) )
result.bufferbloat_grade = grade_bufferbloat(worst_delta, bands) result.bufferbloat_grade = grade_bufferbloat(worst_delta, bands)