using MoShiro.Measurement; namespace MoShiro.Measurement.Tests; public sealed class AdaptiveRateTests { private const long Second = 1_000_000; /// 按当前频率发包、按 决定回不回,每秒评估一次;返回结束时的秒数。 private static long Run(AdaptiveRate rate, long fromSecond, int seconds, Func lost, double rttMs = 20, List? log = null, bool stopAtDead = false) { long second = fromSecond; for (int i = 0; i < seconds; i++, second++) { int probes = Math.Max(1, (int)Math.Round(rate.Hz)); if (rate.State == ProbeState.Dead) { probes = i % 4 == 0 ? 1 : 0; // 退避:几秒才发一个 } for (int k = 0; k < probes; k++) { long sent = second * Second + k * Second / probes; rate.OnSend(sent); if (!lost(second)) { rate.OnReply(sent, sent + (long)(rttMs * 1000), rttMs); } } if (rate.Evaluate((second + 1) * Second) is { } change) { log?.Add(change); if (stopAtDead && change.To == ProbeState.Dead) { return second + 1; } } } return second; } [Fact] public void QuietLine_AfterWarmUp_ProbesJustEnoughToProveTheLossBound() { var rate = new AdaptiveRate(new AdaptiveRateOptions()); long at = Run(rate, 0, 600, _ => false); Assert.Equal(ProbeState.Quiet, rate.State); Assert.Equal(5, rate.Hz, 0.01); // 3 / (0.2% × 300 s) Assert.True(at > 0); } [Fact] public void SustainedLoss_RaisesToTheMaximum_AndComesBackOnlyAfterTheHoldTimes() { var rate = new AdaptiveRate(new AdaptiveRateOptions()); long at = Run(rate, 0, 600, _ => false); at = Run(rate, at, 60, second => second % 10 == 0); // 每 10 秒丢一整秒的包 Assert.Equal(ProbeState.Active, rate.State); Assert.Equal(10, rate.Hz); at = Run(rate, at, 800, _ => false); // 不到 15 分钟:还在 Active Assert.Equal(ProbeState.Active, rate.State); at = Run(rate, at, 400, _ => false); // 超过 15 分钟:降到 Watch Assert.Equal(ProbeState.Watch, rate.State); Run(rate, at, 1900, _ => false); // 最后一次丢包 30 分钟之后:回到 Quiet Assert.Equal(ProbeState.Quiet, rate.State); } [Fact] public void AnIsolatedLoss_OnlyRaisesToWatch() { var rate = new AdaptiveRate(new AdaptiveRateOptions()); long at = Run(rate, 0, 600, _ => false); var log = new List(); Run(rate, at, 120, second => second == at + 10, log: log); Assert.Equal(ProbeState.Watch, rate.State); Assert.DoesNotContain(log, change => change.To == ProbeState.Active); } [Fact] public void Unreachable_BacksOffExponentially_AndRecoversOnTheFirstReply() { var rate = new AdaptiveRate(new AdaptiveRateOptions(), new Random(1)); long at = Run(rate, 0, 600, _ => false); at = Run(rate, at, 60, _ => true, stopAtDead: true); Assert.Equal(ProbeState.Dead, rate.State); var delays = new List(); long now = at * Second; for (int i = 0; i < 8; i++) { double delay = rate.NextDelay().TotalSeconds; delays.Add(delay); now += (long)(delay * Second); rate.OnSend(now); } Assert.InRange(delays[0], 0.8, 1.2); // 从 1 秒开始 Assert.True(delays[3] > delays[0] * 4, string.Join(",", delays)); // 翻倍增长(有抖动) Assert.All(delays, delay => Assert.InRange(delay, 0.8, 36.0)); // 最长 30 秒,抖动 ±20% rate.OnReply(now, now + 30_000, 30); Assert.Equal(ProbeState.Watch, rate.Evaluate(now + 2 * Second)!.To); } [Fact] public void QuietLineWithTightRtt_MayDropBelowTheGrid_ButAJitteryOneStaysAtTheMaximum() { var tight = new AdaptiveRate(new AdaptiveRateOptions()); Run(tight, 0, 1200, _ => false, rttMs: 20); // RTT 恒定:p99 的置信区间几乎为零,降到丢包下界 5 Hz Assert.Equal(ProbeState.Quiet, tight.State); Assert.Equal(5, tight.Hz, 0.01); var jittery = new AdaptiveRate(new AdaptiveRateOptions()); var random = new Random(7); long second = 0; for (int i = 0; i < 1200; i++, second++) { int probes = Math.Max(1, (int)Math.Round(jittery.Hz)); for (int k = 0; k < probes; k++) { long sent = second * Second + k * Second / probes; double rtt = 20 + random.NextDouble() * random.NextDouble() * 60; // 长尾 20 到 80 ms jittery.OnSend(sent); jittery.OnReply(sent, sent + (long)(rtt * 1000), rtt); } jittery.Evaluate((second + 1) * Second); } Assert.Equal(10, jittery.Hz, 0.01); // 分位数精度保护:抖动大,样本不能少 } [Fact] public void Weight_IsTheTimeAProbeStandsFor_InUnitsOfTheGrid() { var rate = new AdaptiveRate(new AdaptiveRateOptions()); Run(rate, 0, 1200, _ => false); Assert.Equal(5, rate.Hz, 0.01); rate.NextDelay(); Assert.Equal(2, rate.Weight); // 5 Hz 的一个包代表 2 个 10 Hz 网格间隔 } [Fact] public void RttWellAboveTheBaseline_RaisesToWatch() { var rate = new AdaptiveRate(new AdaptiveRateOptions()); long at = Run(rate, 0, 900, _ => false, rttMs: 20); Assert.Equal(ProbeState.Quiet, rate.State); Run(rate, at, 30, _ => false, rttMs: 120); Assert.Equal(ProbeState.Watch, rate.State); } [Fact] public void Prior_WarmsUpBeforeTheEveningPeak_AndLetsHistoricallyQuietHoursUseTheLowestRate() { const long peakStart = 1_000 * 3600; // 秒 double? Prior(long us) { long second = us / Second; return second >= peakStart && second < peakStart + 4 * 3600 ? 0.8 : 0.0; } var rate = new AdaptiveRate(new AdaptiveRateOptions { Prior = Prior }); long at = Run(rate, 100_000, 1200, _ => false); // 远离高峰,历史平稳:证明窗口拉长,2 Hz Assert.Equal(ProbeState.Quiet, rate.State); Assert.Equal(2, rate.Hz, 0.01); Run(rate, peakStart - 1500, 120, _ => false); // 距离高峰不到半小时:提前升到 Active Assert.Equal(ProbeState.Active, rate.State); Assert.True(at > 0); } }