diff --git a/src/netsleuth/cli.py b/src/netsleuth/cli.py index 77ef25c..327d63c 100644 --- a/src/netsleuth/cli.py +++ b/src/netsleuth/cli.py @@ -516,6 +516,7 @@ async def _speed_section(client, settings: Settings, options: Options, idle_rtt_ import shutil from netsleuth.speed import ( + BufferbloatMeasureConfig, measure_with_bufferbloat, run_speed_cascade, tier_cloudflare, @@ -559,12 +560,14 @@ async def _speed_section(client, settings: Settings, options: Options, idle_rtt_ result = await measure_with_bufferbloat( result, - idle_rtt_ms=idle_rtt_ms if result.idle_rtt_ms is None else result.idle_rtt_ms, - bands=settings.thresholds.bufferbloat_ms, - run_download=_saturate_down, - run_upload=_saturate_up, - probe=_probe, - interval=cfg.bufferbloat_probe_interval_seconds, + config=BufferbloatMeasureConfig( + idle_rtt_ms=idle_rtt_ms if result.idle_rtt_ms is None else result.idle_rtt_ms, + bands=settings.thresholds.bufferbloat_ms, + run_download=_saturate_down, + run_upload=_saturate_up, + probe=_probe, + interval=cfg.bufferbloat_probe_interval_seconds, + ), ) return ModuleResult(name="speed", status="ok", data=result) diff --git a/src/netsleuth/speed.py b/src/netsleuth/speed.py index 505e1f0..0b1cbfe 100644 --- a/src/netsleuth/speed.py +++ b/src/netsleuth/speed.py @@ -61,6 +61,7 @@ def bufferbloat_delta(idle_rtt_ms: float | None, loaded_rtts_ms: list[float]) -> import asyncio import time from collections.abc import Awaitable, Callable +from dataclasses import dataclass from netsleuth.models import SpeedResult, TierAttempt @@ -241,6 +242,16 @@ from netsleuth.interpret import grade_bufferbloat from netsleuth.stats import rtt_stats +@dataclass(frozen=True) +class BufferbloatMeasureConfig: + idle_rtt_ms: float | None + bands: BufferbloatBands + run_download: Callable[[], Awaitable[None]] + run_upload: Callable[[], Awaitable[None]] + probe: Callable[[], Awaitable[float | None]] + interval: float + + async def probe_while( coro: Awaitable[None], probe: Callable[[], Awaitable[float | None]], @@ -259,25 +270,20 @@ async def probe_while( async def measure_with_bufferbloat( result: SpeedResult, - idle_rtt_ms: float | None, - bands: BufferbloatBands, - run_download: Callable[[], Awaitable[None]], - run_upload: Callable[[], Awaitable[None]], - probe: Callable[[], Awaitable[float | None]], - interval: float, + config: BufferbloatMeasureConfig, ) -> SpeedResult: # This is the one place a measurement is allowed to overlap another: the # whole point is to see what latency does while the link is saturated. - down_samples = await probe_while(run_download(), probe, interval) - up_samples = await probe_while(run_upload(), probe, interval) - result.idle_rtt_ms = idle_rtt_ms + down_samples = await probe_while(config.run_download(), config.probe, config.interval) + up_samples = await probe_while(config.run_upload(), config.probe, config.interval) + result.idle_rtt_ms = config.idle_rtt_ms result.loaded_rtt_down_ms = rtt_stats(list(down_samples)).avg_ms result.loaded_rtt_up_ms = rtt_stats(list(up_samples)).avg_ms - result.bufferbloat_down_ms = bufferbloat_delta(idle_rtt_ms, down_samples) - result.bufferbloat_up_ms = bufferbloat_delta(idle_rtt_ms, up_samples) + result.bufferbloat_down_ms = bufferbloat_delta(config.idle_rtt_ms, down_samples) + result.bufferbloat_up_ms = bufferbloat_delta(config.idle_rtt_ms, up_samples) worst_delta = max( [d for d in (result.bufferbloat_down_ms, result.bufferbloat_up_ms) if d is not None], default=None, ) - result.bufferbloat_grade = grade_bufferbloat(worst_delta, bands) + result.bufferbloat_grade = grade_bufferbloat(worst_delta, config.bands) return result diff --git a/tests/test_speed.py b/tests/test_speed.py index 3722359..019bafd 100644 --- a/tests/test_speed.py +++ b/tests/test_speed.py @@ -14,7 +14,7 @@ from netsleuth.models import SpeedResult from netsleuth.speed import NDT7_CONSENT_NOTICE, run_speed_cascade from netsleuth.speed import tier_ookla from netsleuth.config import BufferbloatBands -from netsleuth.speed import measure_with_bufferbloat, probe_while +from netsleuth.speed import BufferbloatMeasureConfig, measure_with_bufferbloat, probe_while import httpx from pytest_httpx import HTTPXMock from netsleuth.config import Speedtest @@ -371,12 +371,14 @@ async def test_measure_with_bufferbloat_fills_in_both_directions_and_the_grade() result = await measure_with_bufferbloat( SpeedResult(method="cloudflare", download_mbps=300.0, upload_mbps=40.0), - idle_rtt_ms=12.0, - bands=BufferbloatBands(), - run_download=run_download, - run_upload=run_upload, - probe=probe, - interval=0.05, + config=BufferbloatMeasureConfig( + idle_rtt_ms=12.0, + bands=BufferbloatBands(), + run_download=run_download, + run_upload=run_upload, + probe=probe, + interval=0.05, + ), ) assert result.idle_rtt_ms == 12.0 assert result.loaded_rtt_down_ms is not None @@ -396,12 +398,14 @@ async def test_measure_with_bufferbloat_without_an_idle_baseline_grades_unknown( result = await measure_with_bufferbloat( SpeedResult(method="cloudflare", download_mbps=300.0), - idle_rtt_ms=None, - bands=BufferbloatBands(), - run_download=work, - run_upload=work, - probe=probe, - interval=0.02, + config=BufferbloatMeasureConfig( + idle_rtt_ms=None, + bands=BufferbloatBands(), + run_download=work, + run_upload=work, + probe=probe, + interval=0.02, + ), ) assert result.bufferbloat_down_ms is None assert result.bufferbloat_grade == "?"