namespace Monitor.Backend.Analysis; /// 线路基础质量:RTT、丢包、抖动、连续丢包。 public sealed class BaseModel : ServiceModelBase { private static readonly Curve s_rtt = new((30, 100), (80, 80), (150, 50), (300, 10), (500, 0)); private static readonly Curve s_loss = new((0, 100), (0.5, 90), (1, 75), (3, 40), (10, 0)); private static readonly Curve s_jitter = new((2, 100), (8, 85), (20, 50), (80, 0)); private static readonly Curve s_run = new((1, 100), (3, 80), (10, 30), (30, 0)); public override string Id => "base"; public override string Title => "Line quality"; public override int Version => 1; public override IReadOnlyList Needs { get; } = ["rtt.p95", "loss.total", "jitter.down.p95", "jitter.up.p95", "loss.run_max"]; public override ModelScore Score(MetricView view) => Combine( view, [ Dim(view, "rtt.p95", "RTT p95", 3, s_rtt), Dim(view, "loss.total", "Packet loss", 4, s_loss), Dim(view, "jitter.down.p95", "Jitter (down)", 1.5, s_jitter), Dim(view, "jitter.up.p95", "Jitter (up)", 1.5, s_jitter), Dim(view, "loss.run_max", "Longest loss run", 1, s_run), ], estimated: false); } /// /// Minecraft:TCP 上不会丢包,丢包表现为重传和卡顿(队头阻塞)。 /// 没有 tcp_echo 数据时,用 udp_echo 的丢包与 RTT 代入简化的 TCP 恢复模型估算,并标记为 estimated。 /// public sealed class McModel : ServiceModelBase { private static readonly Curve s_rtt = new((40, 100), (100, 80), (200, 45), (400, 5), (600, 0)); private static readonly Curve s_stallRate = new((0, 100), (1, 90), (5, 60), (20, 20), (60, 0)); private static readonly Curve s_stallMax = new((150, 100), (500, 60), (2000, 10), (5000, 0)); private static readonly Curve s_retrans = new((0.1, 100), (1, 80), (3, 50), (10, 0)); public override string Id => "mc"; public override string Title => "Minecraft"; public override int Version => 1; public override IReadOnlyList Needs { get; } = ["tcp.rtt.p95", "tcp.stall.per_min", "tcp.stall_max", "tcp.retrans"]; public override ModelScore Score(MetricView view) { bool hasTcp = view.Has("tcp.rtt.p95") || view.Has("tcp.stall.per_min"); if (hasTcp) { return Combine( view, [ Dim(view, "tcp.rtt.p95", "TCP RTT p95", 3, s_rtt), Dim(view, "tcp.stall.per_min", "Stalls per minute", 3, s_stallRate), Dim(view, "tcp.stall_max", "Longest stall", 2, s_stallMax), Dim(view, "tcp.retrans", "Retransmissions", 2, s_retrans), ], estimated: false); } if (view.Get("loss.total") is not { } lossPct || view.Get("rtt.p95") is not { } rttMs) { return Combine(view, [], estimated: true); } // 一个丢失的包至少造成一次约 max(200 ms, 2·RTT) 的卡顿;约一半能被快速重传恢复。 const double messagesPerMinute = 1200; double stallsPerMin = lossPct / 100 * messagesPerMinute * 0.5; double stallMs = lossPct > 0 ? Math.Max(200, 2 * rttMs) * (1 + (view.Get("loss.run_max") ?? 1) / 3) : 0; var estimate = new MetricView(new Dictionary { ["tcp.rtt.p95"] = rttMs + (lossPct > 0 ? Math.Max(200, 2 * rttMs) * lossPct / 100 * 5 : 0), ["tcp.stall.per_min"] = stallsPerMin, ["tcp.stall_max"] = stallMs, ["tcp.retrans"] = lossPct, }) { MeasuredOnServicePort = view.MeasuredOnServicePort, }; return Combine( estimate, [ Dim(estimate, "tcp.rtt.p95", "TCP RTT p95 (estimated)", 3, s_rtt), Dim(estimate, "tcp.stall.per_min", "Stalls per minute (estimated)", 3, s_stallRate), Dim(estimate, "tcp.stall_max", "Longest stall (estimated)", 2, s_stallMax), Dim(estimate, "tcp.retrans", "Retransmissions (estimated)", 2, s_retrans), ], estimated: true, "estimated_from_udp"); } } /// /// Hysteria2(Brutal 固定码率):有 hy2_ladder 测试结果时,用可持续码率、有效吞吐和限速开始的位置评分; /// 没有测试时,只能用持续的 udp_echo 趋势粗略估算,并标记为 estimated。 /// public sealed class Hy2Model : ServiceModelBase { private static readonly Curve s_sustainable = new((0, 0), (10, 30), (25, 60), (50, 80), (100, 100)); private static readonly Curve s_qosOnset = new((10, 20), (25, 50), (50, 75), (100, 95)); private static readonly Curve s_loss = new((0, 100), (1, 85), (3, 60), (8, 20), (15, 0)); private static readonly Curve s_jitter = new((3, 100), (10, 80), (30, 40), (100, 0)); private static readonly Curve s_rtt = new((40, 100), (120, 80), (250, 40), (500, 0)); public override string Id => "hy2"; public override string Title => "Hysteria2"; public override int Version => 1; public override IReadOnlyList Needs { get; } = ["hy2.sustainable_mbps", "hy2.effective_mbps", "hy2.qos_onset_mbps", "loss.total", "jitter.down.p95", "rtt.p95"]; public override ModelScore Score(MetricView view) { var dimensions = new List(); bool hasTest = view.Has("hy2.sustainable_mbps"); if (hasTest) { dimensions.Add(Dim(view, "hy2.sustainable_mbps", "Sustainable rate (loss ≤ 5%)", 4, s_sustainable)); dimensions.Add(Dim(view, "hy2.effective_mbps", "Effective throughput", 2, s_sustainable)); dimensions.Add(view.Get("hy2.qos_onset_mbps") is { } onset ? new ModelDimension("hy2.qos_onset_mbps", "Throttling onset", 2, onset, s_qosOnset.Evaluate(onset)) : new ModelDimension("hy2.qos_onset_mbps", "Throttling onset", 2, null, 100)); } dimensions.Add(Dim(view, "loss.total", "Packet loss (trend)", hasTest ? 1.5 : 4, s_loss)); dimensions.Add(Dim(view, "jitter.down.p95", "Jitter (down)", hasTest ? 0.5 : 1.5, s_jitter)); dimensions.Add(Dim(view, "rtt.p95", "RTT p95", hasTest ? 0.5 : 1.5, s_rtt)); return Combine(view, dimensions, estimated: !hasTest, hasTest ? [] : ["no_load_test"]); } } /// REALITY(TLS 伪装的 TCP 代理):握手成功率与耗时、长连接稳定性、批量吞吐与重传。 public sealed class RealityModel : ServiceModelBase { private static readonly Curve s_success = new((80, 10), (95, 60), (99, 90), (100, 100)); private static readonly Curve s_tls = new((150, 100), (400, 80), (1000, 40), (3000, 0)); private static readonly Curve s_resets = new((0, 100), (1, 80), (4, 40), (12, 0)); private static readonly Curve s_bulk = new((10, 30), (100, 60), (400, 85), (800, 100)); private static readonly Curve s_retrans = new((0.1, 100), (1, 80), (3, 50), (10, 0)); private static readonly Curve s_rtt = new((40, 100), (120, 80), (250, 40), (500, 0)); public override string Id => "reality"; public override string Title => "REALITY"; public override int Version => 1; public override IReadOnlyList Needs { get; } = ["connect.success", "connect.tls_ms.p95", "hold.resets.per_h", "tcp.bulk.peak.max", "tcp.retrans", "tcp.rtt.p95"]; public override ModelScore Score(MetricView view) { bool hasSession = view.Has("connect.success"); return Combine( view, [ Dim(view, "connect.success", "Connect success", 3, s_success), Dim(view, "connect.tls_ms.p95", "TLS handshake p95", 2, s_tls), Dim(view, "hold.resets.per_h", "Held connection resets", 2, s_resets), Dim(view, "tcp.bulk.peak.max", "Peak bulk throughput", 2, s_bulk), Dim(view, "tcp.retrans", "Retransmissions", 1, s_retrans), Dim(view, "tcp.rtt.p95", "TCP RTT p95", 1, s_rtt), ], estimated: !hasSession, hasSession ? [] : ["no_connect_data"]); } } public static class ModelRegistry { public static readonly IReadOnlyList All = [new BaseModel(), new McModel(), new Hy2Model(), new RealityModel()]; public static IServiceModel? Find(string id) => All.FirstOrDefault(m => m.Id == id); /// 计算全部模型的评分(score.<模型id>)。 public static Dictionary ScoreAll(MetricView view) { var scores = new Dictionary(StringComparer.Ordinal); foreach (IServiceModel model in All) { scores[model.Id] = model.Score(view); } return scores; } }