diff --git a/CHANGELOG.md b/CHANGELOG.md index b758893..265cbfe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,13 @@ The format follows [Keep a Changelog](https://keepachangelog.com/); versions fol which is usually what you meant to test. Set 0, the default, to spread it evenly as before. Profiles remember it, and the log says how often to expect a run. +- **The wireless presets now lose in runs.** Weak WiFi, Cafe, Train/metro, In-flight Wi-Fi, 3G, + Roaming and Satellite pick a run length to match the way a radio link really fails, so picking + one of them now stalls a transfer the way the real thing does instead of sprinkling single + losses. The wired ones - DSL and 56k modem - keep their loss evenly spread, because there it + is a full queue rather than a radio going away. Loss rates are unchanged, and the Wi-Fi + figures come from a published measurement. + - **A "Loss runs" counter** on the Statistics tab, in the stats CSV and in the reproduction report, says how many runs of lost packets a session actually produced. Zero there, with a run length set, means the session was too short to see one rather than the setting doing diff --git a/README.md b/README.md index 532d3d9..cb0512a 100644 --- a/README.md +++ b/README.md @@ -362,9 +362,14 @@ Terrible network - plus your own (saved under a name). The program **always star network"** (nothing is impaired until you set something). Built-in presets cannot be deleted - the "Delete" button is disabled for them. +The wireless ones also lose **in runs** rather than one packet at a time, because that is what a +radio link does: fading, interference and handovers take the channel away for a stretch. The +wired ones (DSL, 56k modem) keep their loss evenly spread, because there the loss is a full queue +rather than a radio going away. + Their numbers come from published measurements wherever measurements exist (Ookla® Speedtest® -medians for satellite and mobile, peer-reviewed studies for Starlink's 15-second reconfiguration -and for in-flight Wi-Fi). Every figure is attributed in the comment block at the top of +medians for satellite and mobile, peer-reviewed studies for Starlink's 15-second reconfiguration, +for in-flight Wi-Fi and for how many Wi-Fi packets are lost in a row). Every figure is attributed in the comment block at the top of `beantester/presets.py`, with the authors, the venue and a DOI, so you can check it rather than trust it. Where no such figure exists - "weak Wi-Fi" is not a measurable quantity - the comment next to the value says so instead of inventing a source. A few of them are worth a sentence: @@ -1039,6 +1044,7 @@ All of them loop except `upload-drop-midway.json`, so you can start one and leav | `congested-vpn.json` | A VPN whose **upload** collapses while download stays fine (512 to 160 KB/s), with latency spikes, an MTU of 1400 and occasional resets. | | `failing-dns.json` | Aimed at **UDP port 53 only**: name resolution degrades to 60% loss and 1.5 s of ping, goes **100% dead for 13 s**, then comes back. Everything else on the machine keeps working, which is what makes it a DNS test rather than an outage test. | | `overloaded-game-server.json` | A server sagging under load: ping, jitter, loss and duplication all climb together, with latency spikes up to 800 ms at 45% of packets. | +| `same-loss-in-runs.json` | The same 5% loss for the whole run, arriving four different ways: evenly spread, then in runs of 5, 15 and 40 packets. Nothing else changes, so whatever breaks is the SHAPE of the loss and not how much of it there is. The one for finding out whether your reconnect path works. | | `upload-drop-midway.json` | **Does not loop** - a one-shot: an upload that starts healthy, degrades, is **cut to zero mid-transfer** with a TCP reset, then partially recovers. For testing resumable uploads and progress bars that lie. | | `blocked-endpoint.json` | One backend (`203.0.113.0/24`) is **blocked** at 20 s while everything else keeps working, then unblocked. For testing timeouts, retries and fallbacks against a single dependency. | diff --git a/README.pl.md b/README.pl.md index 1cd03e8..c188761 100644 --- a/README.pl.md +++ b/README.pl.md @@ -277,7 +277,9 @@ w środku normalnego ruchu. **Profile** - gotowe presety **posortowane od najlepszego (góra) do najgorszego (dół)**: Idealna sieć, Dobre WiFi, Sieć 5G, DSL domowy (VDSL), Sieć LTE/4G, Satelita niskoorbitalny, Odległy serwer (inny kontynent), Słabe WiFi, Kawiarnia (zatłoczone WiFi), Zapchane łącze domowe (bufferbloat), Pociąg / metro (tunele), Sieć 3G, Roaming zagraniczny, Satelita geostacjonarny, Wi-Fi w samolocie, Modem 56k, Fatalna sieć - oraz Twoje własne (zapis pod nazwą). Program **startuje zawsze na „Idealnej sieci”** (nic nie jest psute, dopóki sam czegoś nie ustawisz). Presetów wbudowanych nie da się usunąć - przycisk „Usuń” jest wtedy nieaktywny. -Ich liczby pochodzą z opublikowanych pomiarów wszędzie tam, gdzie pomiary istnieją (mediany Ookla® Speedtest® dla satelity i sieci komórkowych, recenzowane prace naukowe o 15-sekundowym przełączaniu Starlinka i o Wi-Fi w samolocie). Każda liczba jest przypisana do źródła w komentarzu na górze `beantester/presets.py` - z autorami, miejscem publikacji i numerem DOI - żebyś mógł ją sprawdzić, a nie tylko nam uwierzyć. Tam, gdzie takiej liczby nie ma - „słabe Wi-Fi" nie jest wielkością mierzalną - komentarz przy wartości mówi to wprost, zamiast wymyślać źródło. +Te bezprzewodowe gubią też pakiety **seriami**, a nie po jednym, bo tak zachowuje się łącze radiowe: zaniki, zakłócenia i przełączenia zabierają kanał na moment. Te przewodowe (DSL, modem 56k) mają stratę rozłożoną równomiernie, bo tam bierze się ona z zapchanej kolejki, a nie ze znikającego radia. + +Ich liczby pochodzą z opublikowanych pomiarów wszędzie tam, gdzie pomiary istnieją (mediany Ookla® Speedtest® dla satelity i sieci komórkowych, recenzowane prace naukowe o 15-sekundowym przełączaniu Starlinka, o Wi-Fi w samolocie i o tym, ile pakietów Wi-Fi ginie pod rząd). Każda liczba jest przypisana do źródła w komentarzu na górze `beantester/presets.py` - z autorami, miejscem publikacji i numerem DOI - żebyś mógł ją sprawdzić, a nie tylko nam uwierzyć. Tam, gdzie takiej liczby nie ma - „słabe Wi-Fi" nie jest wielkością mierzalną - komentarz przy wartości mówi to wprost, zamiast wymyślać źródło. - **Satelita niskoorbitalny** - wzorowany na Starlinku. W stanie ustalonym jest dobry (około 40 ms pingu, 100 Mb/s). Wyróżnia go przełączanie satelity co 15 sekund, które na chwilę wstrzymuje transmisję i objawia się okazjonalnym skokiem pingu, a nie stratą pakietów. - **Odległy serwer (inny kontynent)** - szybkie łącze, w którym nic nie jest zepsute, tylko jest daleko (~120 ms pingu). To ten preset obnaża gadatliwe protokoły i kod pisany przy założeniu, że serwer stoi obok. @@ -888,6 +890,7 @@ Wszystkie chodzą w pętli poza `upload-drop-midway.json`, więc można je włą | `congested-vpn.json` | VPN, w którym pada **upload**, a pobieranie trzyma się dobrze (512 → 160 KB/s), ze skokami latencji, MTU 1400 i sporadycznymi resetami. | | `failing-dns.json` | Wycelowany **wyłącznie w UDP port 53**: rozwiązywanie nazw degraduje się do 60% strat i 1,5 s pingu, na 13 s **pada w 100%**, po czym wraca. Reszta ruchu działa normalnie - i to właśnie czyni z tego test DNS-u, a nie test awarii. | | `overloaded-game-server.json` | Serwer uginający się pod obciążeniem: ping, jitter, straty i duplikacja rosną razem, ze skokami latencji do 800 ms na 45% pakietów. | +| `same-loss-in-runs.json` | Te same 5% strat przez cały przebieg, przychodzące na cztery sposoby: równomiernie, a potem seriami po 5, 15 i 40 pakietów. Nic innego się nie zmienia, więc to, co pęknie, jest winą KSZTAŁTU straty, a nie jej wielkości. Ten do sprawdzenia, czy ścieżka ponownego łączenia działa. | | `upload-drop-midway.json` | **Bez pętli** - jednorazowy: upload zaczyna zdrowo, degraduje się, zostaje **ucięty do zera w połowie transferu** z resetem TCP, potem częściowo wraca. Do testowania wznawialnych wysyłek i pasków postępu, które kłamią. | | `blocked-endpoint.json` | Jeden backend (`203.0.113.0/24`) zostaje **zablokowany** w 20 s, reszta działa dalej, po czym blokada znika. Do testowania timeoutów, ponowień i fallbacków wobec pojedynczej zależności. | diff --git a/beantester/core.py b/beantester/core.py index 252702a..8170914 100644 --- a/beantester/core.py +++ b/beantester/core.py @@ -65,6 +65,21 @@ def burst_loss_params(loss, mean_burst): the delivered loss and the delivered mean run length both land on the request inside the sampling noise, from 0.5% upward. + On the licensing of all that, checked rather than assumed (2026-09-01), because + two of the three names above are the kind an audit stops on: + + * the MMB 2008 paper is NOT open access. What is used from it is an IDEA - that + the average run length is the intuitive way in - together with an observation + about its printed formula. Ideas and facts carry no copyright, the formula in + this function was worked out here, and no wording of theirs is reproduced. + * ``tc netem`` is named, not used. It ships under GPL-2.0 as part of iproute2, + which convention 35 forbids as a DEPENDENCY - and nothing here depends on it. + No line of it was read into this file. It is named because a reader deserves + to know the model has a widely deployed implementation to compare against. + * Gilbert 1960 and Elliott 1963 are named as the model's origin, which is + attribution rather than reproduction. "Burst-noise channel" is the model's + own term, not a quotation. + 🔴 Not every request is possible, and that is what ``achievable`` is for. ``p <= 1`` needs ``mean_burst >= loss / (1 - loss)``, so 90% loss cannot arrive in runs of 5 - runs that short leave too little room between them. diff --git a/beantester/presets.py b/beantester/presets.py index eae0b1e..42fbae9 100644 --- a/beantester/presets.py +++ b/beantester/presets.py @@ -64,12 +64,66 @@ # 200-2000 ms, "most home links affected" - are a typical range # assembled from general reporting, NOT figures from that paper. # Treat them as a shaped default, not as a measurement. +# [WIFI-BLL] da Silva and Pedroso, "Packet Loss Characterization Using Cross +# Layer Information and HMM for Wi-Fi Networks", Sensors 22(22):8592, +# 2022, doi:10.3390/s22228592. A real 802.11b/g/n network, 24 600 +# minutes of traffic: mean BURST LOSS LENGTH (consecutive packets +# lost) 3.00 in the good channel state, 3.03 and 4.66 in the two +# intermediate ones, 5.67 in the bad one, and 5.37 over the whole +# trace. 🔴 That last figure carries a standard deviation of 31.68 +# and a maximum burst of 8853, i.e. it is heavy-tailed - see the +# note on `loss_burst` below before copying it anywhere. +# [E-MODEL] ITU-T Rec. G.107, the E-model. Defines Burst Ratio as the average +# length of observed loss bursts over the length expected under +# random loss, so BurstR = 1 means independent loss and BurstR > 1 +# means bursty. The Recommendation puts the tested range of its own +# algorithm at a burst ratio of 2, and allows more than that only +# where loss stays under 2%. Restated rather than quoted: ITU +# reserves its rights in the TEXT of a Recommendation, and a +# definition and a threshold are facts rather than that text. Used +# here as a sanity anchor for what counts as ordinary burstiness, +# never as the source of a number in the table. # [3GPP-RTT] GENERAL KNOWLEDGE, deliberately not dressed as a citation: UMTS # round trips of roughly 100-200 ms, HSPA 80-150 ms, real # throughput 0.384-2 Mbit/s. Widely reported engineering ranges; no # single document is being leaned on, and inventing a specification # number to make it look sourced would be worse than saying this. # +# HOW `loss_burst` WAS CHOSEN, and why it is not the number the paper prints +# ----------------------------------------------------------------------------- +# `loss_burst` is the average number of packets lost IN A ROW (0 = the loss is +# spread evenly). It shapes `loss`, it does not add to it. +# +# 🔴 The obvious move - take [WIFI-BLL]'s overall mean of 5.37 and write it down - +# is wrong, and the reason is worth keeping. That mean comes from a heavy-tailed +# distribution (sd 31.68, longest burst 8853), so it is dragged upward by rare +# enormous bursts. Our chain is GEOMETRIC and memoryless: it cannot produce +# "mostly short, occasionally 8853", so fitting its mean to 5.37 would deliver far +# more MEDIUM bursts than the measurement ever saw. The three Wi-Fi presets +# therefore take the paper's PER-STATE means instead, which describe a channel in +# one condition rather than a mixture of all of them - good state 3.00, the worse +# intermediate 4.66, bad state 5.67. +# +# Sanity check on the other side: under [E-MODEL], a burst ratio of 1 is random +# loss and the standard is cautious above 2. So single-digit run lengths are the +# realistic range for a working link, and the run of twenty that flattens a TCP +# window is a case a tester dials in deliberately - not something a preset should +# claim a real network does. +# +# Where it is left at 0, that is a decision and not an omission: +# * `perfect`, `distant`, `bufferbloat` lose nothing, so a run length there +# would be a knob that cannot move; +# * `dsl` and `modem56k` are wired, where loss is queue overflow rather than a +# radio going away, and tail drop on one flow is close to independent; +# * `leo` is left alone on the strength of a SOURCE, which is the strongest +# reason on this list: [STAR-CON] measured the 15-second reconfiguration +# QUEUEING packets rather than dropping them, which is why this preset carries +# a latency spike and not an outage. Bursting it would contradict its own +# citation; +# * `5g` and `lte` lose 0.1-0.3%, little enough that a run length would be an +# unsourced number a tester would rarely see act. +# ----------------------------------------------------------------------------- + # 🔴 LICENSING, before somebody improves this: the figures above are FACTS with # attribution, which is exactly what is allowed - facts carry no copyright and a # handful of them is not a substantial part of any database. That stops being @@ -79,13 +133,35 @@ # Ookla, Speedtest, Starlink, HughesNet and Viasat are trademarks of their # respective owners, named here only to identify whose measurements these are. # This project is not affiliated with, endorsed by or sponsored by any of them. +# +# CHECKED AND CLEAN, so nobody has to check it twice (2026-09-01, at the source +# rather than from memory): +# * [WIFI-BLL] is open access under **CC BY 4.0**, copyright the authors +# ("© 2022 by the authors. Licensee MDPI"), verified on the article page. +# Two independent reasons it is fine here: individual figures are facts and +# carry no copyright at all, and even if they did, CC BY permits reuse with +# attribution - which is given, with authors, venue and DOI. No wording is +# reproduced, and the underlying trace is NOT ingested, which is the line +# that matters (see the Ookla note above). +# * [E-MODEL] is an ITU-T Recommendation and ITU reserves its rights in the +# TEXT. Nothing of that text is here: one definition in our own words and one +# threshold, both attributed, and it is used only as a sanity anchor rather +# than as the source of any value in the table. A definition and a number are +# not the copyrighted expression. +# * Neither belongs in `THIRD-PARTY-NOTICES.md` or `licenses/`. Those carry +# what the BUILD ships - libraries, DLLs, fonts, icons (convention 35). A +# bibliographic citation in a comment ships no third-party work, which is why +# the five older sources here have no entry either. # --------------------------------------------------------------------------- # PRESETS = { # ordered best -> worst (top = best network, bottom = worst) "presets.perfect": dict(loss=0, corrupt=0, dup=0, lat=0, jit=0, down=0, up=0), # JUDGEMENT: "good" is not a measurable quantity. Ping 30 ms to a server on # the internet, a whisper of loss, and the link is not the bottleneck. - "presets.good_wifi": dict(loss=0.1, corrupt=0, dup=0, lat=15, jit=5, down=0, up=0), + # `loss_burst` is the exception and is SOURCED: the good-channel state in + # [WIFI-BLL] loses 3.00 packets in a row on average. + "presets.good_wifi": dict(loss=0.1, corrupt=0, dup=0, lat=15, jit=5, down=0, up=0, + loss_burst=3), # 168 Mbit/s down is mid-band 5G. Upload was 8192 (67 Mbit/s), which no # market reaches: 5G upload runs 50-120% above 4G, so ~21 Mbit/s. [OOKLA25] "presets.5g": dict(loss=0.1, corrupt=0, dup=0, lat=18, jit=8, down=20480, up=2560), @@ -107,8 +183,13 @@ # throughput collapses and jitter dominates. The old 2048/1024 and 1024/384 # made "weak" and "cafe" faster than most home DSL, which is the one thing # they must not be. - "presets.weak_wifi": dict(loss=2, corrupt=0.2, dup=0.5, lat=80, jit=40, down=512, up=256), - "presets.cafe": dict(loss=3, corrupt=0.3, dup=1, lat=120, jit=90, down=256, up=96), + # The two run lengths ARE sourced: [WIFI-BLL] measures 4.66 packets in a row + # in the worse intermediate channel state and 5.67 in the bad one, so a + # degraded link takes the first and a crowded one the second. + "presets.weak_wifi": dict(loss=2, corrupt=0.2, dup=0.5, lat=80, jit=40, down=512, up=256, + loss_burst=5), + "presets.cafe": dict(loss=3, corrupt=0.3, dup=1, lat=120, jit=90, down=256, up=96, + loss_burst=6), # Idle it is a good link; under load the queue is the impairment. 2000 ms of # buffer on a 1 Mbit/s uplink is the classic "the video call dies when # somebody starts a backup" [BLOAT]. NOTE: the buffer only bites once the @@ -120,31 +201,48 @@ # cluster near stations. JUDGEMENT: the 10% duty cycle (3 s out of every 30) # is a choice, not a measurement - no study gives a canonical figure. It is # the only preset that takes the link fully down long enough to make an - # application RECONNECT rather than merely degrade. + # application RECONNECT rather than merely degrade. The run length is + # JUDGEMENT too: it describes the fading BETWEEN those handovers, which is + # why it sits a little above the bad Wi-Fi state rather than modelling the + # handover itself - the flap already does that. "presets.metro": dict(loss=2, corrupt=0, dup=0, lat=50, jit=40, down=1024, up=256, - spike_prob=5, spike_ms=400, flap_period=30, flap_down=10), + spike_prob=5, spike_ms=400, flap_period=30, flap_down=10, + loss_burst=6), # [3GPP-RTT]. The old 384/128 was UMTS R99's kbit/s pair in a KB/s field, # which made "3G" deliver 3.1 Mbit/s - HSPA+, not the experience anybody - # picks this preset to reproduce. - "presets.3g": dict(loss=1, corrupt=0, dup=0, lat=90, jit=60, down=96, up=32), + # picks this preset to reproduce. The run length is JUDGEMENT: a radio link + # fades in clusters, but no source gives a run length for UMTS. + "presets.3g": dict(loss=1, corrupt=0, dup=0, lat=90, jit=60, down=96, up=32, + loss_burst=4), # JUDGEMENT: roaming varies by operator and agreement more than by # technology. The shape is what matters - your traffic goes home first. - "presets.roaming": dict(loss=1.5, corrupt=0, dup=0, lat=200, jit=80, down=256, up=64), + # The run length is JUDGEMENT for the same reason as `3g`. + "presets.roaming": dict(loss=1.5, corrupt=0, dup=0, lat=200, jit=80, down=256, up=64, + loss_burst=4), # Geostationary: SLOW in latency, not in bandwidth - the opposite of what # "satellite" suggests. 680 ms ping and 25 Mbit/s, between what HughesNet - # delivers (8-20) and Viasat (25-60). [OOKLA25] - "presets.satellite": dict(loss=1, corrupt=0, dup=0, lat=340, jit=100, down=3072, up=384), + # delivers (8-20) and Viasat (25-60). [OOKLA25]. The run length is + # JUDGEMENT: rain fade clusters losses, but [OOKLA25] reports rates, not + # run lengths. + "presets.satellite": dict(loss=1, corrupt=0, dup=0, lat=340, jit=100, down=3072, up=384, + loss_burst=4), # [MILEHIGH]. 7% loss is the measured MEDIAN for satellite in-flight, not a # worst case. Per-user throughput is provider policy, not technology: one # operator throttled every user to 100 kbit/s. The flap models losing the - # beam. Aircraft on newer LEO kit behave like `presets.leo` instead. + # beam. Aircraft on newer LEO kit behave like `presets.leo` instead. The run + # length is JUDGEMENT: [MILEHIGH] measures the loss RATE, not how it arrives, + # and this is the worst-behaved link in the table that still claims to be a + # real one. "presets.inflight": dict(loss=7, corrupt=0, dup=0, lat=375, jit=150, down=64, up=24, - flap_period=60, flap_down=3), + flap_period=60, flap_down=3, loss_burst=8), # V.90 in practice: ~41 kbit/s down, ~33 up. These two were always right. "presets.modem56k": dict(loss=0.5, corrupt=0, dup=0, lat=100, jit=30, down=5, up=4), # Not a real network and not meant to be one: the everything-at-once case. - # Its old 256/128 made the WORST preset faster than the 3G one. - "presets.terrible": dict(loss=10, corrupt=2, dup=2, lat=300, jit=150, down=32, up=16), + # Its old 256/128 made the WORST preset faster than the 3G one. The run + # length needs no source for the same reason the rest of this row does not: + # it is meant to be fatal, not realistic. + "presets.terrible": dict(loss=10, corrupt=2, dup=2, lat=300, jit=150, down=32, up=16, + loss_burst=15), } diff --git a/scenarios/cafe-wifi.json b/scenarios/cafe-wifi.json index 5a27347..809462d 100644 --- a/scenarios/cafe-wifi.json +++ b/scenarios/cafe-wifi.json @@ -1,10 +1,10 @@ { "loop": true, "steps": [ - { "at": 0, "settings": { "latency": 20, "jitter": 15, "loss": 1, "down": 1024, "up": 256 } }, - { "at": 20, "settings": { "latency": 45, "jitter": 45, "loss": 3, "down": 512, "up": 160 } }, - { "at": 40, "settings": { "latency": 120, "jitter": 70, "loss": 6, "down": 256, "up": 96, "flap_period": 12, "flap_down": 20 } }, - { "at": 65, "settings": { "latency": 35, "jitter": 30, "loss": 2, "down": 512, "up": 160, "flap_period": 0 } }, - { "at": 85, "settings": { "latency": 20, "jitter": 15, "loss": 1, "down": 1024, "up": 256 } } + { "at": 0, "settings": { "latency": 20, "jitter": 15, "loss": 1, "loss_burst": 3, "down": 1024, "up": 256 } }, + { "at": 20, "settings": { "latency": 45, "jitter": 45, "loss": 3, "loss_burst": 5, "down": 512, "up": 160 } }, + { "at": 40, "settings": { "latency": 120, "jitter": 70, "loss": 6, "loss_burst": 6, "down": 256, "up": 96, "flap_period": 12, "flap_down": 20 } }, + { "at": 65, "settings": { "latency": 35, "jitter": 30, "loss": 2, "loss_burst": 5, "down": 512, "up": 160, "flap_period": 0 } }, + { "at": 85, "settings": { "latency": 20, "jitter": 15, "loss": 1, "loss_burst": 3, "down": 1024, "up": 256 } } ] } diff --git a/scenarios/mobile-lte-to-3g.json b/scenarios/mobile-lte-to-3g.json index f77fb98..7fa8ef4 100644 --- a/scenarios/mobile-lte-to-3g.json +++ b/scenarios/mobile-lte-to-3g.json @@ -2,11 +2,11 @@ "loop": true, "steps": [ { "at": 0, "settings": { "latency": 30, "jitter": 20, "loss": 0, "down": 4096, "up": 1280 } }, - { "at": 25, "settings": { "latency": 60, "jitter": 60, "loss": 2, "down": 512, "up": 128 } }, - { "at": 45, "settings": { "latency": 120, "jitter": 120, "loss": 8, "down": 96, "up": 32 } }, + { "at": 25, "settings": { "latency": 60, "jitter": 60, "loss": 2, "loss_burst": 4, "down": 512, "up": 128 } }, + { "at": 45, "settings": { "latency": 120, "jitter": 120, "loss": 8, "loss_burst": 8, "down": 96, "up": 32 } }, { "at": 60, "action": "reset_tcp" }, { "at": 60, "settings": { "loss": 100 } }, - { "at": 68, "settings": { "loss": 6, "latency": 100, "jitter": 100, "down": 160, "up": 48 } }, + { "at": 68, "settings": { "loss": 6, "loss_burst": 6, "latency": 100, "jitter": 100, "down": 160, "up": 48 } }, { "at": 85, "settings": { "latency": 30, "jitter": 20, "loss": 0, "down": 4096, "up": 1280 } } ] } diff --git a/scenarios/same-loss-in-runs.json b/scenarios/same-loss-in-runs.json new file mode 100644 index 0000000..d33d460 --- /dev/null +++ b/scenarios/same-loss-in-runs.json @@ -0,0 +1,10 @@ +{ + "loop": true, + "steps": [ + { "at": 0, "settings": { "latency": 30, "jitter": 10, "loss": 5, "loss_burst": 0, "down": 1024, "up": 256 } }, + { "at": 25, "settings": { "loss": 5, "loss_burst": 5 } }, + { "at": 50, "settings": { "loss": 5, "loss_burst": 15 } }, + { "at": 75, "settings": { "loss": 5, "loss_burst": 40 } }, + { "at": 100, "settings": { "loss": 5, "loss_burst": 0 } } + ] +} diff --git a/tests/test_presets_filters.py b/tests/test_presets_filters.py index 9d99296..5311c93 100644 --- a/tests/test_presets_filters.py +++ b/tests/test_presets_filters.py @@ -77,6 +77,45 @@ def test_preset_order_best_to_worst(): check("presets: perfect before terrible", idx["presets.perfect"] < idx["presets.terrible"]) +def test_no_preset_sets_a_run_length_it_cannot_use(): + """A run length shapes the loss, so without loss it is a knob that cannot move. + + Silent in every other way: the preset would look configured, the summary + would say nothing, and the field would sit there describing a link the + program never produces. It is the same class as a `spike_ms` with no + `spike_prob`, and cheap enough to make mechanical. + """ + from beantester.presets import PRESETS, preset_to_settings + for key in PRESETS: + settings = preset_to_settings(key) + if settings["loss_burst"]: + check(f"{key} sets a run length and has loss for it to shape", + settings["loss"] > 0, + f"(run={settings['loss_burst']}, loss={settings['loss']})") + + +def test_the_wireless_presets_lose_in_runs_and_the_wired_ones_do_not(): + """The mechanism split, pinned so a later tidy-up cannot quietly erase it. + + Loss on a radio link comes from fading, interference and handovers, which + take the channel away for a stretch. Loss on a wired link is queue overflow, + and tail drop on one flow is close to independent. That distinction is the + whole reason only some of these presets carry a run length, and nothing else + in the tree records it. + """ + from beantester.presets import preset_to_settings + for key in ("presets.weak_wifi", "presets.cafe", "presets.metro", + "presets.inflight", "presets.3g", "presets.roaming", + "presets.satellite"): + check(f"{key} loses in runs", preset_to_settings(key)["loss_burst"] > 0, + f"(run={preset_to_settings(key)['loss_burst']})") + for key in ("presets.dsl", "presets.modem56k", "presets.bufferbloat", + "presets.distant", "presets.perfect"): + check(f"{key} keeps its loss independent", + preset_to_settings(key)["loss_burst"] == 0, + f"(run={preset_to_settings(key)['loss_burst']})") + + def test_resolve_preset_variants(): import beantester as n check("presets: canonical id resolves", n.resolve_preset("presets.3g") == "presets.3g") diff --git a/tools/build_site.py b/tools/build_site.py index 9cf03b0..bc89f8f 100644 --- a/tools/build_site.py +++ b/tools/build_site.py @@ -765,12 +765,19 @@ def preset_table(app, texts): program. Column heads come from the program's own field labels, so they match the window a reader is looking at. """ - heads = [texts["table.preset"], app["app.fields.loss"], app["app.fields.latency"], + heads = [texts["table.preset"], app["app.fields.loss"], app["app.fields.loss_burst"], + app["app.fields.latency"], app["app.fields.jitter"], app["app.fields.download"], app["app.fields.upload"]] rows = ["%s" % "".join("%s" % html.escape(h, quote=True) for h in heads)] for key, values in presets.PRESETS.items(): + # The run length sits next to the loss it shapes, and it is here rather + # than left out with the other advanced fields for one reason: without it + # the loss column means two different things from row to row, and a reader + # comparing "2%" against "2%" has no way to see that one of them arrives + # in runs. A column that changes meaning silently is worse than a wider table. cells = [html.escape(app["app." + key], quote=True), - _cell(values.get("loss"), "%"), _cell(values.get("lat"), "ms"), + _cell(values.get("loss"), "%"), _cell(values.get("loss_burst")), + _cell(values.get("lat"), "ms"), _cell(values.get("jit"), "ms"), _cell(values.get("down"), "KB/s"), _cell(values.get("up"), "KB/s")] rows.append("%s" % "".join("%s" % c for c in cells))