Files
MoShiro/backend/src/Monitor.Backend.Analysis/Models/Models.cs
T
2026-10-04 21:46:39 +08:00

194 lines
8.8 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
namespace Monitor.Backend.Analysis;
/// <summary>线路基础质量:RTT、丢包、抖动、连续丢包。</summary>
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<string> 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);
}
/// <summary>
/// Minecraft:TCP 上不会丢包,丢包表现为重传和卡顿(队头阻塞)。
/// 没有 tcp_echo 数据时,用 udp_echo 的丢包与 RTT 代入简化的 TCP 恢复模型估算,并标记为 estimated。
/// </summary>
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<string> 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<string, double>
{
["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");
}
}
/// <summary>
/// Hysteria2(Brutal 固定码率):有 hy2_ladder 测试结果时,用可持续码率、有效吞吐和限速开始的位置评分;
/// 没有测试时,只能用持续的 udp_echo 趋势粗略估算,并标记为 estimated。
/// </summary>
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<string> 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<ModelDimension>();
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"]);
}
}
/// <summary>REALITY(TLS 伪装的 TCP 代理):握手成功率与耗时、长连接稳定性、批量吞吐与重传。</summary>
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<string> 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<IServiceModel> All = [new BaseModel(), new McModel(), new Hy2Model(), new RealityModel()];
public static IServiceModel? Find(string id) => All.FirstOrDefault(m => m.Id == id);
/// <summary>计算全部模型的评分(score.&lt;模型id&gt;)。</summary>
public static Dictionary<string, ModelScore> ScoreAll(MetricView view)
{
var scores = new Dictionary<string, ModelScore>(StringComparer.Ordinal);
foreach (IServiceModel model in All)
{
scores[model.Id] = model.Score(view);
}
return scores;
}
}