refactor!: rename the project from Monitor to MoShiro [CI]
ci/woodpecker/push/main-amd64-build Pipeline was successful
ci/woodpecker/push/main-amd64-build Pipeline was successful
Namespaces, assemblies, solution, binaries, images, systemd units, config paths, environment variables, protocol methods and the Linux user are all renamed. Breaking: existing databases, node identities and installed units must be recreated.
This commit is contained in:
1 parent
27ac7fb787
commit
2e146d4da7
309 files changed
+1565
-1572
No files matched your search
@@ -0,0 +1,253 @@
|
||||
using System.Text.Json;
|
||||
using MoShiro.Protocol;
|
||||
|
||||
namespace MoShiro.Backend.Analysis;
|
||||
|
||||
public sealed class TestStepResult
|
||||
{
|
||||
public int Index { get; set; }
|
||||
|
||||
public double TargetMbps { get; set; }
|
||||
|
||||
public double LossPct { get; set; }
|
||||
|
||||
public double? JitterP95Ms { get; set; }
|
||||
|
||||
public double DeliveredMbps { get; set; }
|
||||
}
|
||||
|
||||
public sealed class TestResult
|
||||
{
|
||||
/// <summary>导出值:与持续指标同一套 id,另有 <c>hy2.*</c> 等测试专用的值。</summary>
|
||||
public Dictionary<string, double> Metrics { get; } = new(StringComparer.Ordinal);
|
||||
|
||||
/// <summary>逐步或逐秒的曲线,例如码率–丢包曲线。</summary>
|
||||
public Dictionary<string, double[]> Series { get; } = new(StringComparer.Ordinal);
|
||||
|
||||
public List<TestStepResult> Steps { get; } = [];
|
||||
|
||||
public List<string> Caveats { get; } = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试结果的计算:所有阶段的块到达之后,按方案算出指标和曲线。
|
||||
/// udp_train 的各个阶段作为码率阶梯:可持续码率、有效吞吐、限速开始的位置。
|
||||
/// </summary>
|
||||
public static class TestEvaluator
|
||||
{
|
||||
public const double SustainableLossPct = 5;
|
||||
public const double SustainableLossLoosePct = 10;
|
||||
public const double QosShiftPct = 5;
|
||||
|
||||
public static TestResult Evaluate(string plan, IReadOnlyList<RawChunk> chunks)
|
||||
{
|
||||
var result = new TestResult();
|
||||
EvaluateMetrics(chunks, result);
|
||||
if (chunks.Any(c => c.Header.Kind == Kinds.UdpTrain.Id))
|
||||
{
|
||||
EvaluateLadder(chunks, result);
|
||||
}
|
||||
|
||||
if (chunks.Any(c => c.Header.Partial))
|
||||
{
|
||||
result.Caveats.Add("partial_data");
|
||||
}
|
||||
|
||||
if (chunks.Any(c => c.Header.Port is not (0 or 443)))
|
||||
{
|
||||
result.Caveats.Add("port_differs_from_service");
|
||||
}
|
||||
|
||||
_ = plan;
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void EvaluateMetrics(IReadOnlyList<RawChunk> chunks, TestResult result)
|
||||
{
|
||||
var buckets = new SortedDictionary<long, MetricSet>();
|
||||
long span = 24L * 3600 * 1000;
|
||||
foreach (RawChunk chunk in chunks)
|
||||
{
|
||||
WindowEvaluator.Default.Evaluate(chunk, span, buckets);
|
||||
}
|
||||
|
||||
var merged = new MetricSet();
|
||||
foreach (MetricSet set in buckets.Values)
|
||||
{
|
||||
merged.Merge(set);
|
||||
}
|
||||
|
||||
double seconds = chunks.Sum(c => c.Header.DurationMs) / 1000.0;
|
||||
foreach ((MetricDef def, MetricExport export) in MetricRegistry.AllExports())
|
||||
{
|
||||
if (merged.TryGet(def.Id, out Accumulator acc) && MetricDef.Export(acc, export, seconds) is { } value)
|
||||
{
|
||||
result.Metrics[export.Id] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void EvaluateLadder(IReadOnlyList<RawChunk> chunks, TestResult result)
|
||||
{
|
||||
var steps = new List<TestStepResult>();
|
||||
var perSecond = new SortedDictionary<long, double>();
|
||||
int index = 0;
|
||||
|
||||
// 一个阶段(一档码率)可能被切成好几个块(一个块最多 50000 条记录,100 Mbit/s 的 10 秒要切 3~4 块):
|
||||
// 阶梯里的一步是一个阶段,不是一个块——同一阶段的块先合并,再算丢包、抖动和实际吞吐。
|
||||
foreach (IGrouping<int, RawChunk> phase in chunks.Where(c => c.Header.Kind == Kinds.UdpTrain.Id).GroupBy(c => c.Header.Phase ?? 0).OrderBy(g => g.Key))
|
||||
{
|
||||
RawChunk[] parts = [.. phase.Where(c => c.Find("pkt") is not null).OrderBy(c => c.Header.StartUtcMs)];
|
||||
if (parts.Length == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var merged = new MetricSet();
|
||||
long bytes = 0;
|
||||
foreach (RawChunk chunk in parts)
|
||||
{
|
||||
var buckets = new SortedDictionary<long, MetricSet>();
|
||||
WindowEvaluator.Default.Evaluate(chunk, 24L * 3600 * 1000, buckets, new HashSet<string> { "train.loss", "train.jitter", "train.rate" });
|
||||
foreach (MetricSet set in buckets.Values)
|
||||
{
|
||||
merged.Merge(set);
|
||||
}
|
||||
|
||||
bytes += ReceivedBytes(chunk.Find("pkt")!);
|
||||
|
||||
var seconds = new SortedDictionary<long, MetricSet>();
|
||||
WindowEvaluator.Default.Evaluate(chunk, 1000, seconds, new HashSet<string> { "train.rate" });
|
||||
foreach ((long start, MetricSet set) in seconds)
|
||||
{
|
||||
if (set.TryGet("train.rate", out Accumulator rate) && rate is HistogramAcc rh && rh.Mean is { } mean)
|
||||
{
|
||||
perSecond[start] = mean;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var step = new TestStepResult
|
||||
{
|
||||
Index = index++,
|
||||
TargetMbps = TargetMbps(parts[0].Header),
|
||||
};
|
||||
if (merged.TryGet("train.loss", out Accumulator loss) && loss is RatioAcc ratio)
|
||||
{
|
||||
step.LossPct = ratio.Percent ?? 0;
|
||||
}
|
||||
|
||||
if (merged.TryGet("train.jitter", out Accumulator jitter) && jitter is HistogramAcc jh)
|
||||
{
|
||||
step.JitterP95Ms = jh.Quantile(0.95);
|
||||
}
|
||||
|
||||
// 阶段的时长:从发送时间戳算(块头里的时长是 Probe 填的,每个块可能都带着整个阶段的时长,不能相加也不能取并集)
|
||||
double spanSeconds = SendSpanSeconds(parts) ?? ((parts.Max(c => c.Header.StartUtcMs + c.Header.DurationMs) - parts.Min(c => c.Header.StartUtcMs)) / 1000.0);
|
||||
step.DeliveredMbps = spanSeconds > 0 ? bytes * 8 / spanSeconds / 1_000_000 : 0;
|
||||
steps.Add(step);
|
||||
}
|
||||
|
||||
if (steps.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
result.Steps.AddRange(steps);
|
||||
result.Series["step_mbps"] = [.. steps.Select(s => s.TargetMbps)];
|
||||
result.Series["loss_pct"] = [.. steps.Select(s => s.LossPct)];
|
||||
result.Series["delivered_mbps"] = [.. steps.Select(s => s.DeliveredMbps)];
|
||||
result.Series["jitter_p95_ms"] = [.. steps.Select(s => s.JitterP95Ms ?? double.NaN)];
|
||||
|
||||
TestStepResult? sustainable = steps.Where(s => s.LossPct <= SustainableLossPct).MaxBy(s => s.TargetMbps);
|
||||
TestStepResult? loose = steps.Where(s => s.LossPct <= SustainableLossLoosePct).MaxBy(s => s.TargetMbps);
|
||||
result.Metrics["hy2.sustainable_mbps"] = sustainable?.TargetMbps ?? 0;
|
||||
result.Metrics["hy2.sustainable10_mbps"] = loose?.TargetMbps ?? 0;
|
||||
result.Metrics["hy2.effective_mbps"] = steps.Max(s => s.DeliveredMbps);
|
||||
|
||||
// 限速开始:丢包率随码率阶梯出现显著跳变的那一档。
|
||||
int onset = ChangePoint.FirstJump([.. steps.Select(s => s.LossPct)], QosShiftPct);
|
||||
if (onset >= 0)
|
||||
{
|
||||
result.Metrics["hy2.qos_onset_mbps"] = steps[onset].TargetMbps;
|
||||
}
|
||||
|
||||
if (perSecond.Count >= 6)
|
||||
{
|
||||
double[] rates = [.. perSecond.Values];
|
||||
double peak = rates.Max();
|
||||
int second = ChangePoint.Detect(rates, Math.Max(1, peak * 0.2));
|
||||
if (second >= 0 && rates[second..].Average() < rates[..second].Average())
|
||||
{
|
||||
result.Metrics["hy2.qos_onset_s"] = second;
|
||||
}
|
||||
|
||||
result.Series["delivered_mbps_per_s"] = rates;
|
||||
}
|
||||
}
|
||||
|
||||
private static double TargetMbps(RawChunkHeader header) =>
|
||||
header.Params.TryGetValue("mbps", out JsonElement v) && v.ValueKind == JsonValueKind.Number ? v.GetDouble() : 0;
|
||||
|
||||
/// <summary>
|
||||
/// 发送端的时间戳(Agent 时钟)在 UTC 上的跨度:所有块里第一个包到最后一个包,再补上一个平均间隔(n 个包有 n−1 个间隔)。
|
||||
/// 没有时间戳就返回 null。
|
||||
/// </summary>
|
||||
private static double? SendSpanSeconds(RawChunk[] parts)
|
||||
{
|
||||
long min = long.MaxValue;
|
||||
long max = long.MinValue;
|
||||
long count = 0;
|
||||
foreach (RawChunk chunk in parts)
|
||||
{
|
||||
RawStream? pkt = chunk.Find("pkt");
|
||||
RawColumn? send = pkt?.Find("t_send");
|
||||
if (pkt is null || send is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
ChunkClock clock = chunk.Header.Clock;
|
||||
for (int i = 0; i < pkt.Count; i++)
|
||||
{
|
||||
if (!send.IsValid(i))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
long utcUs = (clock.UtcMs * 1000) + (send.Values[i] - clock.AgentMonoUs);
|
||||
min = Math.Min(min, utcUs);
|
||||
max = Math.Max(max, utcUs);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
if (count < 2 || max <= min)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return (max - min) / 1_000_000.0 * count / (count - 1);
|
||||
}
|
||||
|
||||
private static long ReceivedBytes(RawStream pkt)
|
||||
{
|
||||
RawColumn? recv = pkt.Find("t_recv");
|
||||
RawColumn? size = pkt.Find("size");
|
||||
if (recv is null || size is null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
long bytes = 0;
|
||||
for (int i = 0; i < pkt.Count; i++)
|
||||
{
|
||||
if (recv.IsValid(i))
|
||||
{
|
||||
bytes += size.Values[i];
|
||||
}
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user