namespace MoShiro.Backend.Analysis; public sealed record PresetDef(string Id, string Label, IReadOnlyList MetricIds); /// /// 指标注册表:存储、降采样、API、目录都由它驱动。新增指标时,在对应的 Metrics 文件里加一行声明, /// 再把它放进这里的 (显式注册,AOT 安全,不使用反射)。 /// public static class MetricRegistry { public static readonly IReadOnlyList All = [ LinkMetrics.Rtt, LinkMetrics.LossUp, LinkMetrics.LossDown, LinkMetrics.LossTotal, LinkMetrics.JitterUp, LinkMetrics.JitterDown, LinkMetrics.LossRun, LinkMetrics.Reorder, LinkMetrics.Dup, LinkMetrics.SamplesUdp, TcpMetrics.Rtt, TcpMetrics.Slow, TcpMetrics.Stall, TcpMetrics.StallMax, TcpMetrics.Retrans, TcpMetrics.Srtt, TcpMetrics.BulkRate, TcpMetrics.BulkPeak, TcpMetrics.SamplesTcp, SessionMetrics.ConnectSuccess, SessionMetrics.ConnectTcp, SessionMetrics.ConnectTls, SessionMetrics.FailTimeout, SessionMetrics.FailReset, SessionMetrics.FailTls, SessionMetrics.HoldResets, SessionMetrics.HoldLifetime, SessionMetrics.TrainLoss, SessionMetrics.TrainJitter, SessionMetrics.TrainRate, SessionMetrics.SamplesConnect, ]; /// 指标分组:前端按分组展示,新增指标只需要把它加进分组。 public static readonly IReadOnlyList Presets = [ new("udp", "UDP", ["loss.up", "loss.down", "loss.total", "rtt.p50", "rtt.p95", "rtt.p99", "jitter.up.p95", "jitter.down.p95", "loss.run_max", "reorder", "dup"]), new("tcp", "TCP", ["tcp.rtt.p50", "tcp.rtt.p95", "tcp.rtt.p99", "tcp.rtt.p999", "tcp.rtt.max", "tcp.rtt.tail", "tcp.slow", "tcp.stall.per_min", "tcp.stall_max", "tcp.retrans", "tcp.srtt.p95"]), new("connect", "Connect", ["connect.success", "connect.tcp_ms.p95", "connect.tls_ms.p95", "connect.fail.timeout", "connect.fail.rst", "connect.fail.tls"]), new("session", "Session", ["hold.resets.per_h", "hold.lifetime_max"]), new("scores", "Scores", ["score.base", "score.mc", "score.hy2", "score.reality"]), ]; private static readonly Dictionary s_byId = All.ToDictionary(m => m.Id, StringComparer.Ordinal); private static readonly Dictionary s_byExport = BuildExportIndex(); public static MetricDef? Find(string metricId) => s_byId.GetValueOrDefault(metricId); /// 按导出 id(例如 rtt.p95)查找。 public static bool TryFindExport(string exportId, out MetricDef def, out MetricExport export) { if (s_byExport.TryGetValue(exportId, out (MetricDef Def, MetricExport Export) found)) { def = found.Def; export = found.Export; return true; } def = null!; export = null!; return false; } public static PresetDef? FindPreset(string id) => Presets.FirstOrDefault(p => p.Id == id); public static IEnumerable<(MetricDef Def, MetricExport Export)> AllExports() => All.SelectMany(def => def.Exports.Select(export => (def, export))); private static Dictionary BuildExportIndex() { var index = new Dictionary(StringComparer.Ordinal); foreach (MetricDef def in All) { foreach (MetricExport export in def.Exports) { if (!index.TryAdd(export.Id, (def, export))) { throw new InvalidOperationException($"duplicate export id {export.Id}"); } } } return index; } }