This commit is contained in:
Andrew Pimenov 2026-10-07 07:08:52 +03:00 • committed by GitHub
commit c8c3b48361
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 44 additions and 31 deletions

View file

@ -516,6 +516,7 @@ async def _speed_section(client, settings: Settings, options: Options, idle_rtt_
import shutil import shutil
from netsleuth.speed import ( from netsleuth.speed import (
BufferbloatMeasureConfig,
measure_with_bufferbloat, measure_with_bufferbloat,
run_speed_cascade, run_speed_cascade,
tier_cloudflare, tier_cloudflare,
@ -559,12 +560,14 @@ async def _speed_section(client, settings: Settings, options: Options, idle_rtt_
result = await measure_with_bufferbloat( result = await measure_with_bufferbloat(
result, result,
config=BufferbloatMeasureConfig(
idle_rtt_ms=idle_rtt_ms if result.idle_rtt_ms is None else result.idle_rtt_ms, idle_rtt_ms=idle_rtt_ms if result.idle_rtt_ms is None else result.idle_rtt_ms,
bands=settings.thresholds.bufferbloat_ms, bands=settings.thresholds.bufferbloat_ms,
run_download=_saturate_down, run_download=_saturate_down,
run_upload=_saturate_up, run_upload=_saturate_up,
probe=_probe, probe=_probe,
interval=cfg.bufferbloat_probe_interval_seconds, interval=cfg.bufferbloat_probe_interval_seconds,
),
) )
return ModuleResult(name="speed", status="ok", data=result) return ModuleResult(name="speed", status="ok", data=result)

View file

@ -61,6 +61,7 @@ def bufferbloat_delta(idle_rtt_ms: float | None, loaded_rtts_ms: list[float]) ->
import asyncio import asyncio
import time import time
from collections.abc import Awaitable, Callable from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from netsleuth.models import SpeedResult, TierAttempt from netsleuth.models import SpeedResult, TierAttempt
@ -241,6 +242,16 @@ from netsleuth.interpret import grade_bufferbloat
from netsleuth.stats import rtt_stats 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( async def probe_while(
coro: Awaitable[None], coro: Awaitable[None],
probe: Callable[[], Awaitable[float | None]], probe: Callable[[], Awaitable[float | None]],
@ -259,25 +270,20 @@ async def probe_while(
async def measure_with_bufferbloat( async def measure_with_bufferbloat(
result: SpeedResult, result: SpeedResult,
idle_rtt_ms: float | None, config: BufferbloatMeasureConfig,
bands: BufferbloatBands,
run_download: Callable[[], Awaitable[None]],
run_upload: Callable[[], Awaitable[None]],
probe: Callable[[], Awaitable[float | None]],
interval: float,
) -> SpeedResult: ) -> SpeedResult:
# This is the one place a measurement is allowed to overlap another: the # 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. # whole point is to see what latency does while the link is saturated.
down_samples = await probe_while(run_download(), probe, interval) down_samples = await probe_while(config.run_download(), config.probe, config.interval)
up_samples = await probe_while(run_upload(), probe, interval) up_samples = await probe_while(config.run_upload(), config.probe, config.interval)
result.idle_rtt_ms = idle_rtt_ms result.idle_rtt_ms = config.idle_rtt_ms
result.loaded_rtt_down_ms = rtt_stats(list(down_samples)).avg_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.loaded_rtt_up_ms = rtt_stats(list(up_samples)).avg_ms
result.bufferbloat_down_ms = bufferbloat_delta(idle_rtt_ms, down_samples) result.bufferbloat_down_ms = bufferbloat_delta(config.idle_rtt_ms, down_samples)
result.bufferbloat_up_ms = bufferbloat_delta(idle_rtt_ms, up_samples) result.bufferbloat_up_ms = bufferbloat_delta(config.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, config.bands)
return result return result

View file

@ -14,7 +14,7 @@ from netsleuth.models import SpeedResult
from netsleuth.speed import NDT7_CONSENT_NOTICE, run_speed_cascade from netsleuth.speed import NDT7_CONSENT_NOTICE, run_speed_cascade
from netsleuth.speed import tier_ookla from netsleuth.speed import tier_ookla
from netsleuth.config import BufferbloatBands 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 import httpx
from pytest_httpx import HTTPXMock from pytest_httpx import HTTPXMock
from netsleuth.config import Speedtest 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( result = await measure_with_bufferbloat(
SpeedResult(method="cloudflare", download_mbps=300.0, upload_mbps=40.0), SpeedResult(method="cloudflare", download_mbps=300.0, upload_mbps=40.0),
config=BufferbloatMeasureConfig(
idle_rtt_ms=12.0, idle_rtt_ms=12.0,
bands=BufferbloatBands(), bands=BufferbloatBands(),
run_download=run_download, run_download=run_download,
run_upload=run_upload, run_upload=run_upload,
probe=probe, probe=probe,
interval=0.05, interval=0.05,
),
) )
assert result.idle_rtt_ms == 12.0 assert result.idle_rtt_ms == 12.0
assert result.loaded_rtt_down_ms is not None 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( result = await measure_with_bufferbloat(
SpeedResult(method="cloudflare", download_mbps=300.0), SpeedResult(method="cloudflare", download_mbps=300.0),
config=BufferbloatMeasureConfig(
idle_rtt_ms=None, idle_rtt_ms=None,
bands=BufferbloatBands(), bands=BufferbloatBands(),
run_download=work, run_download=work,
run_upload=work, run_upload=work,
probe=probe, probe=probe,
interval=0.02, interval=0.02,
),
) )
assert result.bufferbloat_down_ms is None assert result.bufferbloat_down_ms is None
assert result.bufferbloat_grade == "?" assert result.bufferbloat_grade == "?"