using MoShiro.Protocol; namespace MoShiro.Backend.Analysis; public enum Dir { Up, Down, Total, } public enum GapOutput { Count, MaxMs, } /// 回显流(stream msg)的列:t1/t4 是 Probe 时钟,t2/t3 是 Agent 时钟;缺失的 t2 表示上行丢失,缺失的 t4 表示下行丢失。 internal readonly struct EchoColumns { public EchoColumns(RawStream stream) { Count = stream.Count; Seq = stream.Find("seq"); T1 = stream.Find("t1"); T2 = stream.Find("t2"); T3 = stream.Find("t3"); T4 = stream.Find("t4"); } public int Count { get; } public RawColumn? Seq { get; } public RawColumn? T1 { get; } public RawColumn? T2 { get; } public RawColumn? T3 { get; } public RawColumn? T4 { get; } public bool HasBasics => T1 is not null && T4 is not null; /// 行 i 的发送时刻(UTC 毫秒)。 public long SentUtcMs(ChunkView view, int i) => view.ColumnToUtcMs(T1!, T1!.Values[i]); public bool IsUpLost(int i) => T2 is not null && !T2.IsValid(i); public bool IsReplyLost(int i) => T4 is null || !T4.IsValid(i); /// 按到达时间(t4)排序后的行号,只含收到回复的行。 public int[] ArrivalOrder() { var rows = new List(Count); for (int i = 0; i < Count; i++) { if (!IsReplyLost(i)) { rows.Add(i); } } int[] order = [.. rows]; long[] keys = new long[order.Length]; for (int i = 0; i < order.Length; i++) { keys[i] = T4!.Values[order[i]]; } Array.Sort(keys, order); return order; } } internal sealed class RttOp : MetricOp { public override string Signature => "rtt/1"; public override void Evaluate(ChunkView view, RawStream stream, IObservationSink sink) { var c = new EchoColumns(stream); if (!c.HasBasics) { return; } for (int i = 0; i < c.Count; i++) { if (c.IsReplyLost(i) || !c.T1!.IsValid(i)) { continue; } long rttUs = c.T4!.Values[i] - c.T1.Values[i]; if (c.T2 is not null && c.T3 is not null && c.T2.IsValid(i) && c.T3.IsValid(i)) { rttUs -= c.T3.Values[i] - c.T2.Values[i]; } if (rttUs >= 0) { sink.Emit(c.SentUtcMs(view, i), rttUs / 1000.0); } } } } /// /// 长尾回显:每个有回复的回显一个观测(是否"慢")。阈值以同一个块(10 秒)里 RTT 的中位数为基线: /// max(1.8 × 基线, 基线 + max(15 ms, 0.35 × 基线))。基线取自块内数据,所以每个观测只依赖自己所在的块,窗口合并仍然精确; /// 块里的样本少于 minSamples 时基线不可靠,不产生观测。TCP 的长尾通常是丢包后的重传(RTT 聚在 RTO 附近)或队头阻塞。 /// internal sealed class SlowShareOp : MetricOp { private readonly int _minSamples; public SlowShareOp(int minSamples) { _minSamples = minSamples; } public override string Signature => $"slow_share/{_minSamples}/1"; public override void Evaluate(ChunkView view, RawStream stream, IObservationSink sink) { var c = new EchoColumns(stream); if (!c.HasBasics) { return; } var rtts = new List<(int Row, double Ms)>(c.Count); for (int i = 0; i < c.Count; i++) { if (c.IsReplyLost(i) || !c.T1!.IsValid(i)) { continue; } long rttUs = c.T4!.Values[i] - c.T1.Values[i]; if (c.T2 is not null && c.T3 is not null && c.T2.IsValid(i) && c.T3.IsValid(i)) { rttUs -= c.T3.Values[i] - c.T2.Values[i]; } if (rttUs >= 0) { rtts.Add((i, rttUs / 1000.0)); } } if (rtts.Count < _minSamples) { return; } double[] sorted = [.. rtts.Select(r => r.Ms).Order()]; double baseline = sorted[sorted.Length / 2]; 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); } } } internal sealed class LossOp : MetricOp { private readonly Dir _dir; public LossOp(Dir dir) { _dir = dir; } public override string Signature => "loss/" + _dir + "/1"; public override void Evaluate(ChunkView view, RawStream stream, IObservationSink sink) { var c = new EchoColumns(stream); if (!c.HasBasics || (_dir != Dir.Total && c.T2 is null)) { return; } for (int i = 0; i < c.Count; i++) { if (!c.T1!.IsValid(i)) { continue; } switch (_dir) { case Dir.Up: sink.Emit(c.SentUtcMs(view, i), c.IsUpLost(i) ? 1 : 0, 1); break; case Dir.Down: // 只统计上行已经到达 Agent 的包:回复没回来就是下行丢失。 if (!c.IsUpLost(i)) { sink.Emit(c.SentUtcMs(view, i), c.IsReplyLost(i) ? 1 : 0, 1); } break; default: sink.Emit(c.SentUtcMs(view, i), c.IsReplyLost(i) ? 1 : 0, 1); break; } } } } /// 去除偏移和线性漂移后的单向时延变化:上行用 t2−t1,下行用 t4−t3。 internal sealed class OwdVarOp : MetricOp { private readonly Dir _dir; public OwdVarOp(Dir dir) { _dir = dir; } public override string Signature => "owd_var/" + _dir + "/1"; public override void Evaluate(ChunkView view, RawStream stream, IObservationSink sink) { var c = new EchoColumns(stream); RawColumn? a = _dir == Dir.Up ? c.T1 : c.T3; RawColumn? b = _dir == Dir.Up ? c.T2 : c.T4; if (c.T1 is null || a is null || b is null) { return; } var times = new List(c.Count); var delays = new List(c.Count); var utc = new List(c.Count); for (int i = 0; i < c.Count; i++) { if (!c.T1.IsValid(i) || !a.IsValid(i) || !b.IsValid(i)) { continue; } times.Add(c.T1.Values[i]); delays.Add(b.Values[i] - a.Values[i]); utc.Add(c.SentUtcMs(view, i)); } JitterEmitter.Emit(times, delays, utc, sink); } } internal static class JitterEmitter { public static void Emit(List timesUs, List delaysUs, List utcMs, IObservationSink sink) { double[]? jitter = ClockDriftFit.Jitter([.. timesUs], [.. delaysUs]); if (jitter is null) { return; } for (int i = 0; i < jitter.Length; i++) { sink.Emit(utcMs[i], jitter[i] / 1000.0); } } } /// 连续丢失段的长度(按发送顺序)。 internal sealed class LossRunsOp : MetricOp { private readonly Dir _direction; public LossRunsOp(Dir direction) { _direction = direction; } public override string Signature => $"loss_runs/{_direction}/2"; public override void Evaluate(ChunkView view, RawStream stream, IObservationSink sink) { var c = new EchoColumns(stream); if (!c.HasBasics) { return; } int run = 0; long runStart = 0; for (int i = 0; i < c.Count; i++) { if (!c.T1!.IsValid(i)) { continue; } bool lost = _direction switch { Dir.Up => c.IsUpLost(i), Dir.Down => !c.IsUpLost(i) && c.IsReplyLost(i), _ => c.IsReplyLost(i), }; if (lost) { if (run == 0) { runStart = c.SentUtcMs(view, i); } run++; } else if (run > 0) { sink.Emit(runStart, run); run = 0; } } if (run > 0) { sink.Emit(runStart, run); } } } /// 每个块一个最长连续丢包值;无丢包的有效块明确输出 0。 internal sealed class LossRunMaxOp(Dir direction) : MetricOp { public override string Signature => $"loss_run_max/{direction}/1"; public override void Evaluate(ChunkView view, RawStream stream, IObservationSink sink) { var c = new EchoColumns(stream); if (!c.HasBasics || (direction != Dir.Total && c.T2 is null)) { return; } int run = 0; int maximum = 0; bool hasSample = false; long sampleAt = 0; for (int i = 0; i < c.Count; i++) { if (!c.T1!.IsValid(i)) { continue; } bool lost = direction switch { Dir.Up => c.IsUpLost(i), Dir.Down => !c.IsUpLost(i) && c.IsReplyLost(i), _ => c.IsReplyLost(i), }; if (direction == Dir.Down && c.IsUpLost(i)) { // A request lost before reaching the Agent is not a downstream sample. run = 0; continue; } if (!hasSample) { sampleAt = c.SentUtcMs(view, i); hasSample = true; } run = lost ? run + 1 : 0; maximum = Math.Max(maximum, run); } if (hasSample) { sink.Emit(sampleAt, maximum); } } } /// 乱序:按到达顺序,序号比之前见过的最大序号还小。重复的序号不算乱序。 internal sealed class ReorderOp : MetricOp { public override string Signature => "reorder/1"; public override void Evaluate(ChunkView view, RawStream stream, IObservationSink sink) { var c = new EchoColumns(stream); if (!c.HasBasics || c.Seq is null) { return; } var seen = new HashSet(); long maxSeq = long.MinValue; foreach (int row in c.ArrivalOrder()) { long seq = c.Seq.Values[row]; if (!seen.Add(seq)) { continue; } if (seq < maxSeq) { sink.Emit(view.ColumnToUtcMs(c.T4!, c.T4!.Values[row]), 1); } maxSeq = Math.Max(maxSeq, seq); } } } /// 重复:同一个序号收到不止一次。 internal sealed class DupOp : MetricOp { public override string Signature => "dup/1"; public override void Evaluate(ChunkView view, RawStream stream, IObservationSink sink) { var c = new EchoColumns(stream); if (!c.HasBasics || c.Seq is null) { return; } var seen = new HashSet(); foreach (int row in c.ArrivalOrder()) { if (!seen.Add(c.Seq.Values[row])) { sink.Emit(view.ColumnToUtcMs(c.T4!, c.T4!.Values[row]), 1); } } } } /// 卡顿:到达间隔超过“期望间隔 + 阈值”。 internal sealed class GapOp : MetricOp { private readonly GapOutput _output; private readonly string _rateParam; private readonly double _thresholdMs; public GapOp(GapOutput output, string rateParam, double thresholdMs) { _output = output; _rateParam = rateParam; _thresholdMs = thresholdMs; } public override string Signature => $"gap/{_output}/{_rateParam}/{_thresholdMs}/1"; public override void Evaluate(ChunkView view, RawStream stream, IObservationSink sink) { var c = new EchoColumns(stream); if (!c.HasBasics) { return; } double rate = Math.Max(0.1, view.ParamNumber(_rateParam, 1)); double expectedMs = 1000.0 / rate; int[] order = c.ArrivalOrder(); for (int i = 1; i < order.Length; i++) { double gapMs = (c.T4!.Values[order[i]] - c.T4.Values[order[i - 1]]) / 1000.0; if (gapMs - expectedMs <= _thresholdMs) { continue; } long at = view.ColumnToUtcMs(c.T4, c.T4.Values[order[i]]); sink.Emit(at, _output == GapOutput.Count ? 1 : gapMs); } } } /// 每一行一个观测值,用于统计样本量。 internal sealed class RowCountOp : MetricOp { public override string Signature => "row_count/1"; public override void Evaluate(ChunkView view, RawStream stream, IObservationSink sink) { RawColumn? time = null; foreach (RawColumn column in stream.Columns) { if (column.Role != ColumnRole.None) { time = column; break; } } if (time is null) { return; } for (int i = 0; i < stream.Count; i++) { if (time.IsValid(i)) { sink.Emit(view.ColumnToUtcMs(time, time.Values[i]), 1); } } } }