backend: serve single-agent task templates with their status and results, rename the kinds in storage
This commit is contained in:
1 parent
e77880d372
commit
3a9ea46d6f
45 files changed
+763
-216
No files matched your search
@@ -31,7 +31,7 @@ public sealed class TestResult
|
||||
|
||||
/// <summary>
|
||||
/// 测试结果的计算:所有阶段的块到达之后,按方案算出指标和曲线。
|
||||
/// udp_train 的各个阶段作为码率阶梯:可持续码率、有效吞吐、限速开始的位置。
|
||||
/// quic_stream 的各个阶段作为码率阶梯:可持续码率、有效吞吐、限速开始的位置。
|
||||
/// </summary>
|
||||
public static class TestEvaluator
|
||||
{
|
||||
@@ -43,7 +43,7 @@ public static class TestEvaluator
|
||||
{
|
||||
var result = new TestResult();
|
||||
EvaluateMetrics(chunks, result);
|
||||
if (chunks.Any(c => c.Header.Kind == Kinds.UdpTrain.Id))
|
||||
if (chunks.Any(c => c.Header.Kind == Kinds.QuicStream.Id))
|
||||
{
|
||||
EvaluateLadder(chunks, result);
|
||||
}
|
||||
@@ -95,7 +95,7 @@ public static class TestEvaluator
|
||||
|
||||
// 一个阶段(一档码率)可能被切成好几个块(一个块最多 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))
|
||||
foreach (IGrouping<int, RawChunk> phase in chunks.Where(c => c.Header.Kind == Kinds.QuicStream.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)
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
namespace MoShiro.Backend.Analysis;
|
||||
|
||||
/// <summary>http 任务(单个 Agent 请求一个 URL)的指标。耗时只统计成功的请求。</summary>
|
||||
public static class HttpMetrics
|
||||
{
|
||||
private static readonly string[] s_req = ["http_request.req"];
|
||||
|
||||
public static readonly MetricDef Success = new(
|
||||
"http.success", AccKind.Ratio, s_req, Ops.HttpSuccess, "HTTP success", "Share of requests that got an answer with an accepted status.", Better.Higher,
|
||||
[new MetricExport("http.success", Stat.Pct, Unit.Pct, 99, 90)]);
|
||||
|
||||
public static readonly MetricDef Dns = Metric.Histogram(
|
||||
"http.dns_ms", s_req, Ops.HttpPhaseMs("dns_us"), "DNS time", "Time to resolve the target host name.", Unit.Ms, [Stat.P95], good: 50, poor: 300);
|
||||
|
||||
public static readonly MetricDef Tcp = Metric.Histogram(
|
||||
"http.tcp_ms", s_req, Ops.HttpPhaseMs("tcp_us"), "TCP connect time", "Time to establish the TCP connection (HTTP/1.1 and HTTP/2).", Unit.Ms, [Stat.P95], good: 100, poor: 400);
|
||||
|
||||
public static readonly MetricDef Ttfb = Metric.Histogram(
|
||||
"http.ttfb_ms", s_req, Ops.HttpPhaseMs("ttfb_us"), "Time to first byte", "Time from starting the request until the response headers arrive, including connection setup.", Unit.Ms,
|
||||
[Stat.P50, Stat.P95, Stat.P99], good: 300, poor: 1000);
|
||||
|
||||
public static readonly MetricDef Total = Metric.Histogram(
|
||||
"http.total_ms", s_req, Ops.HttpPhaseMs("total_us"), "Total time", "Time until the response body was read.", Unit.Ms, [Stat.P50, Stat.P95, Stat.P99], good: 500, poor: 2000);
|
||||
|
||||
public static readonly MetricDef FailTimeout = Metric.Counter(
|
||||
"http.fail.timeout", s_req, Ops.HttpOutcomeCount(HttpOutcome.Timeout), "HTTP timeouts", "Requests that timed out.", new MetricExport("http.fail.timeout", Stat.Count, Unit.Count));
|
||||
|
||||
public static readonly MetricDef FailDns = Metric.Counter(
|
||||
"http.fail.dns", s_req, Ops.HttpOutcomeCount(HttpOutcome.Dns), "DNS failures", "Requests whose host name did not resolve.", new MetricExport("http.fail.dns", Stat.Count, Unit.Count));
|
||||
|
||||
public static readonly MetricDef FailReset = Metric.Counter(
|
||||
"http.fail.rst", s_req, Ops.HttpOutcomeCount(HttpOutcome.Reset), "HTTP resets", "Requests answered with a reset or refused.", new MetricExport("http.fail.rst", Stat.Count, Unit.Count));
|
||||
|
||||
public static readonly MetricDef FailTls = Metric.Counter(
|
||||
"http.fail.tls", s_req, Ops.HttpOutcomeCount(HttpOutcome.Tls), "TLS failures", "Requests that ended in a TLS error.", new MetricExport("http.fail.tls", Stat.Count, Unit.Count));
|
||||
|
||||
public static readonly MetricDef FailStatus = Metric.Counter(
|
||||
"http.fail.status", s_req, Ops.HttpOutcomeCount(HttpOutcome.BadStatus), "Bad status", "Requests that got an answer with an unaccepted status.", new MetricExport("http.fail.status", Stat.Count, Unit.Count));
|
||||
|
||||
public static readonly MetricDef FailUnsupported = Metric.Counter(
|
||||
"http.fail.unsupported", s_req, Ops.HttpOutcomeCount(HttpOutcome.Unsupported), "Unsupported version", "Requests that could not use the requested HTTP version (for HTTP/3: no QUIC library, or the server does not offer it).", new MetricExport("http.fail.unsupported", Stat.Count, Unit.Count));
|
||||
|
||||
public static readonly MetricDef FailOther = Metric.Counter(
|
||||
"http.fail.other", s_req, Ops.HttpOutcomeCount(HttpOutcome.Other), "Other failures", "Requests that failed for a reason not listed separately.", new MetricExport("http.fail.other", Stat.Count, Unit.Count));
|
||||
|
||||
public static readonly MetricDef Samples = Metric.Counter(
|
||||
"n.http", s_req, Ops.RowCount, "http samples", "Number of http requests.", new MetricExport("n.http", Stat.Count, Unit.Count), Better.None);
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
namespace MoShiro.Backend.Analysis;
|
||||
|
||||
/// <summary>线路基础指标(udp_echo / icmp_echo 的回显流)。新增指标 = 加一行声明。</summary>
|
||||
/// <summary>线路基础指标(quic_ping / icmp_ping 的回显流)。新增指标 = 加一行声明。</summary>
|
||||
public static class LinkMetrics
|
||||
{
|
||||
private static readonly string[] s_udpMsg = ["udp_echo.msg"];
|
||||
private static readonly string[] s_udpOrIcmpMsg = ["udp_echo.msg", "icmp_echo.msg"];
|
||||
private static readonly string[] s_udpMsg = ["quic_ping.msg"];
|
||||
private static readonly string[] s_udpOrIcmpMsg = ["quic_ping.msg", "icmp_ping.msg"];
|
||||
|
||||
public static readonly MetricDef Rtt = Metric.Histogram(
|
||||
"rtt", s_udpOrIcmpMsg, Ops.Rtt, "RTT", "Round-trip time with the Agent processing time removed.", Unit.Ms,
|
||||
@@ -45,5 +45,5 @@ public static class LinkMetrics
|
||||
"dup", s_udpMsg, Ops.Dup, "Duplicates", "Replies received more than once.", new MetricExport("dup", Stat.Count, Unit.Count));
|
||||
|
||||
public static readonly MetricDef SamplesUdp = Metric.Counter(
|
||||
"n.udp_echo", s_udpMsg, Ops.RowCount, "udp_echo samples", "Number of udp_echo records.", new MetricExport("n.udp_echo", Stat.Count, Unit.Count), Better.None);
|
||||
"n.quic_ping", s_udpMsg, Ops.RowCount, "quic_ping samples", "Number of quic_ping records.", new MetricExport("n.quic_ping", Stat.Count, Unit.Count), Better.None);
|
||||
}
|
||||
@@ -15,6 +15,8 @@ public static class MetricRegistry
|
||||
TcpMetrics.Rtt, TcpMetrics.Slow, TcpMetrics.Stall, TcpMetrics.StallMax, TcpMetrics.Retrans, TcpMetrics.RetransUp, TcpMetrics.RetransDown, TcpMetrics.Srtt, TcpMetrics.BulkRate, TcpMetrics.BulkPeak, TcpMetrics.SamplesTcp,
|
||||
SessionMetrics.ConnectSuccess, SessionMetrics.ConnectTcp, SessionMetrics.ConnectTls,
|
||||
SessionMetrics.FailTimeout, SessionMetrics.FailReset, SessionMetrics.FailTls,
|
||||
HttpMetrics.Success, HttpMetrics.Dns, HttpMetrics.Tcp, HttpMetrics.Ttfb, HttpMetrics.Total,
|
||||
HttpMetrics.FailTimeout, HttpMetrics.FailDns, HttpMetrics.FailReset, HttpMetrics.FailTls, HttpMetrics.FailStatus, HttpMetrics.FailUnsupported, HttpMetrics.FailOther, HttpMetrics.Samples,
|
||||
SessionMetrics.HoldResets, SessionMetrics.HoldLifetime,
|
||||
SessionMetrics.TrainLoss, SessionMetrics.TrainJitter, SessionMetrics.TrainRate, SessionMetrics.SamplesConnect,
|
||||
];
|
||||
@@ -26,6 +28,7 @@ public static class MetricRegistry
|
||||
new("udp", "QUIC Datagram (legacy alias)", ["loss.up", "loss.down", "loss.total", "rtt.p50", "rtt.p95", "rtt.p99", "jitter.up.p95", "jitter.down.p95", "loss.run_max", "reorder", "dup"]),
|
||||
new("tcp", "TCP", ["tcp.rtt.p50", "tcp.rtt.p95", "tcp.rtt.p99", "tcp.rtt.p999", "tcp.rtt.max", "tcp.rtt.tail", "tcp.slow", "tcp.stall.per_min", "tcp.stall_max", "tcp.retrans", "tcp.retrans.up", "tcp.retrans.down", "tcp.srtt.p95"]),
|
||||
new("connect", "Connect", ["connect.success", "connect.tcp_ms.p95", "connect.tls_ms.p95", "connect.fail.timeout", "connect.fail.rst", "connect.fail.tls"]),
|
||||
new("http", "HTTP", ["http.success", "http.dns_ms.p95", "http.tcp_ms.p95", "http.ttfb_ms.p50", "http.ttfb_ms.p95", "http.ttfb_ms.p99", "http.total_ms.p50", "http.total_ms.p95", "http.total_ms.p99", "http.fail.timeout", "http.fail.dns", "http.fail.rst", "http.fail.tls", "http.fail.status", "http.fail.unsupported", "http.fail.other"]),
|
||||
new("session", "Session", ["hold.resets.per_h", "hold.lifetime_max"]),
|
||||
new("scores", "Scores", ["score.base", "score.mc", "score.hy2", "score.reality"]),
|
||||
];
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
namespace MoShiro.Backend.Analysis;
|
||||
|
||||
/// <summary>TCP 建连、长连接、QUIC Datagram 压测(legacy kind udp_train)的指标。</summary>
|
||||
/// <summary>TCP 建连、长连接、QUIC Datagram 压测(legacy kind quic_stream)的指标。</summary>
|
||||
public static class SessionMetrics
|
||||
{
|
||||
private static readonly string[] s_conn = ["connect.conn"];
|
||||
private static readonly string[] s_hold = ["hold.hold"];
|
||||
private static readonly string[] s_train = ["udp_train.pkt"];
|
||||
private static readonly string[] s_conn = ["tcp_connect.conn"];
|
||||
private static readonly string[] s_hold = ["tls_hold.hold"];
|
||||
private static readonly string[] s_train = ["quic_stream.pkt"];
|
||||
|
||||
public static readonly MetricDef ConnectSuccess = new(
|
||||
"connect.success", AccKind.Ratio, s_conn, Ops.OutcomeRatio(ConnectOutcome.Ok), "Connect success", "Share of connection attempts that completed.", Better.Higher,
|
||||
|
||||
@@ -3,9 +3,9 @@ namespace MoShiro.Backend.Analysis;
|
||||
/// <summary>TCP 指标:回显的 RTT 与卡顿、内核采样的重传、批量传输的吞吐。</summary>
|
||||
public static class TcpMetrics
|
||||
{
|
||||
private static readonly string[] s_tcpMsg = ["tcp_echo.msg"];
|
||||
private static readonly string[] s_tcpInfo = ["tcp_echo.tcp_info", "tcp_bulk.tcp_info"];
|
||||
private static readonly string[] s_bulkBytes = ["tcp_bulk.bytes"];
|
||||
private static readonly string[] s_tcpMsg = ["tcp_ping.msg"];
|
||||
private static readonly string[] s_tcpInfo = ["tcp_ping.tcp_info", "tcp_transfer.tcp_info"];
|
||||
private static readonly string[] s_bulkBytes = ["tcp_transfer.bytes"];
|
||||
|
||||
public static readonly MetricDef Rtt = Metric.Histogram(
|
||||
"tcp.rtt", s_tcpMsg, Ops.Rtt, "TCP RTT", "Application-level echo round trip over TCP (includes retransmission delay).", Unit.Ms,
|
||||
@@ -47,5 +47,5 @@ public static class TcpMetrics
|
||||
[Stat.Max], Better.Higher);
|
||||
|
||||
public static readonly MetricDef SamplesTcp = Metric.Counter(
|
||||
"n.tcp_echo", s_tcpMsg, Ops.RowCount, "tcp_echo samples", "Number of tcp_echo records.", new MetricExport("n.tcp_echo", Stat.Count, Unit.Count), Better.None);
|
||||
"n.tcp_ping", s_tcpMsg, Ops.RowCount, "tcp_ping samples", "Number of tcp_ping records.", new MetricExport("n.tcp_ping", Stat.Count, Unit.Count), Better.None);
|
||||
}
|
||||
@@ -32,7 +32,7 @@ public sealed class BaseModel : ServiceModelBase
|
||||
|
||||
/// <summary>
|
||||
/// Minecraft:TCP 上不会丢包,丢包表现为重传和卡顿(队头阻塞)。
|
||||
/// 没有 tcp_echo 数据时,用 udp_echo 的丢包与 RTT 代入简化的 TCP 恢复模型估算,并标记为 estimated。
|
||||
/// 没有 tcp_ping 数据时,用 quic_ping 的丢包与 RTT 代入简化的 TCP 恢复模型估算,并标记为 estimated。
|
||||
/// </summary>
|
||||
public sealed class McModel : ServiceModelBase
|
||||
{
|
||||
@@ -99,7 +99,7 @@ public sealed class McModel : ServiceModelBase
|
||||
|
||||
/// <summary>
|
||||
/// Hysteria2(Brutal 固定码率):有 hy2_ladder 测试结果时,用可持续码率、有效吞吐和限速开始的位置评分;
|
||||
/// 没有测试时,只能用持续的 udp_echo 趋势粗略估算,并标记为 estimated。
|
||||
/// 没有测试时,只能用持续的 quic_ping 趋势粗略估算,并标记为 estimated。
|
||||
/// </summary>
|
||||
public sealed class Hy2Model : ServiceModelBase
|
||||
{
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
using MoShiro.Protocol;
|
||||
|
||||
namespace MoShiro.Backend.Analysis;
|
||||
|
||||
/// <summary>http 请求的结果,编码与 Agent 的 <c>HttpOutcomes</c> 一致。</summary>
|
||||
public enum HttpOutcome
|
||||
{
|
||||
Ok = 0,
|
||||
Timeout = 1,
|
||||
Reset = 2,
|
||||
Tls = 3,
|
||||
Other = 4,
|
||||
Dns = 5,
|
||||
BadStatus = 6,
|
||||
Unsupported = 7,
|
||||
NotAllowed = 8,
|
||||
}
|
||||
|
||||
/// <summary>http 流(req)的公共读法:权重 <c>w</c> 是这一行代表的网格间隔数(自适应降频后每次请求代表更长的时间)。</summary>
|
||||
internal readonly struct HttpColumns
|
||||
{
|
||||
public HttpColumns(RawStream stream)
|
||||
{
|
||||
Stream = stream;
|
||||
Ts = stream.Find("ts");
|
||||
Outcome = stream.Find("outcome");
|
||||
Weights = stream.Find("w");
|
||||
}
|
||||
|
||||
public RawStream Stream { get; }
|
||||
|
||||
public RawColumn? Ts { get; }
|
||||
|
||||
public RawColumn? Outcome { get; }
|
||||
|
||||
private RawColumn? Weights { get; }
|
||||
|
||||
public bool Has => Ts is not null && Outcome is not null;
|
||||
|
||||
public int Weight(int row) => Weights is not null && Weights.IsValid(row) ? (int)Math.Clamp(Weights.Values[row], 1, 65535) : 1;
|
||||
}
|
||||
|
||||
/// <summary>成功率(分子是按权重累计的成功次数)或某种结果的次数。</summary>
|
||||
internal sealed class HttpOutcomeOp : MetricOp
|
||||
{
|
||||
private readonly HttpOutcome _code;
|
||||
private readonly bool _asRatio;
|
||||
|
||||
public HttpOutcomeOp(HttpOutcome code, bool asRatio)
|
||||
{
|
||||
_code = code;
|
||||
_asRatio = asRatio;
|
||||
}
|
||||
|
||||
public override string Signature => $"http_outcome/{_code}/{(_asRatio ? "ratio" : "count")}/1";
|
||||
|
||||
public override void Evaluate(ChunkView view, RawStream stream, IObservationSink sink)
|
||||
{
|
||||
var c = new HttpColumns(stream);
|
||||
if (!c.Has)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < stream.Count; i++)
|
||||
{
|
||||
bool hit = c.Outcome!.Values[i] == (long)_code;
|
||||
long at = view.ColumnToUtcMs(c.Ts!, c.Ts!.Values[i]);
|
||||
if (_asRatio)
|
||||
{
|
||||
int w = c.Weight(i);
|
||||
sink.Emit(at, hit ? w : 0, w);
|
||||
}
|
||||
else if (hit)
|
||||
{
|
||||
sink.Emit(at, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>一个阶段的耗时(毫秒):只统计成功的请求,按权重累计,所以分位数是按时间平均的。</summary>
|
||||
internal sealed class HttpPhaseMsOp : MetricOp
|
||||
{
|
||||
private const int MaxRepeat = 256;
|
||||
|
||||
private readonly string _column;
|
||||
|
||||
public HttpPhaseMsOp(string column)
|
||||
{
|
||||
_column = column;
|
||||
}
|
||||
|
||||
public override string Signature => $"http_phase_ms/{_column}/1";
|
||||
|
||||
public override void Evaluate(ChunkView view, RawStream stream, IObservationSink sink)
|
||||
{
|
||||
var c = new HttpColumns(stream);
|
||||
RawColumn? column = stream.Find(_column);
|
||||
if (!c.Has || column is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < stream.Count; i++)
|
||||
{
|
||||
if (c.Outcome!.Values[i] != (long)HttpOutcome.Ok || !column.IsValid(i))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
long at = view.ColumnToUtcMs(c.Ts!, c.Ts!.Values[i]);
|
||||
for (int k = Math.Min(c.Weight(i), MaxRepeat); k > 0; k--)
|
||||
{
|
||||
sink.Emit(at, column.Values[i] / 1000.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,12 @@ public static class Ops
|
||||
|
||||
public static MetricOp OutcomeCount(ConnectOutcome code) => new OutcomeOp(code, asRatio: false);
|
||||
|
||||
public static MetricOp HttpSuccess { get; } = new HttpOutcomeOp(HttpOutcome.Ok, asRatio: true);
|
||||
|
||||
public static MetricOp HttpOutcomeCount(HttpOutcome code) => new HttpOutcomeOp(code, asRatio: false);
|
||||
|
||||
public static MetricOp HttpPhaseMs(string column) => new HttpPhaseMsOp(column);
|
||||
|
||||
public static MetricOp HoldEvents(HoldEvent holdEvent) => new HoldEventOp(holdEvent);
|
||||
|
||||
public static MetricOp HoldLifetime { get; } = new HoldLifetimeOp();
|
||||
|
||||
@@ -330,7 +330,7 @@ internal sealed class HoldLifetimeOp : MetricOp
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>单向 UDP 流(udp_train)的丢包:接收端没有记录到 t_recv 即为丢失。</summary>
|
||||
/// <summary>单向 UDP 流(quic_stream)的丢包:接收端没有记录到 t_recv 即为丢失。</summary>
|
||||
internal sealed class TrainLossOp : MetricOp
|
||||
{
|
||||
public override string Signature => "train_loss/1";
|
||||
|
||||
Reference in new issue
Block a user