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.
177 lines
7.9 KiB
C#
177 lines
7.9 KiB
C#
namespace MoShiro.Protocol;
|
||
|
||
/// <summary>
|
||
/// Kind 目录。新增一种采集方法 = 在这里加一个静态字段并放进 <see cref="All"/>;
|
||
/// 参数一律用 <see cref="P"/> 声明默认值和上下限。
|
||
/// </summary>
|
||
public static class Kinds
|
||
{
|
||
private static readonly string[] s_directions = ["fwd", "rev", "bidir"];
|
||
|
||
/// <summary>这个任务可能达到的最高探测频率(Hz):自适应时是 <c>rate_max_hz</c>,否则是固定的 <c>rate_hz</c>。预算、限速和准入都按它算。</summary>
|
||
public static long PeakRateHz(string kindId, ParamValues values) =>
|
||
(kindId is "udp_echo" or "tcp_echo") && values.GetBool("adaptive") ? Math.Max(values.GetInt("rate_max_hz"), values.GetInt("rate_min_hz")) : values.GetInt("rate_hz");
|
||
|
||
private static readonly StreamSpec s_echoMsg = new(
|
||
"msg",
|
||
[
|
||
Col.Seq("seq"), Col.U16("size"), Col.U16("w"),
|
||
Col.TsDialer("t1"), Col.TsListener("t2", nullable: true), Col.TsListener("t3", nullable: true), Col.TsDialer("t4", nullable: true),
|
||
],
|
||
Side.Dialer,
|
||
"seq");
|
||
|
||
private static readonly StreamSpec s_tcpInfo = new(
|
||
"tcp_info",
|
||
[
|
||
Col.TsLocal("ts"), Col.U8("side"), Col.U32("srtt_us"), Col.U32("rttvar_us"), Col.U32("cwnd"),
|
||
Col.U32("retrans_total"), Col.U32("segs_out"), Col.U64("bytes_acked"),
|
||
new ColumnSpec("conn", ColumnType.U64, ColumnEnc.Plain, true),
|
||
],
|
||
Union: true);
|
||
|
||
public static readonly KindSpec UdpEcho = new(
|
||
"udp_echo",
|
||
"QUIC Datagram echo",
|
||
"Fixed-rate QUIC Datagram echo with four timestamps per packet.",
|
||
Scope.Line,
|
||
new BlockSet("quic_datagram", "fixed", "fixed", "echo", []),
|
||
[
|
||
P.Bool("adaptive", true, "Adapt the probe rate to the loss rate: low when the line is quiet, up to rate_max_hz when it loses packets, exponential back-off when it is unreachable."),
|
||
P.Int("rate_hz", 10, 1, 200, "Hz", "Fixed rate, used when adaptive is false."),
|
||
P.Int("rate_min_hz", 2, 1, 200, "Hz"),
|
||
P.Int("rate_max_hz", 10, 1, 200, "Hz", "Highest rate, and the reference grid for sample weights."),
|
||
P.Number("tau_pct", 0.2, 0.01, 5, "%", "Loss rate a quiet line has to be able to prove below, at 95% confidence."),
|
||
P.Number("rtt_tol_pct", 2, 0.1, 20, "%", "Half-width of the 95% confidence interval of the RTT p99 a quiet line has to keep, as a share of p99 (never below 0.3 ms)."),
|
||
P.Int("size", 64, 44, 1400, "B"),
|
||
P.Int("port", 7443, 1, 65535),
|
||
],
|
||
[s_echoMsg],
|
||
p => Bandwidth.UdpEcho(PeakRateHz("udp_echo", p), p.GetInt("size")));
|
||
|
||
public static readonly KindSpec UdpTrain = new(
|
||
"udp_train",
|
||
"QUIC Datagram train",
|
||
"Fixed-rate or laddered one-way QUIC Datagram stream (Agent to 拨号方) for loss and delay variation under load.",
|
||
Scope.Line,
|
||
new BlockSet("quic_datagram", "ladder", "fixed", "sink", []),
|
||
[
|
||
P.Number("mbps", 10, 0.1, 1000, "Mbit/s"),
|
||
P.Int("size", 1250, 200, 1400, "B"),
|
||
P.Int("duration_s", 10, 1, 120, "s"),
|
||
P.Enum("direction", "rev", s_directions),
|
||
P.Int("port", 7443, 1, 65535),
|
||
],
|
||
[
|
||
new StreamSpec("pkt", [Col.Seq("seq"), Col.U16("size"), Col.TsListener("t_send"), Col.TsDialer("t_recv", nullable: true)], Side.Listener, "seq"),
|
||
new StreamSpec("step", [Col.U16("index"), Col.U32("mbps"), Col.TsDialer("start_ts"), Col.TsDialer("end_ts")]),
|
||
],
|
||
p => (long)(p.GetNumber("mbps") * 1_000_000));
|
||
|
||
public static readonly KindSpec TcpEcho = new(
|
||
"tcp_echo",
|
||
"TCP echo",
|
||
"Small TCP messages echoed with four timestamps, plus periodic kernel tcp_info samples.",
|
||
Scope.Line,
|
||
new BlockSet("tcp", "fixed", "uniform", "echo", ["tcp_info"]),
|
||
[
|
||
P.Bool("adaptive", true, "Adapt the echo rate: as low as the loss bound and the RTT percentile precision allow when the line is quiet, the maximum when it is not, back-off when it stalls."),
|
||
P.Int("rate_hz", 20, 1, 200, "Hz", "Fixed rate, used when adaptive is false."),
|
||
P.Int("rate_min_hz", 2, 1, 200, "Hz"),
|
||
P.Int("rate_max_hz", 20, 1, 200, "Hz", "Highest rate, and the reference grid for sample weights."),
|
||
P.Number("tau_pct", 0.2, 0.01, 5, "%", "Share of retransmitted segments a quiet line has to be able to prove below, at 95% confidence."),
|
||
P.Number("rtt_tol_pct", 2, 0.1, 20, "%", "Half-width of the 95% confidence interval of the RTT p99 a quiet line has to keep, as a share of p99 (never below 0.3 ms)."),
|
||
P.Int("min", 32, 16, 1400, "B"),
|
||
P.Int("max", 64, 16, 1400, "B"),
|
||
P.Int("tcp_info_ms", 1000, 10, 1000, "ms"),
|
||
P.Int("port", 7444, 1, 65535),
|
||
P.Bool("tls", false),
|
||
],
|
||
[s_echoMsg, s_tcpInfo],
|
||
p => Bandwidth.TcpEcho(PeakRateHz("tcp_echo", p), p.GetInt("max")));
|
||
|
||
public static readonly KindSpec TcpBulk = new(
|
||
"tcp_bulk",
|
||
"TCP bulk",
|
||
"Bulk TCP transfer on several streams with periodic byte counters and tcp_info.",
|
||
Scope.Line,
|
||
new BlockSet("tcp", "fixed", "fixed", "source", ["tcp_info"]),
|
||
[
|
||
P.Int("streams", 4, 1, 16),
|
||
P.Int("duration_s", 4, 1, 120, "s"),
|
||
P.Enum("direction", "rev", s_directions),
|
||
P.Int("sample_ms", 100, 20, 1000, "ms"),
|
||
P.Int("port", 7444, 1, 65535),
|
||
P.Bool("tls", true),
|
||
],
|
||
[
|
||
new StreamSpec("bytes", [Col.TsDialer("ts"), Col.U8("conn"), Col.U64("bytes")]),
|
||
s_tcpInfo,
|
||
],
|
||
_ => 0);
|
||
|
||
public static readonly KindSpec Connect = new(
|
||
"connect",
|
||
"Connect",
|
||
"Repeated TCP and TLS connection establishment with per-phase timings and outcome.",
|
||
Scope.Line | Scope.Target,
|
||
new BlockSet("tcp", "fixed", "fixed", "connect", []),
|
||
[
|
||
P.Int("per_minute", 6, 1, 60),
|
||
P.Bool("tls", true),
|
||
P.Int("timeout_ms", 5000, 500, 30000, "ms"),
|
||
P.Int("port", 7444, 1, 65535),
|
||
],
|
||
[
|
||
new StreamSpec(
|
||
"conn",
|
||
[Col.Seq("attempt"), Col.TsDialer("ts"), Col.U32("tcp_us", nullable: true), Col.U32("tls_us", nullable: true), Col.U8("outcome")]),
|
||
],
|
||
p => p.GetInt("per_minute") * 4000 / 60);
|
||
|
||
public static readonly KindSpec Hold = new(
|
||
"hold",
|
||
"Hold",
|
||
"A long-lived TLS connection with periodic heartbeats; records resets and lifetime.",
|
||
Scope.Line,
|
||
new BlockSet("tls", "fixed", "fixed", "hold", []),
|
||
[
|
||
P.Int("heartbeat_s", 30, 5, 300, "s"),
|
||
P.Int("port", 7444, 1, 65535),
|
||
],
|
||
[
|
||
new StreamSpec("hold", [Col.TsDialer("ts"), Col.U8("event"), Col.U32("lifetime_ms")]),
|
||
],
|
||
p => 2000 / Math.Max(1, p.GetInt("heartbeat_s")) * 8);
|
||
|
||
public static readonly KindSpec IcmpEcho = new(
|
||
"icmp_echo",
|
||
"ICMP echo",
|
||
"拨号方 or Agent side ICMP echo to a target address.",
|
||
Scope.Target,
|
||
new BlockSet("icmp", "fixed", "fixed", "echo", []),
|
||
[
|
||
P.Int("rate_hz", 1, 1, 20, "Hz"),
|
||
P.Int("size", 64, 44, 1400, "B"),
|
||
],
|
||
[
|
||
new StreamSpec("msg", [Col.Seq("seq"), Col.U16("size"), Col.TsLocal("t1"), new ColumnSpec("t4", ColumnType.I64, ColumnEnc.Delta, true, ColumnRole.TsLocal)]),
|
||
],
|
||
p => Bandwidth.UdpEcho(p.GetInt("rate_hz"), p.GetInt("size")));
|
||
|
||
public static readonly IReadOnlyList<KindSpec> All = [UdpEcho, UdpTrain, TcpEcho, TcpBulk, Connect, Hold, IcmpEcho];
|
||
|
||
public static KindSpec? Find(string id)
|
||
{
|
||
foreach (KindSpec kind in All)
|
||
{
|
||
if (kind.Id == id)
|
||
{
|
||
return kind;
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
}
|