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);
}
}
}
}