speed: throughput math, cfL4 header parsing, bufferbloat delta

This commit is contained in:
Andrew Pimenov 2026-08-09 02:37:14 +03:00
parent 8d38788671
commit 5eec7aacab
2 changed files with 144 additions and 0 deletions

58
src/netcheck/speed.py Normal file
View file

@ -0,0 +1,58 @@
from __future__ import annotations
import re
from urllib.parse import parse_qs
from netcheck.models import CfL4Stats
from netcheck.stats import percentile
_CFL4_RE = re.compile(r'cfL4\s*;\s*desc\s*=\s*"?\??(?P<query>[^",]*)"?')
def mbps(bytes_transferred: int, seconds: float) -> float:
if seconds <= 0 or bytes_transferred <= 0:
return 0.0
return (bytes_transferred * 8) / seconds / 1_000_000
def throughput_from_samples(samples: list[tuple[int, float]], p: float = 90.0) -> float:
rates = [mbps(size, duration) for size, duration in samples]
rates = [rate for rate in rates if rate > 0]
if not rates:
return 0.0
return round(percentile(rates, p), 3)
def _as_int(value: str | None) -> int | None:
try:
return int(value) # type: ignore[arg-type]
except (TypeError, ValueError):
return None
def _us_to_ms(value: str | None) -> float | None:
number = _as_int(value)
return None if number is None else round(number / 1000.0, 3)
def parse_server_timing_cfl4(header: str) -> CfL4Stats | None:
match = _CFL4_RE.search(header or "")
if not match:
return None
fields = {k: v[0] for k, v in parse_qs(match.group("query"), keep_blank_values=True).items()}
return CfL4Stats(
rtt_ms=_us_to_ms(fields.get("rtt")),
min_rtt_ms=_us_to_ms(fields.get("min_rtt")),
rtt_var_ms=_us_to_ms(fields.get("rtt_var")),
delivery_rate_bps=_as_int(fields.get("delivery_rate")),
cwnd=_as_int(fields.get("cwnd")),
unsent_bytes=_as_int(fields.get("unsent_bytes")),
recv_bytes=_as_int(fields.get("recv_bytes")),
)
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:
return None
loaded = percentile(sorted(loaded_rtts_ms), 95.0)
return round(max(0.0, loaded - idle_rtt_ms), 3)

86
tests/test_speed_math.py Normal file
View file

@ -0,0 +1,86 @@
from __future__ import annotations
import pytest
from netcheck.speed import bufferbloat_delta, mbps, parse_server_timing_cfl4, throughput_from_samples
def test_mbps_converts_bytes_and_seconds():
assert mbps(1_000_000, 1.0) == pytest.approx(8.0)
assert mbps(12_500_000, 1.0) == pytest.approx(100.0)
assert mbps(25_000_000, 2.0) == pytest.approx(100.0)
def test_mbps_of_a_zero_or_negative_interval_is_zero_not_infinity():
# Zero-duration timing math is exactly where inf leaks into the JSON report.
assert mbps(1_000_000, 0.0) == 0.0
assert mbps(1_000_000, -1.0) == 0.0
def test_mbps_of_no_bytes_is_zero():
assert mbps(0, 1.5) == 0.0
def test_throughput_uses_the_ninetieth_percentile_of_the_samples():
samples = [(1_000_000, 1.0), (1_000_000, 0.5), (1_000_000, 0.4), (1_000_000, 0.25)]
# Per-sample Mbps: 8, 16, 20, 32 -> p90 interpolates to 28.4
assert throughput_from_samples(samples) == pytest.approx(28.4)
def test_throughput_of_a_single_sample_is_that_sample():
assert throughput_from_samples([(12_500_000, 1.0)]) == pytest.approx(100.0)
def test_throughput_of_no_samples_is_zero():
assert throughput_from_samples([]) == 0.0
def test_cfl4_header_is_parsed_into_typed_stats():
header = (
'cfL4;desc="?proto=tcp&rtt=12345&min_rtt=11000&rtt_var=1500&sent=100&recv=200'
'&lost=0&retrans=0&sent_bytes=1000&recv_bytes=1048576&delivery_rate=35000000'
'&cwnd=42&unsent_bytes=0&cid=abcdef&ts=1&x=0"'
)
stats = parse_server_timing_cfl4(header)
assert stats is not None
assert stats.rtt_ms == pytest.approx(12.345)
assert stats.min_rtt_ms == pytest.approx(11.0)
assert stats.rtt_var_ms == pytest.approx(1.5)
assert stats.delivery_rate_bps == 35000000
assert stats.cwnd == 42
assert stats.unsent_bytes == 0
assert stats.recv_bytes == 1048576
def test_cfl4_parsing_picks_its_entry_out_of_a_multi_metric_header():
header = 'cfRequestDuration;dur=42.1, cfL4;desc="?proto=tcp&rtt=9000&cwnd=10", cfCacheStatus;desc="HIT"'
stats = parse_server_timing_cfl4(header)
assert stats is not None
assert stats.rtt_ms == pytest.approx(9.0)
assert stats.cwnd == 10
assert stats.delivery_rate_bps is None
def test_cfl4_parsing_of_a_header_without_the_entry_is_none():
assert parse_server_timing_cfl4("cfCacheStatus;desc=HIT") is None
assert parse_server_timing_cfl4("") is None
def test_cfl4_parsing_survives_unexpected_values():
stats = parse_server_timing_cfl4('cfL4;desc="?proto=tcp&rtt=notanumber&cwnd=7"')
assert stats is not None
assert stats.rtt_ms is None
assert stats.cwnd == 7
def test_bufferbloat_delta_is_the_rise_over_the_idle_baseline():
assert bufferbloat_delta(12.0, [15.0, 60.0, 200.0, 210.0]) == pytest.approx(196.5)
def test_bufferbloat_delta_never_goes_negative():
assert bufferbloat_delta(50.0, [10.0, 12.0, 11.0]) == 0.0
def test_bufferbloat_delta_needs_both_a_baseline_and_samples():
assert bufferbloat_delta(None, [10.0]) is None
assert bufferbloat_delta(12.0, []) is None