measurement: adapt the echo probe rate to loss and RTT, and weight samples by the time they stand for

udp_echo and tcp_echo run at the lowest rate that still proves the loss bound and keeps the RTT p99 confidence interval within tolerance, go to the maximum when the line loses packets or its RTT rises, and back off exponentially when it is unreachable. Every echo row carries a weight (the time it stands for, in grid intervals) and the analysis ops accumulate loss and RTT by it, so p50/p95/p99 and loss stay time-averaged when the rate changes. State changes are logged for audit.
This commit is contained in:
justme committed 2026-10-22 05:28:28 +08:00
1 parent 57acecdb86
commit e0376b5e33
15 files changed
+858 -55

No files matched your search

@@ -1,4 +1,5 @@
using System.Net;
using Fleet.Core;
using MoShiro.Protocol;
namespace MoShiro.Measurement;
@@ -41,6 +42,76 @@ public abstract class BlockBase : IBlock
Env.Fragments!.Ship(Spec.KindId, Spec.Scope, streams, parameters ?? Spec.Params.Values, Env.Clock.UtcMsAt(first), (int)((last - first) / 1000), Spec.Port);
}
/// <summary>
/// 按自适应的节奏发包:下一个包的时刻按绝对时间排(不累积误差),每秒评估一次状态,状态变化记进日志(供审计)。
/// 落后了就从现在重新排,不补发。
/// </summary>
protected async Task PaceAsync(AdaptiveRate rate, Func<ValueTask> sendOne, CancellationToken ct)
{
long due = Env.Clock.NowUs();
long lastEvaluation = due;
while (!ct.IsCancellationRequested)
{
due += (long)(rate.NextDelay().TotalMilliseconds * 1000);
long wait = due - Env.Clock.NowUs();
if (wait > 0)
{
await Task.Delay(TimeSpan.FromMicroseconds(wait), Env.Time, ct);
}
else
{
due = Env.Clock.NowUs();
}
await sendOne();
long now = Env.Clock.NowUs();
if (now - lastEvaluation >= 1_000_000 || rate.State == ProbeState.Dead)
{
lastEvaluation = now;
if (rate.Evaluate(now) is { } change)
{
LogTransition(change);
}
}
}
}
/// <summary>读这个任务的自适应频率参数(<c>udp_echo</c> 和 <c>tcp_echo</c> 共用)。</summary>
protected AdaptiveRate? CreateAdaptiveRate()
{
if (!Spec.Bool("adaptive"))
{
return null;
}
double max = Math.Max(Spec.Int("rate_max_hz"), Spec.Int("rate_min_hz"));
return new AdaptiveRate(new AdaptiveRateOptions
{
MinHz = Spec.Int("rate_min_hz"),
MaxHz = max,
TauPct = Spec.Number("tau_pct"),
RttTolPct = Spec.Number("rtt_tol_pct"),
Prior = Env.LossPrior,
});
}
private void LogTransition(ProbeTransition change)
{
var fields = new (string, object?)[]
{
("stream", Spec.StreamId), ("kind", Spec.KindId), ("from", change.From.ToString().ToLowerInvariant()), ("to", change.To.ToString().ToLowerInvariant()), ("hz", Math.Round(change.Hz, 1)),
("reason", change.Reason), ("loss_pct", Math.Round(change.LossPct, 3)), ("sent", change.Sent), ("lost", change.Lost),
};
if (change.To == ProbeState.Dead)
{
Env.Log.Warn("probe rate changed", fields);
}
else
{
Env.Log.Info("probe rate changed", fields);
}
}
/// <summary>什么也不做、一直等到取消(监听方里不需要自己运行逻辑的积木)。</summary>
protected async Task WaitUntilCancelledAsync(CancellationToken ct)
{