Files
MoShiro/backend/src/MoShiro.Backend.Analysis/Ops/EchoOps.cs
T

513 lines
14 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.
using MoShiro.Protocol;
namespace MoShiro.Backend.Analysis;
public enum Dir
{
Up,
Down,
Total,
}
public enum GapOutput
{
Count,
MaxMs,
}
/// <summary>回显流(stream <c>msg</c>)的列:t1/t4 是 Probe 时钟,t2/t3 是 Agent 时钟;缺失的 t2 表示上行丢失,缺失的 t4 表示下行丢失。</summary>
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;
/// <summary>行 i 的发送时刻(UTC 毫秒)。</summary>
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);
/// <summary>按到达时间(t4)排序后的行号,只含收到回复的行。</summary>
public int[] ArrivalOrder()
{
var rows = new List<int>(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);
}
}
}
}
/// <summary>
/// 长尾回显:每个有回复的回显一个观测(是否"慢")。阈值以同一个块(10 秒)里 RTT 的中位数为基线:
/// <c>max(1.8 × 基线, 基线 + max(15 ms, 0.35 × 基线))</c>。基线取自块内数据,所以每个观测只依赖自己所在的块,窗口合并仍然精确;
/// 块里的样本少于 <c>minSamples</c> 时基线不可靠,不产生观测。TCP 的长尾通常是丢包后的重传(RTT 聚在 RTO 附近)或队头阻塞。
/// </summary>
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;
}
}
}
}
/// <summary>去除偏移和线性漂移后的单向时延变化:上行用 t2−t1,下行用 t4−t3。</summary>
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<double>(c.Count);
var delays = new List<double>(c.Count);
var utc = new List<long>(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<double> timesUs, List<double> delaysUs, List<long> 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);
}
}
}
/// <summary>连续丢失段的长度(按发送顺序)。</summary>
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);
}
}
}
/// <summary>每个块一个最长连续丢包值;无丢包的有效块明确输出 0。</summary>
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);
}
}
}
/// <summary>乱序:按到达顺序,序号比之前见过的最大序号还小。重复的序号不算乱序。</summary>
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>();
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);
}
}
}
/// <summary>重复:同一个序号收到不止一次。</summary>
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<long>();
foreach (int row in c.ArrivalOrder())
{
if (!seen.Add(c.Seq.Values[row]))
{
sink.Emit(view.ColumnToUtcMs(c.T4!, c.T4!.Values[row]), 1);
}
}
}
}
/// <summary>卡顿:到达间隔超过“期望间隔 + 阈值”。</summary>
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);
}
}
}
/// <summary>每一行一个观测值,用于统计样本量。</summary>
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);
}
}
}
}