chore: initial project import
This commit is contained in:
commit
3fc4e5ca53
347 files changed
+56172
No files matched your search
@@ -0,0 +1,162 @@
|
||||
using Monitor.Backend.Analysis;
|
||||
|
||||
namespace Monitor.Backend.Analysis.Tests;
|
||||
|
||||
public sealed class AccumulatorTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(0.5)]
|
||||
[InlineData(0.9)]
|
||||
[InlineData(0.95)]
|
||||
[InlineData(0.99)]
|
||||
public void Histogram_Quantile_WithinRelativeError(double q)
|
||||
{
|
||||
var rng = new Random(42);
|
||||
var values = new List<double>();
|
||||
var hist = new HistogramAcc();
|
||||
for (int i = 0; i < 20_000; i++)
|
||||
{
|
||||
// 对数正态分布:中位数约 50 ms,长尾
|
||||
double v = 50 * Math.Exp(rng.NextDouble() * 2.5 - 1.2);
|
||||
values.Add(v);
|
||||
hist.Add(v);
|
||||
}
|
||||
|
||||
values.Sort();
|
||||
double exact = values[(int)(q * (values.Count - 1))];
|
||||
double estimate = hist.Quantile(q)!.Value;
|
||||
Assert.InRange(Math.Abs(estimate - exact) / exact, 0, 0.045);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Histogram_Merge_EqualsSingleAccumulator()
|
||||
{
|
||||
var rng = new Random(7);
|
||||
var whole = new HistogramAcc();
|
||||
var left = new HistogramAcc();
|
||||
var right = new HistogramAcc();
|
||||
for (int i = 0; i < 5000; i++)
|
||||
{
|
||||
double v = rng.NextDouble() * 200;
|
||||
whole.Add(v);
|
||||
(i % 2 == 0 ? left : right).Add(v);
|
||||
}
|
||||
|
||||
left.Merge(right);
|
||||
Assert.Equal(whole.Count, left.Count);
|
||||
Assert.Equal(whole.Quantile(0.95), left.Quantile(0.95));
|
||||
Assert.Equal(whole.Quantile(0.5), left.Quantile(0.5));
|
||||
Assert.Equal(whole.Max, left.Max);
|
||||
Assert.Equal(whole.NonEmptyBuckets, left.NonEmptyBuckets);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Histogram_Empty_HasNoQuantiles()
|
||||
{
|
||||
var hist = new HistogramAcc();
|
||||
Assert.Null(hist.Quantile(0.5));
|
||||
Assert.Null(hist.Mean);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Histogram_SingleSample_QuantilesClampToSample()
|
||||
{
|
||||
var hist = new HistogramAcc();
|
||||
hist.Add(42);
|
||||
Assert.Equal(42, hist.Quantile(0.01));
|
||||
Assert.Equal(42, hist.Quantile(0.99));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Histogram_IgnoresNegativeAndNaN()
|
||||
{
|
||||
var hist = new HistogramAcc();
|
||||
hist.Add(-1);
|
||||
hist.Add(double.NaN);
|
||||
Assert.Equal(0UL, hist.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Histogram_ValuesBeyondRange_LandInEdgeBuckets()
|
||||
{
|
||||
var hist = new HistogramAcc();
|
||||
hist.Add(0.01);
|
||||
hist.Add(1_000_000);
|
||||
Assert.Equal(2UL, hist.Count);
|
||||
Assert.Equal(2, hist.NonEmptyBuckets);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ratio_Merge_SumsNumeratorAndDenominator()
|
||||
{
|
||||
var a = new RatioAcc();
|
||||
a.Add(1, 10);
|
||||
var b = new RatioAcc();
|
||||
b.Add(3, 10);
|
||||
a.Merge(b);
|
||||
Assert.Equal(20.0, a.Percent);
|
||||
Assert.Null(new RatioAcc().Percent);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Merge_DifferentKinds_Throws()
|
||||
{
|
||||
Assert.Throws<InvalidOperationException>(() => new CounterAcc().Merge(new RatioAcc()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Codec_RoundTrip_PreservesEveryKind()
|
||||
{
|
||||
var set = new MetricSet();
|
||||
set.GetOrAdd("c", AccKind.Counter).Add(5);
|
||||
set.GetOrAdd("r", AccKind.Ratio).Add(2, 9);
|
||||
set.GetOrAdd("s", AccKind.Sum).Add(2.5);
|
||||
set.GetOrAdd("n", AccKind.Min).Add(-3);
|
||||
set.GetOrAdd("x", AccKind.Max).Add(11);
|
||||
set.GetOrAdd("l", AccKind.RunLength).Add(4);
|
||||
var hist = (HistogramAcc)set.GetOrAdd("h", AccKind.Histogram);
|
||||
foreach (double v in new[] { 1.0, 20, 20, 300 })
|
||||
{
|
||||
hist.Add(v);
|
||||
}
|
||||
|
||||
set.GetOrAdd("t", AccKind.Series).Add(2, 7.5);
|
||||
|
||||
MetricSet decoded = AccumulatorCodec.Decode(AccumulatorCodec.Encode(set)).Value;
|
||||
|
||||
Assert.Equal(5UL, ((CounterAcc)decoded.Items["c"]).Value);
|
||||
Assert.Equal(9UL, ((RatioAcc)decoded.Items["r"]).Denominator);
|
||||
Assert.Equal(2.5, ((SumAcc)decoded.Items["s"]).Sum);
|
||||
Assert.Equal(-3, ((MinAcc)decoded.Items["n"]).Value);
|
||||
Assert.Equal(11, ((MaxAcc)decoded.Items["x"]).Value);
|
||||
Assert.Equal(4, ((RunLengthAcc)decoded.Items["l"]).Max);
|
||||
var h2 = (HistogramAcc)decoded.Items["h"];
|
||||
Assert.Equal(hist.Count, h2.Count);
|
||||
Assert.Equal(hist.Quantile(0.5), h2.Quantile(0.5));
|
||||
Assert.Equal(7.5, ((SeriesAcc)decoded.Items["t"]).Points[2]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Codec_Decode_RejectsTruncatedData()
|
||||
{
|
||||
var set = new MetricSet();
|
||||
set.GetOrAdd("h", AccKind.Histogram).Add(10);
|
||||
byte[] bytes = AccumulatorCodec.Encode(set);
|
||||
Assert.False(AccumulatorCodec.Decode(bytes.AsSpan(0, bytes.Length - 3)).IsSuccess);
|
||||
Assert.False(AccumulatorCodec.Decode([200, 200]).IsSuccess);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MetricSet_Merge_CombinesDisjointAndSharedMetrics()
|
||||
{
|
||||
var a = new MetricSet();
|
||||
a.GetOrAdd("x", AccKind.Counter).Add(1);
|
||||
var b = new MetricSet();
|
||||
b.GetOrAdd("x", AccKind.Counter).Add(2);
|
||||
b.GetOrAdd("y", AccKind.Counter).Add(4);
|
||||
a.Merge(b);
|
||||
Assert.Equal(3UL, ((CounterAcc)a.Items["x"]).Value);
|
||||
Assert.Equal(4UL, ((CounterAcc)a.Items["y"]).Value);
|
||||
Assert.Equal(2UL, ((CounterAcc)b.Items["x"]).Value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
using Fleet.Core;
|
||||
using Monitor.Protocol;
|
||||
using Monitor.TestSupport;
|
||||
|
||||
namespace Monitor.Backend.Analysis.Tests;
|
||||
|
||||
public sealed class ModelAndTestEvaluatorTests
|
||||
{
|
||||
private const long T0 = 1_700_000_000_000;
|
||||
private static readonly NodeId s_agent = new("agt_test");
|
||||
|
||||
private static MetricView View(params (string Id, double Value)[] values) =>
|
||||
new(values.ToDictionary(v => v.Id, v => v.Value));
|
||||
|
||||
[Fact]
|
||||
public void Registry_ExportIds_AreUnique_AndPresetsResolve()
|
||||
{
|
||||
Assert.Equal(MetricRegistry.AllExports().Count(), MetricRegistry.AllExports().Select(e => e.Export.Id).Distinct().Count());
|
||||
foreach (PresetDef preset in MetricRegistry.Presets)
|
||||
{
|
||||
foreach (string id in preset.MetricIds)
|
||||
{
|
||||
Assert.True(
|
||||
MetricRegistry.TryFindExport(id, out _, out _) || id.StartsWith("score.", StringComparison.Ordinal),
|
||||
$"preset {preset.Id} references unknown metric {id}");
|
||||
}
|
||||
}
|
||||
|
||||
Assert.All(MetricRegistry.All, m => Assert.NotEmpty(m.Fingerprint));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fingerprint_ChangesWhenTheOpChanges()
|
||||
{
|
||||
var a = Metric.Counter("x", ["udp_echo.msg"], Ops.Gap(GapOutput.Count, "rate_hz", 100), "x", "x", new MetricExport("x", Stat.Count, Unit.Count));
|
||||
var b = Metric.Counter("x", ["udp_echo.msg"], Ops.Gap(GapOutput.Count, "rate_hz", 200), "x", "x", new MetricExport("x", Stat.Count, Unit.Count));
|
||||
Assert.NotEqual(a.Fingerprint, b.Fingerprint);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BaseModel_PerfectLine_ScoresHigh_AndBadLine_ScoresLow()
|
||||
{
|
||||
double good = new BaseModel().Score(View(("rtt.p95", 25), ("loss.total", 0), ("jitter.down.p95", 1), ("jitter.up.p95", 1), ("loss.run_max", 1))).Score!.Value;
|
||||
double bad = new BaseModel().Score(View(("rtt.p95", 400), ("loss.total", 12), ("jitter.down.p95", 90), ("jitter.up.p95", 90), ("loss.run_max", 40))).Score!.Value;
|
||||
Assert.True(good > 95);
|
||||
Assert.True(bad < 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Models_WithNoData_ReturnNullScore()
|
||||
{
|
||||
foreach (IServiceModel model in ModelRegistry.All)
|
||||
{
|
||||
Assert.Null(model.Score(View()).Score);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void McModel_WithoutTcpData_EstimatesFromUdp()
|
||||
{
|
||||
ModelScore score = new McModel().Score(View(("rtt.p95", 60), ("loss.total", 2), ("loss.run_max", 2)));
|
||||
Assert.True(score.Estimated);
|
||||
Assert.NotNull(score.Score);
|
||||
Assert.Contains("estimated_from_udp", score.Caveats);
|
||||
Assert.False(new McModel().Score(View(("tcp.rtt.p95", 60), ("tcp.stall.per_min", 0))).Estimated);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Hy2Model_TestResultsDominate_AndNoTestIsEstimated()
|
||||
{
|
||||
ModelScore withTest = new Hy2Model().Score(View(("hy2.sustainable_mbps", 100), ("hy2.effective_mbps", 95), ("loss.total", 0.2)));
|
||||
ModelScore without = new Hy2Model().Score(View(("loss.total", 0.2), ("rtt.p95", 50)));
|
||||
Assert.False(withTest.Estimated);
|
||||
Assert.True(without.Estimated);
|
||||
Assert.Contains("no_load_test", without.Caveats);
|
||||
Assert.True(withTest.Score > 90);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Models_AreMonotonic_WorseMetricsNeverRaiseTheScore()
|
||||
{
|
||||
// 对每个模型的每个所需指标:在“好”和“差”之间扫描,分数不能随指标变差而升高。
|
||||
var ranges = new Dictionary<string, double[]>
|
||||
{
|
||||
["rtt.p95"] = [10, 30, 80, 150, 300, 600], ["loss.total"] = [0, 0.5, 1, 3, 10, 30],
|
||||
["jitter.down.p95"] = [0, 2, 8, 20, 80, 200], ["jitter.up.p95"] = [0, 2, 8, 20, 80, 200], ["loss.run_max"] = [1, 3, 10, 30, 60],
|
||||
["tcp.rtt.p95"] = [10, 40, 100, 200, 400, 800], ["tcp.stall.per_min"] = [0, 1, 5, 20, 60, 200], ["tcp.stall_max"] = [50, 150, 500, 2000, 5000],
|
||||
["tcp.retrans"] = [0, 0.1, 1, 3, 10, 30], ["connect.tls_ms.p95"] = [50, 150, 400, 1000, 3000], ["hold.resets.per_h"] = [0, 1, 4, 12, 30],
|
||||
["connect.success"] = [100, 99, 95, 80, 50], ["tcp.bulk.peak.max"] = [800, 400, 100, 10, 1], ["hy2.sustainable_mbps"] = [100, 50, 25, 10, 0],
|
||||
["hy2.effective_mbps"] = [100, 50, 25, 10, 0], ["hy2.qos_onset_mbps"] = [100, 50, 25, 10],
|
||||
};
|
||||
|
||||
foreach (IServiceModel model in ModelRegistry.All)
|
||||
{
|
||||
foreach (string need in model.Needs.Where(ranges.ContainsKey))
|
||||
{
|
||||
var baseline = ranges.Where(r => model.Needs.Contains(r.Key)).ToDictionary(r => r.Key, r => r.Value[0]);
|
||||
double previous = double.MaxValue;
|
||||
foreach (double value in ranges[need])
|
||||
{
|
||||
var values = new Dictionary<string, double>(baseline) { [need] = value };
|
||||
double score = model.Score(new MetricView(values)).Score ?? 0;
|
||||
Assert.True(score <= previous + 1e-9, $"{model.Id}: {need}={value} raised the score from {previous} to {score}");
|
||||
previous = score;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestEvaluator_Hy2Ladder_APhaseSplitIntoSeveralChunksIsOneStep()
|
||||
{
|
||||
// 真实的 Probe 把一个阶段切成几块(一块最多 50000 条记录):阶梯里的一步是一个阶段,不是一个块
|
||||
RawChunk[] chunks =
|
||||
[
|
||||
ChunkFactory.Train(s_agent, T0, 10, 0),
|
||||
ChunkFactory.Train(s_agent, T0 + 10_000, 50, 1, seconds: 5, lossEvery: 5),
|
||||
ChunkFactory.Train(s_agent, T0 + 15_000, 50, 1, seconds: 5, lossEvery: 5),
|
||||
];
|
||||
|
||||
TestResult result = TestEvaluator.Evaluate("hy2_ladder", chunks);
|
||||
|
||||
Assert.Equal(2, result.Steps.Count);
|
||||
Assert.Equal(new double[] { 10, 50 }, result.Series["step_mbps"]);
|
||||
Assert.InRange(result.Steps[1].LossPct, 19, 21);
|
||||
Assert.InRange(result.Steps[1].DeliveredMbps, 38, 42); // 50 Mbit/s × (1 − 20% 丢包),按整个阶段的时长算
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestEvaluator_Hy2Ladder_FindsSustainableRateAndOnset()
|
||||
{
|
||||
// 10/25 Mbps 无丢包;50 Mbps 起被限速,丢包 20%、40%
|
||||
RawChunk[] chunks =
|
||||
[
|
||||
ChunkFactory.Train(s_agent, T0, 10, 0),
|
||||
ChunkFactory.Train(s_agent, T0 + 10_000, 25, 1),
|
||||
ChunkFactory.Train(s_agent, T0 + 20_000, 50, 2, lossEvery: 5),
|
||||
ChunkFactory.Train(s_agent, T0 + 30_000, 100, 3, lossEvery: 2),
|
||||
];
|
||||
|
||||
TestResult result = TestEvaluator.Evaluate("hy2_ladder", chunks);
|
||||
|
||||
Assert.Equal(4, result.Steps.Count);
|
||||
Assert.Equal(25, result.Metrics["hy2.sustainable_mbps"]);
|
||||
Assert.Equal(50, result.Metrics["hy2.qos_onset_mbps"]);
|
||||
Assert.InRange(result.Steps[2].LossPct, 19, 21);
|
||||
Assert.InRange(result.Steps[3].LossPct, 49, 51);
|
||||
Assert.InRange(result.Steps[0].DeliveredMbps, 9.5, 10.5);
|
||||
Assert.Equal(new double[] { 10, 25, 50, 100 }, result.Series["step_mbps"]);
|
||||
Assert.Contains("port_differs_from_service", result.Caveats);
|
||||
|
||||
// 模型能直接消费测试结果
|
||||
ModelScore score = new Hy2Model().Score(new MetricView(result.Metrics));
|
||||
Assert.False(score.Estimated);
|
||||
Assert.InRange(score.Score!.Value, 20, 80);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestEvaluator_PartialChunks_AddCaveat()
|
||||
{
|
||||
RawChunk chunk = ChunkFactory.Train(s_agent, T0, 10, 0);
|
||||
chunk.Header.Partial = true;
|
||||
Assert.Contains("partial_data", TestEvaluator.Evaluate("hy2_ladder", [chunk]).Caveats);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<IsPackable>false</IsPackable>
|
||||
<NoWarn>$(NoWarn);CA1861</NoWarn>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="xunit.v3" />
|
||||
<ProjectReference Include="..\..\src\Monitor.Backend.Analysis\Monitor.Backend.Analysis.csproj" />
|
||||
<ProjectReference Include="..\Monitor.TestSupport\Monitor.TestSupport.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,254 @@
|
||||
using Fleet.Core;
|
||||
using Monitor.Protocol;
|
||||
using Monitor.TestSupport;
|
||||
|
||||
namespace Monitor.Backend.Analysis.Tests;
|
||||
|
||||
public sealed class OpTests
|
||||
{
|
||||
private const long T0 = 1_700_000_000_000;
|
||||
private static readonly NodeId s_agent = new("agt_test");
|
||||
|
||||
private static MetricSet EvaluateOneBucket(RawChunk chunk, params string[] only)
|
||||
{
|
||||
var buckets = new SortedDictionary<long, MetricSet>();
|
||||
WindowEvaluator.Default.Evaluate(chunk, 24L * 3600 * 1000, buckets, only.Length == 0 ? null : new HashSet<string>(only));
|
||||
var merged = new MetricSet();
|
||||
foreach (MetricSet set in buckets.Values)
|
||||
{
|
||||
merged.Merge(set);
|
||||
}
|
||||
|
||||
return merged;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Rtt_RemovesAgentProcessingTime()
|
||||
{
|
||||
RawChunk chunk = ChunkFactory.Echo(s_agent, "udp_echo", T0, 100, 100, _ => new EchoPacket(false, false, 20_000, 30_000, ProcessUs: 5_000));
|
||||
var rtt = (HistogramAcc)EvaluateOneBucket(chunk, "rtt").Items["rtt"];
|
||||
Assert.Equal(100UL, rtt.Count);
|
||||
Assert.InRange(rtt.Quantile(0.5)!.Value, 50 * 0.955, 50 * 1.045);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Loss_SeparatesUpAndDown()
|
||||
{
|
||||
RawChunk chunk = ChunkFactory.Echo(
|
||||
s_agent, "udp_echo", T0, 100, 100,
|
||||
i => new EchoPacket(LostUp: i < 10, LostDown: i is >= 10 and < 15, 20_000, 20_000));
|
||||
MetricSet set = EvaluateOneBucket(chunk, "loss.up", "loss.down", "loss.total");
|
||||
|
||||
Assert.Equal(10.0, ((RatioAcc)set.Items["loss.up"]).Percent);
|
||||
// 下行只统计上行已到达的 90 个包里的 5 个
|
||||
Assert.Equal(100.0 * 5 / 90, ((RatioAcc)set.Items["loss.down"]).Percent!.Value, 6);
|
||||
Assert.Equal(15.0, ((RatioAcc)set.Items["loss.total"]).Percent);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Jitter_ConstantDelayWithClockOffset_IsZero()
|
||||
{
|
||||
RawChunk chunk = ChunkFactory.UdpEchoClean(s_agent, T0);
|
||||
var jitter = (HistogramAcc)EvaluateOneBucket(chunk, "jitter.up", "jitter.down").Items["jitter.up"];
|
||||
Assert.True(jitter.Count > 0);
|
||||
Assert.True(jitter.Quantile(0.95)!.Value < 0.2, "a constant delay must not look like jitter despite the 5 s clock offset");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Jitter_OnlyOneDirectionVaries_IsAttributedToThatDirection()
|
||||
{
|
||||
// 下行时延每隔一个包多 20 ms
|
||||
RawChunk chunk = ChunkFactory.Echo(s_agent, "udp_echo", T0, 100, 100, i => new EchoPacket(false, false, 20_000, 20_000 + (i % 2 == 0 ? 20_000 : 0)));
|
||||
MetricSet set = EvaluateOneBucket(chunk, "jitter.up", "jitter.down");
|
||||
Assert.True(((HistogramAcc)set.Items["jitter.up"]).Quantile(0.95) < 0.5);
|
||||
Assert.InRange(((HistogramAcc)set.Items["jitter.down"]).Quantile(0.95)!.Value, 15, 25);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Jitter_TooFewSamples_ProducesNoObservations()
|
||||
{
|
||||
RawChunk chunk = ChunkFactory.UdpEchoClean(s_agent, T0, count: 5);
|
||||
MetricSet set = EvaluateOneBucket(chunk, "jitter.up");
|
||||
Assert.False(set.Items.ContainsKey("jitter.up"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LossRuns_ReportsConsecutiveLosses()
|
||||
{
|
||||
bool[] lost = [false, true, true, true, false, true, false, false, true, true];
|
||||
RawChunk chunk = ChunkFactory.Echo(s_agent, "udp_echo", T0, lost.Length, 100, i => new EchoPacket(false, lost[i], 20_000, 20_000));
|
||||
var runs = (RunLengthAcc)EvaluateOneBucket(chunk, "loss.run").Items["loss.run"];
|
||||
Assert.Equal(3UL, runs.Runs);
|
||||
Assert.Equal(3, runs.Max);
|
||||
Assert.Equal(2.0, runs.Mean);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LossRuns_TrailingRunIsReported()
|
||||
{
|
||||
RawChunk chunk = ChunkFactory.Echo(s_agent, "udp_echo", T0, 4, 100, i => new EchoPacket(false, i >= 2, 20_000, 20_000));
|
||||
Assert.Equal(2, ((RunLengthAcc)EvaluateOneBucket(chunk, "loss.run").Items["loss.run"]).Max);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reorder_CountsLateArrivals_AndIgnoresDuplicates()
|
||||
{
|
||||
// 包 3 的回复比包 4 晚到
|
||||
RawChunk chunk = ChunkFactory.Echo(s_agent, "udp_echo", T0, 6, 100, i => new EchoPacket(false, false, 20_000, i == 3 ? 150_000 : 20_000));
|
||||
MetricSet set = EvaluateOneBucket(chunk, "reorder", "dup");
|
||||
Assert.Equal(1UL, ((CounterAcc)set.Items["reorder"]).Value);
|
||||
Assert.False(set.Items.ContainsKey("dup"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Stall_DetectsHeadOfLineBlocking()
|
||||
{
|
||||
// 第 40 个包之后的 10 个包被队头阻塞,全部在 2540 ms 之后几乎同时到达
|
||||
RawChunk chunk = ChunkFactory.Echo(
|
||||
s_agent, "tcp_echo", T0, 100, 50,
|
||||
i => new EchoPacket(false, false, 20_000, i is >= 40 and < 50 ? ((2540 + (i - 40) - (50 * i) - 20) * 1000) - 50 : 20_000));
|
||||
MetricSet set = EvaluateOneBucket(chunk, "tcp.stall", "tcp.stall_max");
|
||||
Assert.Equal(1UL, ((CounterAcc)set.Items["tcp.stall"]).Value);
|
||||
Assert.InRange(((MaxAcc)set.Items["tcp.stall_max"]).Value, 500, 600);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Stall_SteadyArrivals_AreNotStalls()
|
||||
{
|
||||
RawChunk chunk = ChunkFactory.Echo(s_agent, "tcp_echo", T0, 100, 50, _ => new EchoPacket(false, false, 20_000, 20_000));
|
||||
MetricSet set = EvaluateOneBucket(chunk, "tcp.stall");
|
||||
Assert.False(set.Items.ContainsKey("tcp.stall"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Slow_CountsEchoesBeyondTheChunkLocalLongTailThreshold()
|
||||
{
|
||||
// 基线 50 ms(单程 25 ms),阈值 = max(90, 50 + max(15, 17.5)) = 90 ms;第 10、20、30 个回显的 RTT 是 200 ms
|
||||
RawChunk chunk = ChunkFactory.Echo(
|
||||
s_agent, "tcp_echo", T0, 100, 50,
|
||||
i => new EchoPacket(false, false, 25_000, i % 10 == 0 && i > 0 && i < 40 ? 175_000 : 25_000, ProcessUs: 0));
|
||||
var slow = (RatioAcc)EvaluateOneBucket(chunk, "tcp.slow").Items["tcp.slow"];
|
||||
Assert.Equal(100UL, slow.Denominator);
|
||||
Assert.Equal(3UL, slow.Numerator);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Slow_ASteadyConnection_HasNoLongTail_AndTooFewSamplesGiveNoObservation()
|
||||
{
|
||||
RawChunk steady = ChunkFactory.Echo(s_agent, "tcp_echo", T0, 100, 50, _ => new EchoPacket(false, false, 25_000, 25_000));
|
||||
Assert.Equal(0UL, ((RatioAcc)EvaluateOneBucket(steady, "tcp.slow").Items["tcp.slow"]).Numerator);
|
||||
|
||||
RawChunk few = ChunkFactory.Echo(s_agent, "tcp_echo", T0, 10, 50, i => new EchoPacket(false, false, 25_000, i == 5 ? 500_000 : 25_000));
|
||||
Assert.False(EvaluateOneBucket(few, "tcp.slow").Items.ContainsKey("tcp.slow"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RttTail_P999AndTailRatioComeFromTheSameMergedHistogram()
|
||||
{
|
||||
// 1000 个回显,其中 5 个 1 s 的长尾:p50 约 50 ms,p99 仍在 50 ms 附近,p99.9 落在长尾上
|
||||
RawChunk chunk = ChunkFactory.Echo(
|
||||
s_agent, "tcp_echo", T0, 1000, 10,
|
||||
i => new EchoPacket(false, false, 25_000, i % 200 == 0 && i > 0 ? 975_000 : 25_000, ProcessUs: 0), durationMs: 20_000);
|
||||
var rtt = (HistogramAcc)EvaluateOneBucket(chunk, "tcp.rtt").Items["tcp.rtt"];
|
||||
MetricDef def = TcpMetrics.Rtt;
|
||||
|
||||
double? p999 = MetricDef.Export(rtt, def.Exports.Single(e => e.Stat == Stat.P999), 20);
|
||||
double? tail = MetricDef.Export(rtt, def.Exports.Single(e => e.Stat == Stat.Tail), 20);
|
||||
double? max = MetricDef.Export(rtt, def.Exports.Single(e => e.Stat == Stat.Max), 20);
|
||||
Assert.InRange(p999!.Value, 500, 1100);
|
||||
Assert.InRange(max!.Value, 999, 1001);
|
||||
Assert.InRange(tail!.Value, 0.9, 1.2); // p99 / p50:偶发的尖峰不改变 p99,所以长尾比接近 1,真正的长尾由 p99.9 和 max 暴露
|
||||
Assert.Equal(Unit.None, def.Exports.Single(e => e.Stat == Stat.Tail).Unit);
|
||||
Assert.Equal("tcp.rtt.p999", def.Exports.Single(e => e.Stat == Stat.P999).Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Retrans_UsesKernelCounterDeltas()
|
||||
{
|
||||
long baseUs = ChunkFactory.ProbeUs(0);
|
||||
RawStream info = ChunkFactory.TcpInfo(
|
||||
[
|
||||
(baseUs, 0, 40_000u, 0u, 100u),
|
||||
(baseUs + 1_000_000, 0, 40_000u, 2u, 300u),
|
||||
(baseUs + 2_000_000, 0, 40_000u, 2u, 500u),
|
||||
]);
|
||||
RawChunk chunk = ChunkFactory.Echo(s_agent, "tcp_echo", T0, 10, 100, _ => new EchoPacket(false, false, 20_000, 20_000), extraStreams: [info]);
|
||||
var retrans = (RatioAcc)EvaluateOneBucket(chunk, "tcp.retrans").Items["tcp.retrans"];
|
||||
Assert.Equal(2UL, retrans.Numerator);
|
||||
Assert.Equal(400UL, retrans.Denominator);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Retrans_CounterResetNeverGoesNegative()
|
||||
{
|
||||
long baseUs = ChunkFactory.ProbeUs(0);
|
||||
RawStream info = ChunkFactory.TcpInfo([(baseUs, 0, 1u, 10u, 100u), (baseUs + 1_000_000, 0, 1u, 0u, 50u), (baseUs + 2_000_000, 0, 1u, 1u, 150u)]);
|
||||
RawChunk chunk = ChunkFactory.Echo(s_agent, "tcp_echo", T0, 10, 100, _ => new EchoPacket(false, false, 20_000, 20_000), extraStreams: [info]);
|
||||
var retrans = (RatioAcc)EvaluateOneBucket(chunk, "tcp.retrans").Items["tcp.retrans"];
|
||||
Assert.Equal(1UL, retrans.Numerator);
|
||||
Assert.Equal(100UL, retrans.Denominator);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Connect_Outcomes_AreCountedByClass()
|
||||
{
|
||||
RawChunk chunk = ChunkFactory.Connect(
|
||||
s_agent, T0,
|
||||
[
|
||||
(30_000, 80_000, ConnectOutcomeCode.Ok),
|
||||
(31_000, 90_000, ConnectOutcomeCode.Ok),
|
||||
(0, null, ConnectOutcomeCode.Timeout),
|
||||
(30_000, null, ConnectOutcomeCode.Reset),
|
||||
]);
|
||||
MetricSet set = EvaluateOneBucket(chunk);
|
||||
Assert.Equal(50.0, ((RatioAcc)set.Items["connect.success"]).Percent);
|
||||
Assert.Equal(1UL, ((CounterAcc)set.Items["connect.fail.timeout"]).Value);
|
||||
Assert.Equal(1UL, ((CounterAcc)set.Items["connect.fail.rst"]).Value);
|
||||
Assert.False(set.Items.ContainsKey("connect.fail.tls"));
|
||||
Assert.Equal(3UL, ((HistogramAcc)set.Items["connect.tcp_ms"]).Count);
|
||||
Assert.Equal(2UL, ((HistogramAcc)set.Items["connect.tls_ms"]).Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evaluate_AssignsObservationsToTheirOwnBuckets()
|
||||
{
|
||||
// 10 秒的块,按 1 秒分桶:每个桶 10 个包
|
||||
RawChunk chunk = ChunkFactory.UdpEchoClean(s_agent, T0);
|
||||
var buckets = new SortedDictionary<long, MetricSet>();
|
||||
WindowEvaluator.Default.Evaluate(chunk, 1000, buckets, new HashSet<string> { "n.udp_echo" });
|
||||
Assert.Equal(10, buckets.Count);
|
||||
Assert.All(buckets.Values, set => Assert.Equal(10UL, ((CounterAcc)set.Items["n.udp_echo"]).Value));
|
||||
Assert.Equal(T0, buckets.Keys.First());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evaluate_ChunkOfAnotherKind_ProducesNothingForUnrelatedMetrics()
|
||||
{
|
||||
RawChunk chunk = ChunkFactory.UdpEchoClean(s_agent, T0);
|
||||
MetricSet set = EvaluateOneBucket(chunk);
|
||||
Assert.False(set.Items.ContainsKey("tcp.rtt.p95"));
|
||||
Assert.False(set.Items.ContainsKey("tcp.rtt"));
|
||||
Assert.True(set.Items.ContainsKey("rtt"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ChangePoint_FindsTheShift_AndIgnoresNoise()
|
||||
{
|
||||
double[] shifted = [10, 10, 11, 10, 9, 10, 4, 4, 5, 4, 4, 3];
|
||||
Assert.Equal(6, ChangePoint.Detect(shifted, minShift: 3));
|
||||
double[] flat = [10, 10.2, 9.8, 10.1, 9.9, 10, 10.1, 9.9];
|
||||
Assert.Equal(-1, ChangePoint.Detect(flat, minShift: 3));
|
||||
Assert.Equal(-1, ChangePoint.Detect([1, 2], minShift: 1));
|
||||
Assert.Equal(2, ChangePoint.FirstJump([0, 1, 20, 50], minJump: 5));
|
||||
Assert.Equal(-1, ChangePoint.FirstJump([0, 1, 2, 3], minJump: 5));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClockDriftFit_RemovesLinearDrift()
|
||||
{
|
||||
double[] t = Enumerable.Range(0, 50).Select(i => i * 100_000.0).ToArray();
|
||||
double[] delay = t.Select(x => 5_000_000 + (x * 50e-6) + 20_000).ToArray(); // 偏移 + 50 ppm 漂移 + 固定时延
|
||||
double[] jitter = ClockDriftFit.Jitter(t, delay)!;
|
||||
Assert.All(jitter, j => Assert.True(j < 1));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user