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

@@ -33,5 +33,8 @@ public sealed class ChunkView
public double ParamNumber(string name, double fallback) =>
Header.Params.TryGetValue(name, out JsonElement v) && v.ValueKind == JsonValueKind.Number && v.TryGetDouble(out double d) ? d : fallback;
public bool ParamBool(string name, bool fallback) =>
Header.Params.TryGetValue(name, out JsonElement v) && v.ValueKind is JsonValueKind.True or JsonValueKind.False ? v.GetBoolean() : fallback;
private static long FloorDiv(long a, long b) => (a / b) - ((a % b != 0 && (a < 0) != (b < 0)) ? 1 : 0);
}
@@ -26,6 +26,7 @@ internal readonly struct EchoColumns
T2 = stream.Find("t2");
T3 = stream.Find("t3");
T4 = stream.Find("t4");
W = stream.Find("w");
}
public int Count { get; }
@@ -40,6 +41,11 @@ internal readonly struct EchoColumns
public RawColumn? T4 { get; }
/// <summary>每个包的权重(它代表多少个基础网格间隔);旧数据和固定频率的任务没有这一列,按 1 算。</summary>
public RawColumn? W { get; }
public int Weight(int i) => W is null || !W.IsValid(i) ? 1 : (int)Math.Clamp(W.Values[i], 1, 65535);
public bool HasBasics => T1 is not null && T4 is not null;
/// <summary>行 i 的发送时刻(UTC 毫秒)。</summary>
@@ -75,7 +81,7 @@ internal readonly struct EchoColumns
internal sealed class RttOp : MetricOp
{
public override string Signature => "rtt/1";
public override string Signature => "rtt/2";
public override void Evaluate(ChunkView view, RawStream stream, IObservationSink sink)
{
@@ -100,7 +106,11 @@ internal sealed class RttOp : MetricOp
if (rttUs >= 0)
{
sink.Emit(c.SentUtcMs(view, i), rttUs / 1000.0);
// 权重 k:这个样本代表 k 个网格间隔,按 k 次累计,分位数就是按时间平均的
for (int k = c.Weight(i); k > 0; k--)
{
sink.Emit(c.SentUtcMs(view, i), rttUs / 1000.0);
}
}
}
}
@@ -160,7 +170,7 @@ internal sealed class SlowShareOp : MetricOp
double threshold = Math.Max(baseline * 1.8, baseline + Math.Max(15, baseline * 0.35));
foreach ((int row, double ms) in rtts)
{
sink.Emit(c.SentUtcMs(view, row), ms >= threshold ? 1 : 0, 1);
sink.Emit(c.SentUtcMs(view, row), ms >= threshold ? c.Weight(row) : 0, c.Weight(row));
}
}
}
@@ -174,7 +184,7 @@ internal sealed class LossOp : MetricOp
_dir = dir;
}
public override string Signature => "loss/" + _dir + "/1";
public override string Signature => "loss/" + _dir + "/2";
public override void Evaluate(ChunkView view, RawStream stream, IObservationSink sink)
{
@@ -194,18 +204,18 @@ internal sealed class LossOp : MetricOp
switch (_dir)
{
case Dir.Up:
sink.Emit(c.SentUtcMs(view, i), c.IsUpLost(i) ? 1 : 0, 1);
sink.Emit(c.SentUtcMs(view, i), c.IsUpLost(i) ? c.Weight(i) : 0, c.Weight(i));
break;
case Dir.Down:
// 只统计上行已经到达 Agent 的包:回复没回来就是下行丢失。
if (!c.IsUpLost(i))
{
sink.Emit(c.SentUtcMs(view, i), c.IsReplyLost(i) ? 1 : 0, 1);
sink.Emit(c.SentUtcMs(view, i), c.IsReplyLost(i) ? c.Weight(i) : 0, c.Weight(i));
}
break;
default:
sink.Emit(c.SentUtcMs(view, i), c.IsReplyLost(i) ? 1 : 0, 1);
sink.Emit(c.SentUtcMs(view, i), c.IsReplyLost(i) ? c.Weight(i) : 0, c.Weight(i));
break;
}
}
@@ -452,7 +462,7 @@ internal sealed class GapOp : MetricOp
_thresholdMs = thresholdMs;
}
public override string Signature => $"gap/{_output}/{_rateParam}/{_thresholdMs}/1";
public override string Signature => $"gap/{_output}/{_rateParam}/{_thresholdMs}/2";
public override void Evaluate(ChunkView view, RawStream stream, IObservationSink sink)
{
@@ -462,11 +472,13 @@ internal sealed class GapOp : MetricOp
return;
}
double rate = Math.Max(0.1, view.ParamNumber(_rateParam, 1));
double expectedMs = 1000.0 / rate;
// 自适应频率时,每个包的权重就是它和上一个包的间隔(以 1/rate_max_hz 为单位),期望间隔按它算
bool adaptive = view.ParamBool("adaptive", false);
double rate = Math.Max(0.1, view.ParamNumber(adaptive ? "rate_max_hz" : _rateParam, 1));
int[] order = c.ArrivalOrder();
for (int i = 1; i < order.Length; i++)
{
double expectedMs = 1000.0 * (adaptive ? c.Weight(order[i]) : 1) / rate;
double gapMs = (c.T4!.Values[order[i]] - c.T4.Values[order[i - 1]]) / 1000.0;
if (gapMs - expectedMs <= _thresholdMs)
{