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);
}
}