Network 层的迁移与强化已完成,TODO.md 与 MobaSyncMVP.md 给出后续方向

This commit is contained in:
SepComet
2026-03-27 22:14:33 +08:00
parent e361510100
commit 156d72bf4a
57 changed files with 2933 additions and 102 deletions
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Runtime.InteropServices;
using kcp;
@@ -16,6 +17,9 @@ namespace Network.NetworkTransport
private IKCPCB* _kcp;
private bool _disposed;
private uint _nextUpdateAt;
private readonly Dictionary<uint, uint> _observedSegmentXmitBySequence = new();
private long _observedRetransmissionSends;
private long _observedLossSignals;
public KcpSession(KcpTransport owner, IPEndPoint remoteEndPoint, uint conv)
{
@@ -32,6 +36,7 @@ namespace Network.NetworkTransport
KCP.ikcp_setmtu(_kcp, DefaultMtu);
_nextUpdateAt = GetCurrentTimeMilliseconds();
_owner.RecordSessionDiagnostics(this, "active");
}
public uint Conv { get; }
@@ -68,6 +73,7 @@ namespace Network.NetworkTransport
LastActivityUtc = DateTime.UtcNow;
UpdateNoLock(GetCurrentTimeMilliseconds());
_owner.RecordSessionDiagnostics(this, "active");
}
}
@@ -100,6 +106,7 @@ namespace Network.NetworkTransport
LastActivityUtc = DateTime.UtcNow;
UpdateNoLock(GetCurrentTimeMilliseconds());
_owner.RecordSessionDiagnostics(this, "active");
}
}
@@ -139,6 +146,7 @@ namespace Network.NetworkTransport
}
LastActivityUtc = DateTime.UtcNow;
_owner.RecordSessionDiagnostics(this, "active");
return true;
}
}
@@ -158,6 +166,81 @@ namespace Network.NetworkTransport
}
UpdateNoLock(current);
_owner.RecordSessionDiagnostics(this, "active");
}
}
public TransportSessionDiagnosticsSnapshot CaptureDiagnostics(string lifecycleState)
{
lock (_gate)
{
var now = DateTimeOffset.UtcNow;
if (_disposed || _kcp == null)
{
return new TransportSessionDiagnosticsSnapshot
{
LifecycleState = lifecycleState,
ObservedAtUtc = now,
IdleMs = Math.Max(0L, (long)(now - LastActivityUtc).TotalMilliseconds)
};
}
var retransmittedSegmentsInFlight = 0;
var observedRetransmissionDelta = 0L;
var head = &_kcp->snd_buf;
for (var node = head->next; node != head; node = node->next)
{
var segment = (IKCPSEG*)node;
var currentXmit = Math.Max(0u, segment->xmit);
_observedSegmentXmitBySequence.TryGetValue(segment->sn, out var observedXmit);
var baselineXmit = observedXmit == 0 ? 1u : observedXmit;
if (currentXmit > baselineXmit)
{
observedRetransmissionDelta += currentXmit - baselineXmit;
}
if (currentXmit > observedXmit)
{
_observedSegmentXmitBySequence[segment->sn] = currentXmit;
}
if (currentXmit > 1)
{
retransmittedSegmentsInFlight++;
}
}
if (observedRetransmissionDelta > 0)
{
_observedRetransmissionSends += observedRetransmissionDelta;
_observedLossSignals += observedRetransmissionDelta;
}
return new TransportSessionDiagnosticsSnapshot
{
LifecycleState = lifecycleState,
ObservedAtUtc = now,
IdleMs = Math.Max(0L, (long)(now - LastActivityUtc).TotalMilliseconds),
KcpStateCode = unchecked((int)_kcp->state),
SmoothedRttMs = Math.Max(0, _kcp->rx_srtt),
RttVarianceMs = Math.Max(0, _kcp->rx_rttval),
RetransmissionTimeoutMs = Math.Max(0, _kcp->rx_rto),
LocalSendWindow = checked((int)_kcp->snd_wnd),
LocalReceiveWindow = checked((int)_kcp->rcv_wnd),
RemoteWindow = checked((int)_kcp->rmt_wnd),
CongestionWindow = checked((int)_kcp->cwnd),
WaitSendCount = Math.Max(0, KCP.ikcp_waitsnd(_kcp)),
SendQueueCount = checked((int)_kcp->nsnd_que),
SendBufferCount = checked((int)_kcp->nsnd_buf),
ReceiveQueueCount = checked((int)_kcp->nrcv_que),
ReceiveBufferCount = checked((int)_kcp->nrcv_buf),
DeadLinkThreshold = checked((int)_kcp->dead_link),
SegmentTransmitCount = _kcp->xmit,
RetransmittedSegmentsInFlight = retransmittedSegmentsInFlight,
ObservedRetransmissionSends = _observedRetransmissionSends,
ObservedLossSignals = _observedLossSignals
};
}
}
@@ -221,4 +304,4 @@ namespace Network.NetworkTransport
}
}
}
}
}
@@ -10,7 +10,7 @@ using kcp;
namespace Network.NetworkTransport
{
public partial class KcpTransport : ITransport
public partial class KcpTransport : ITransport, ITransportMetricsSink
{
private const uint DefaultConv = 1;
private const int DefaultNoDelay = 1;
@@ -69,6 +69,11 @@ namespace Network.NetworkTransport
return _metricsModule.GetCurrentSnapshot();
}
public void RecordApplicationSessionSnapshot(TransportApplicationSessionSnapshot snapshot)
{
_metricsModule.RecordApplicationSessionSnapshot(snapshot);
}
public Task StartAsync()
{
if (_isRunning)
@@ -318,6 +323,7 @@ namespace Network.NetworkTransport
foreach (var session in sessions)
{
_metricsModule.RecordSessionDiagnostics(session.RemoteEndPoint, session.CaptureDiagnostics("closed"));
session.Dispose();
_metricsModule.RecordSessionClosed(session.RemoteEndPoint);
}
@@ -358,5 +364,15 @@ namespace Network.NetworkTransport
{
_metricsModule.RecordError(stage, remoteEndPoint, detail);
}
private void RecordSessionDiagnostics(KcpSession session, string lifecycleState)
{
if (session == null)
{
return;
}
_metricsModule.RecordSessionDiagnostics(session.RemoteEndPoint, session.CaptureDiagnostics(lifecycleState));
}
}
}
}
@@ -0,0 +1,225 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Network.NetworkTransport
{
public static class TransportMetricsDiagnosisFormatter
{
public static string BuildChineseDiagnosis(TransportMetricsSnapshot snapshot)
{
if (snapshot == null)
{
throw new ArgumentNullException(nameof(snapshot));
}
var builder = new StringBuilder();
builder.AppendLine("Transport Metrics Diagnosis / 传输诊断结论");
builder.AppendLine();
builder.AppendLine("结论摘要");
builder.AppendLine($"- 传输实现:{snapshot.TransportName}");
builder.AppendLine($"- 运行模式:{TranslateMode(snapshot.Mode)}");
builder.AppendLine($"- 运行时长:{snapshot.DurationMs} 毫秒");
builder.AppendLine($"- 总体判断:{BuildOverallAssessment(snapshot)}");
builder.AppendLine();
builder.AppendLine("关键观察");
foreach (var finding in BuildFindings(snapshot))
{
builder.AppendLine($"- {finding}");
}
builder.AppendLine();
builder.AppendLine("会话状态");
foreach (var line in BuildSessionLines(snapshot))
{
builder.AppendLine($"- {line}");
}
builder.AppendLine();
builder.AppendLine("热点对端");
foreach (var peerLine in BuildPeerLines(snapshot))
{
builder.AppendLine($"- {peerLine}");
}
return builder.ToString();
}
private static IEnumerable<string> BuildFindings(TransportMetricsSnapshot snapshot)
{
yield return $"业务消息发送/接收={snapshot.PayloadMessagesSent}/{snapshot.PayloadMessagesReceived},数据报发送/接收={snapshot.DatagramsSent}/{snapshot.DatagramsReceived}。";
if (snapshot.ApplicationSessionsTracked > 0)
{
yield return $"共享会话已跟踪 {snapshot.ApplicationSessionsTracked} 个,状态分布:{FormatCounts(snapshot.ApplicationSessionStateCounts)}。";
}
if (snapshot.SessionsWithDiagnostics == 0)
{
yield return "没有采集到 KCP 会话诊断数据,当前无法判断 RTT、队列堆积和重传情况。";
yield break;
}
yield return $"平均 RTT={snapshot.AverageSmoothedRttMs:F1} 毫秒,峰值 RTT={snapshot.PeakSmoothedRttMs} 毫秒。";
yield return $"WaitSnd 总量={snapshot.TotalWaitSendCount},发送队列={snapshot.TotalSendQueueCount},发送缓冲={snapshot.TotalSendBufferCount}。";
yield return $"在途重传段={snapshot.TotalRetransmittedSegmentsInFlight},累计观察到的重传次数={snapshot.TotalObservedRetransmissionSends}。";
if (snapshot.TotalWaitSendCount > 0 || snapshot.TotalSendQueueCount > 0 || snapshot.TotalSendBufferCount > 0)
{
yield return "存在发送侧堆积迹象,建议优先排查发送频率、消息体积和远端处理能力。";
}
if (snapshot.TotalObservedRetransmissionSends > 0 || snapshot.TotalRetransmittedSegmentsInFlight > 0)
{
yield return "存在重传迹象,建议结合网络抖动、丢包和窗口大小继续排查。";
}
if (snapshot.AverageSmoothedRttMs >= 150 || snapshot.PeakSmoothedRttMs >= 250)
{
yield return "延迟已偏高,若游戏表现出现卡顿或回滚,优先关注 RTT 抖动。";
}
if (snapshot.SendErrors + snapshot.ReceiveErrors + snapshot.OtherErrors > 0)
{
yield return $"记录到错误 {snapshot.SendErrors + snapshot.ReceiveErrors + snapshot.OtherErrors} 次,最高频阶段={FindBusiestStage(snapshot)}。";
}
}
private static IEnumerable<string> BuildSessionLines(TransportMetricsSnapshot snapshot)
{
if (snapshot.ApplicationSessionSummaries.Count == 0)
{
yield return "没有共享会话快照。";
yield break;
}
foreach (var session in snapshot.ApplicationSessionSummaries
.OrderBy(item => item.Scope, StringComparer.Ordinal)
.ThenBy(item => item.RemoteEndPoint, StringComparer.Ordinal))
{
var target = string.IsNullOrWhiteSpace(session.RemoteEndPoint) ? "default" : session.RemoteEndPoint;
var details = new List<string>
{
$"scope={session.Scope}",
$"target={target}",
$"state={session.ConnectionState}"
};
if (session.CanSendHeartbeat)
{
details.Add("heartbeat=ready");
}
if (session.LastRoundTripTimeMs.HasValue)
{
details.Add($"rtt={session.LastRoundTripTimeMs.Value}ms");
}
if (session.NextReconnectAtUtc.HasValue)
{
details.Add($"reconnectAt={session.NextReconnectAtUtc.Value:O}");
}
if (!string.IsNullOrWhiteSpace(session.LastFailureReason))
{
details.Add($"reason={session.LastFailureReason}");
}
if (session.CurrentServerTick.HasValue)
{
details.Add($"serverTick={session.CurrentServerTick.Value}");
}
yield return string.Join(", ", details);
}
}
private static IEnumerable<string> BuildPeerLines(TransportMetricsSnapshot snapshot)
{
var peers = snapshot.PeerSummaries
.OrderByDescending(peer => peer.PayloadBytesSent + peer.PayloadBytesReceived)
.ThenBy(peer => peer.RemoteEndPoint, StringComparer.Ordinal)
.Take(5)
.ToList();
if (peers.Count == 0)
{
yield return "没有热点对端数据。";
yield break;
}
foreach (var peer in peers)
{
yield return
$"{peer.RemoteEndPoint}: payload={peer.PayloadMessagesSent + peer.PayloadMessagesReceived} 条, datagram={peer.DatagramsSent + peer.DatagramsReceived} 个, " +
$"waitSnd={peer.SessionDiagnostics.WaitSendCount}, retrans={peer.ObservedRetransmissionSends}, state={peer.SessionLifecycleState}";
}
}
private static string BuildOverallAssessment(TransportMetricsSnapshot snapshot)
{
if (snapshot.SessionsWithDiagnostics == 0 && snapshot.ApplicationSessionsTracked == 0)
{
return "观测数据不足";
}
var hasErrors = snapshot.SendErrors + snapshot.ReceiveErrors + snapshot.OtherErrors > 0;
var hasRetransmissions = snapshot.TotalObservedRetransmissionSends > 0 || snapshot.TotalRetransmittedSegmentsInFlight > 0;
var hasBacklog = snapshot.TotalWaitSendCount > 0 || snapshot.TotalSendQueueCount > 0 || snapshot.TotalSendBufferCount > 0;
var hasReconnect = snapshot.ApplicationSessionStateCounts.TryGetValue("ReconnectPending", out var pending) && pending > 0;
var hasTimeout = snapshot.ApplicationSessionStateCounts.TryGetValue("TimedOut", out var timedOut) && timedOut > 0;
var highLatency = snapshot.AverageSmoothedRttMs >= 150 || snapshot.PeakSmoothedRttMs >= 250;
if (hasTimeout || hasReconnect)
{
return "已出现会话不稳定迹象";
}
if (hasErrors && (hasRetransmissions || hasBacklog || highLatency))
{
return "网络质量存在明显风险";
}
if (hasRetransmissions || hasBacklog || highLatency)
{
return "网络链路存在轻中度异常";
}
return "整体稳定";
}
private static string FindBusiestStage(TransportMetricsSnapshot snapshot)
{
return snapshot.ErrorCountsByStage.Count == 0
? "无"
: snapshot.ErrorCountsByStage
.OrderByDescending(pair => pair.Value)
.ThenBy(pair => pair.Key, StringComparer.Ordinal)
.First()
.Key;
}
private static string FormatCounts(IReadOnlyDictionary<string, long> counts)
{
if (counts == null || counts.Count == 0)
{
return "无";
}
return string.Join(",", counts.OrderBy(pair => pair.Key, StringComparer.Ordinal).Select(pair => $"{pair.Key}={pair.Value}"));
}
private static string TranslateMode(string mode)
{
return string.Equals(mode, "server", StringComparison.OrdinalIgnoreCase)
? "server / 服务端"
: string.Equals(mode, "client", StringComparison.OrdinalIgnoreCase)
? "client / 客户端"
: mode ?? "unknown / 未知";
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 71d7507d35817ed418a2e8194de6455e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -14,6 +14,8 @@ namespace Network.NetworkTransport
void BeginRun(TransportRunDescriptor descriptor);
void RecordSessionOpened(IPEndPoint remoteEndPoint);
void RecordSessionClosed(IPEndPoint remoteEndPoint);
void RecordSessionDiagnostics(IPEndPoint remoteEndPoint, TransportSessionDiagnosticsSnapshot diagnostics);
void RecordApplicationSessionSnapshot(TransportApplicationSessionSnapshot snapshot);
void RecordPayloadSent(IPEndPoint remoteEndPoint, int bytes);
void RecordPayloadReceived(IPEndPoint remoteEndPoint, int bytes);
void RecordDatagramSent(IPEndPoint remoteEndPoint, int bytes);
@@ -23,6 +25,11 @@ namespace Network.NetworkTransport
TransportMetricsSnapshot CompleteRun();
}
public interface ITransportMetricsSink
{
void RecordApplicationSessionSnapshot(TransportApplicationSessionSnapshot snapshot);
}
public sealed class TransportRunDescriptor
{
public TransportRunDescriptor(string transportName, bool isServer, IPEndPoint defaultRemoteEndPoint = null)
@@ -55,23 +62,98 @@ namespace Network.NetworkTransport
[DataMember(Order = 6)] public DateTimeOffset? CompletedAtUtc { get; set; }
[DataMember(Order = 7)] public long DurationMs { get; set; }
[DataMember(Order = 8)] public string ReportPath { get; set; }
[DataMember(Order = 9)] public int ActiveSessions { get; set; }
[DataMember(Order = 10)] public int PeakActiveSessions { get; set; }
[DataMember(Order = 11)] public long SessionsCreated { get; set; }
[DataMember(Order = 12)] public long SessionsClosed { get; set; }
[DataMember(Order = 13)] public long PayloadMessagesSent { get; set; }
[DataMember(Order = 14)] public long PayloadBytesSent { get; set; }
[DataMember(Order = 15)] public long PayloadMessagesReceived { get; set; }
[DataMember(Order = 16)] public long PayloadBytesReceived { get; set; }
[DataMember(Order = 17)] public long DatagramsSent { get; set; }
[DataMember(Order = 18)] public long DatagramBytesSent { get; set; }
[DataMember(Order = 19)] public long DatagramsReceived { get; set; }
[DataMember(Order = 20)] public long DatagramBytesReceived { get; set; }
[DataMember(Order = 21)] public long SendErrors { get; set; }
[DataMember(Order = 22)] public long ReceiveErrors { get; set; }
[DataMember(Order = 23)] public long OtherErrors { get; set; }
[DataMember(Order = 24)] public Dictionary<string, long> ErrorCountsByStage { get; set; } = new(StringComparer.Ordinal);
[DataMember(Order = 25)] public List<TransportPeerMetricsSnapshot> PeerSummaries { get; set; } = new();
[DataMember(Order = 9)] public string SummaryPath { get; set; }
[DataMember(Order = 10)] public TransportMetricsReadableSummary ReadableSummary { get; set; } = new();
[DataMember(Order = 11)] public int ActiveSessions { get; set; }
[DataMember(Order = 12)] public int PeakActiveSessions { get; set; }
[DataMember(Order = 13)] public long SessionsCreated { get; set; }
[DataMember(Order = 14)] public long SessionsClosed { get; set; }
[DataMember(Order = 15)] public long PayloadMessagesSent { get; set; }
[DataMember(Order = 16)] public long PayloadBytesSent { get; set; }
[DataMember(Order = 17)] public long PayloadMessagesReceived { get; set; }
[DataMember(Order = 18)] public long PayloadBytesReceived { get; set; }
[DataMember(Order = 19)] public long DatagramsSent { get; set; }
[DataMember(Order = 20)] public long DatagramBytesSent { get; set; }
[DataMember(Order = 21)] public long DatagramsReceived { get; set; }
[DataMember(Order = 22)] public long DatagramBytesReceived { get; set; }
[DataMember(Order = 23)] public long SendErrors { get; set; }
[DataMember(Order = 24)] public long ReceiveErrors { get; set; }
[DataMember(Order = 25)] public long OtherErrors { get; set; }
[DataMember(Order = 26)] public int SessionsWithDiagnostics { get; set; }
[DataMember(Order = 27)] public double AverageSmoothedRttMs { get; set; }
[DataMember(Order = 28)] public int PeakSmoothedRttMs { get; set; }
[DataMember(Order = 29)] public double AverageRetransmissionTimeoutMs { get; set; }
[DataMember(Order = 30)] public int PeakRetransmissionTimeoutMs { get; set; }
[DataMember(Order = 31)] public long TotalWaitSendCount { get; set; }
[DataMember(Order = 32)] public long PeakWaitSendCount { get; set; }
[DataMember(Order = 33)] public long TotalSendQueueCount { get; set; }
[DataMember(Order = 34)] public long TotalSendBufferCount { get; set; }
[DataMember(Order = 35)] public long TotalReceiveQueueCount { get; set; }
[DataMember(Order = 36)] public long TotalReceiveBufferCount { get; set; }
[DataMember(Order = 37)] public long TotalRetransmittedSegmentsInFlight { get; set; }
[DataMember(Order = 38)] public long PeakRetransmittedSegmentsInFlight { get; set; }
[DataMember(Order = 39)] public long TotalObservedRetransmissionSends { get; set; }
[DataMember(Order = 40)] public long TotalObservedLossSignals { get; set; }
[DataMember(Order = 41)] public Dictionary<string, long> SessionStateCounts { get; set; } = new(StringComparer.Ordinal);
[DataMember(Order = 42)] public int ApplicationSessionsTracked { get; set; }
[DataMember(Order = 43)] public Dictionary<string, long> ApplicationSessionStateCounts { get; set; } = new(StringComparer.Ordinal);
[DataMember(Order = 44)] public List<TransportApplicationSessionSnapshot> ApplicationSessionSummaries { get; set; } = new();
[DataMember(Order = 45)] public Dictionary<string, long> ErrorCountsByStage { get; set; } = new(StringComparer.Ordinal);
[DataMember(Order = 46)] public List<TransportPeerMetricsSnapshot> PeerSummaries { get; set; } = new();
}
[DataContract]
public sealed class TransportMetricsReadableSummary
{
[DataMember(Order = 1)] public string Headline { get; set; }
[DataMember(Order = 2)] public string SessionSummary { get; set; }
[DataMember(Order = 3)] public string TrafficSummary { get; set; }
[DataMember(Order = 4)] public string ErrorSummary { get; set; }
[DataMember(Order = 5)] public string LifecycleSummary { get; set; }
[DataMember(Order = 6)] public string HealthSummary { get; set; }
[DataMember(Order = 7)] public List<string> TopPeerHighlights { get; set; } = new();
}
[DataContract]
public sealed class TransportApplicationSessionSnapshot
{
[DataMember(Order = 1)] public string Scope { get; set; }
[DataMember(Order = 2)] public string RemoteEndPoint { get; set; }
[DataMember(Order = 3)] public string ConnectionState { get; set; }
[DataMember(Order = 4)] public bool CanSendHeartbeat { get; set; }
[DataMember(Order = 5)] public long? LastRoundTripTimeMs { get; set; }
[DataMember(Order = 6)] public string LastFailureReason { get; set; }
[DataMember(Order = 7)] public DateTimeOffset? LastLivenessUtc { get; set; }
[DataMember(Order = 8)] public DateTimeOffset? LastHeartbeatSentUtc { get; set; }
[DataMember(Order = 9)] public DateTimeOffset? NextReconnectAtUtc { get; set; }
[DataMember(Order = 10)] public long? CurrentServerTick { get; set; }
[DataMember(Order = 11)] public DateTimeOffset? ObservedAtUtc { get; set; }
}
[DataContract]
public sealed class TransportSessionDiagnosticsSnapshot
{
[DataMember(Order = 1)] public string LifecycleState { get; set; }
[DataMember(Order = 2)] public DateTimeOffset? ObservedAtUtc { get; set; }
[DataMember(Order = 3)] public long IdleMs { get; set; }
[DataMember(Order = 4)] public int KcpStateCode { get; set; }
[DataMember(Order = 5)] public int SmoothedRttMs { get; set; }
[DataMember(Order = 6)] public int RttVarianceMs { get; set; }
[DataMember(Order = 7)] public int RetransmissionTimeoutMs { get; set; }
[DataMember(Order = 8)] public int LocalSendWindow { get; set; }
[DataMember(Order = 9)] public int LocalReceiveWindow { get; set; }
[DataMember(Order = 10)] public int RemoteWindow { get; set; }
[DataMember(Order = 11)] public int CongestionWindow { get; set; }
[DataMember(Order = 12)] public int WaitSendCount { get; set; }
[DataMember(Order = 13)] public int SendQueueCount { get; set; }
[DataMember(Order = 14)] public int SendBufferCount { get; set; }
[DataMember(Order = 15)] public int ReceiveQueueCount { get; set; }
[DataMember(Order = 16)] public int ReceiveBufferCount { get; set; }
[DataMember(Order = 17)] public int DeadLinkThreshold { get; set; }
[DataMember(Order = 18)] public long SegmentTransmitCount { get; set; }
[DataMember(Order = 19)] public int RetransmittedSegmentsInFlight { get; set; }
[DataMember(Order = 20)] public long ObservedRetransmissionSends { get; set; }
[DataMember(Order = 21)] public long ObservedLossSignals { get; set; }
}
[DataContract]
@@ -90,7 +172,15 @@ namespace Network.NetworkTransport
[DataMember(Order = 11)] public long DatagramBytesSent { get; set; }
[DataMember(Order = 12)] public long DatagramsReceived { get; set; }
[DataMember(Order = 13)] public long DatagramBytesReceived { get; set; }
[DataMember(Order = 14)] public Dictionary<string, long> ErrorCountsByStage { get; set; } = new(StringComparer.Ordinal);
[DataMember(Order = 14)] public string SessionLifecycleState { get; set; }
[DataMember(Order = 15)] public TransportSessionDiagnosticsSnapshot SessionDiagnostics { get; set; } = new();
[DataMember(Order = 16)] public int PeakSmoothedRttMs { get; set; }
[DataMember(Order = 17)] public int PeakRetransmissionTimeoutMs { get; set; }
[DataMember(Order = 18)] public int PeakWaitSendCount { get; set; }
[DataMember(Order = 19)] public int PeakRetransmittedSegmentsInFlight { get; set; }
[DataMember(Order = 20)] public long ObservedRetransmissionSends { get; set; }
[DataMember(Order = 21)] public long ObservedLossSignals { get; set; }
[DataMember(Order = 22)] public Dictionary<string, long> ErrorCountsByStage { get; set; } = new(StringComparer.Ordinal);
}
public sealed class DefaultTransportMetricsModule : ITransportMetricsModule
@@ -99,7 +189,14 @@ namespace Network.NetworkTransport
private readonly Func<DateTimeOffset> utcNowProvider;
private readonly TextWriter consoleWriter;
private readonly string reportDirectory;
private readonly bool writeJsonReport;
private readonly bool writeTextSummaryReport;
private readonly bool writeDiagnosisReport;
private readonly bool emitConsoleSummary;
private readonly int maxPeerSummariesInTextReport;
private readonly int maxPeerSummariesInConsole;
private readonly Dictionary<string, PeerAccumulator> peers = new(StringComparer.Ordinal);
private readonly Dictionary<string, TransportApplicationSessionSnapshot> applicationSessions = new(StringComparer.Ordinal);
private readonly Dictionary<string, long> errorCountsByStage = new(StringComparer.Ordinal);
private string runId;
@@ -129,12 +226,32 @@ namespace Network.NetworkTransport
private TransportMetricsSnapshot completedSnapshot;
public DefaultTransportMetricsModule(string reportDirectory = null, Func<DateTimeOffset> utcNowProvider = null, TextWriter consoleWriter = null)
: this(
new TransportMetricsOptions
{
ReportDirectory = reportDirectory,
ConsoleWriter = consoleWriter
},
utcNowProvider)
{
}
public DefaultTransportMetricsModule(TransportMetricsOptions options, Func<DateTimeOffset> utcNowProvider = null)
{
options ??= TransportMetricsOptions.Default;
reportDirectory = options.ReportDirectory;
this.reportDirectory = string.IsNullOrWhiteSpace(reportDirectory)
? Path.Combine(Directory.GetCurrentDirectory(), "Logs", "transport-metrics")
: reportDirectory;
this.utcNowProvider = utcNowProvider ?? (() => DateTimeOffset.UtcNow);
consoleWriter = options.ConsoleWriter;
this.consoleWriter = consoleWriter ?? Console.Out;
writeJsonReport = options.WriteJsonReport;
writeTextSummaryReport = options.WriteTextSummaryReport;
writeDiagnosisReport = options.WriteDiagnosisReport;
emitConsoleSummary = options.EmitConsoleSummary;
maxPeerSummariesInTextReport = Math.Max(0, options.MaxPeerSummariesInTextReport);
maxPeerSummariesInConsole = Math.Max(0, options.MaxPeerSummariesInConsole);
}
public void BeginRun(TransportRunDescriptor descriptor)
@@ -147,6 +264,7 @@ namespace Network.NetworkTransport
lock (gate)
{
peers.Clear();
applicationSessions.Clear();
errorCountsByStage.Clear();
runId = Guid.NewGuid().ToString("N");
transportName = descriptor.TransportName;
@@ -182,6 +300,7 @@ namespace Network.NetworkTransport
activeSessions++;
peakActiveSessions = Math.Max(peakActiveSessions, activeSessions);
peer.SessionOpens++;
peer.SessionLifecycleState = "active";
});
public void RecordSessionClosed(IPEndPoint remoteEndPoint) => Update(remoteEndPoint, peer =>
@@ -189,8 +308,33 @@ namespace Network.NetworkTransport
sessionsClosed++;
activeSessions = Math.Max(0, activeSessions - 1);
peer.SessionCloses++;
peer.SessionLifecycleState = "closed";
});
public void RecordSessionDiagnostics(IPEndPoint remoteEndPoint, TransportSessionDiagnosticsSnapshot diagnostics) => Update(remoteEndPoint, peer =>
{
if (diagnostics == null)
{
return;
}
peer.RecordDiagnostics(diagnostics);
});
public void RecordApplicationSessionSnapshot(TransportApplicationSessionSnapshot snapshot)
{
lock (gate)
{
if (!hasRun || snapshot == null)
{
return;
}
var key = BuildApplicationSessionKey(snapshot.Scope, snapshot.RemoteEndPoint);
applicationSessions[key] = Clone(snapshot);
}
}
public void RecordPayloadSent(IPEndPoint remoteEndPoint, int bytes) => Update(remoteEndPoint, peer =>
{
payloadMessagesSent++;
@@ -286,8 +430,14 @@ namespace Network.NetworkTransport
completed = true;
completedSnapshot = BuildSnapshot();
completedSnapshot.ReportPath = WriteJsonReport(completedSnapshot);
completedSnapshot.SummaryPath = WriteTextSummaryReport(completedSnapshot);
WriteDiagnosisReport(completedSnapshot);
reportPath = completedSnapshot.ReportPath;
consoleWriter.WriteLine($"[TransportMetrics] {completedSnapshot.TransportName} mode={completedSnapshot.Mode} run={completedSnapshot.RunId} durationMs={completedSnapshot.DurationMs} peak={completedSnapshot.PeakActiveSessions} payloadTx={completedSnapshot.PayloadMessagesSent}/{completedSnapshot.PayloadBytesSent}B payloadRx={completedSnapshot.PayloadMessagesReceived}/{completedSnapshot.PayloadBytesReceived}B datagramTx={completedSnapshot.DatagramsSent}/{completedSnapshot.DatagramBytesSent}B datagramRx={completedSnapshot.DatagramsReceived}/{completedSnapshot.DatagramBytesReceived}B errors={completedSnapshot.SendErrors + completedSnapshot.ReceiveErrors + completedSnapshot.OtherErrors} report={completedSnapshot.ReportPath ?? "none"}");
if (emitConsoleSummary)
{
WriteConsoleSummary(completedSnapshot);
}
return completedSnapshot;
}
}
@@ -310,6 +460,26 @@ namespace Network.NetworkTransport
private TransportMetricsSnapshot BuildSnapshot()
{
var end = completedAtUtc ?? utcNowProvider();
var peerSnapshots = peers.Values.Select(peer => peer.ToSnapshot()).OrderBy(peer => peer.RemoteEndPoint, StringComparer.Ordinal).ToList();
var peersWithDiagnostics = peerSnapshots.Where(peer => peer.SessionDiagnostics?.ObservedAtUtc != null).ToList();
var averageSmoothedRttMs = peersWithDiagnostics.Count == 0
? 0d
: peersWithDiagnostics.Average(peer => peer.SessionDiagnostics.SmoothedRttMs);
var averageRetransmissionTimeoutMs = peersWithDiagnostics.Count == 0
? 0d
: peersWithDiagnostics.Average(peer => peer.SessionDiagnostics.RetransmissionTimeoutMs);
var sessionStateCounts = peerSnapshots
.GroupBy(peer => string.IsNullOrWhiteSpace(peer.SessionLifecycleState) ? "unknown" : peer.SessionLifecycleState, StringComparer.Ordinal)
.ToDictionary(group => group.Key, group => (long)group.Count(), StringComparer.Ordinal);
var applicationSessionSnapshots = applicationSessions.Values
.Select(Clone)
.OrderBy(snapshot => snapshot.Scope, StringComparer.Ordinal)
.ThenBy(snapshot => snapshot.RemoteEndPoint, StringComparer.Ordinal)
.ToList();
var applicationSessionStateCounts = applicationSessionSnapshots
.GroupBy(snapshot => string.IsNullOrWhiteSpace(snapshot.ConnectionState) ? "unknown" : snapshot.ConnectionState, StringComparer.Ordinal)
.ToDictionary(group => group.Key, group => (long)group.Count(), StringComparer.Ordinal);
return new TransportMetricsSnapshot
{
RunId = runId,
@@ -320,6 +490,8 @@ namespace Network.NetworkTransport
CompletedAtUtc = completedAtUtc,
DurationMs = startedAtUtc.HasValue ? Math.Max(0L, (long)(end - startedAtUtc.Value).TotalMilliseconds) : 0L,
ReportPath = reportPath,
SummaryPath = null,
ReadableSummary = BuildReadableSummary(),
ActiveSessions = activeSessions,
PeakActiveSessions = peakActiveSessions,
SessionsCreated = sessionsCreated,
@@ -335,13 +507,107 @@ namespace Network.NetworkTransport
SendErrors = sendErrors,
ReceiveErrors = receiveErrors,
OtherErrors = otherErrors,
SessionsWithDiagnostics = peersWithDiagnostics.Count,
AverageSmoothedRttMs = averageSmoothedRttMs,
PeakSmoothedRttMs = peerSnapshots.Count == 0 ? 0 : peerSnapshots.Max(peer => peer.PeakSmoothedRttMs),
AverageRetransmissionTimeoutMs = averageRetransmissionTimeoutMs,
PeakRetransmissionTimeoutMs = peerSnapshots.Count == 0 ? 0 : peerSnapshots.Max(peer => peer.PeakRetransmissionTimeoutMs),
TotalWaitSendCount = peerSnapshots.Sum(peer => (long)peer.SessionDiagnostics.WaitSendCount),
PeakWaitSendCount = peerSnapshots.Count == 0 ? 0 : peerSnapshots.Max(peer => (long)peer.PeakWaitSendCount),
TotalSendQueueCount = peerSnapshots.Sum(peer => (long)peer.SessionDiagnostics.SendQueueCount),
TotalSendBufferCount = peerSnapshots.Sum(peer => (long)peer.SessionDiagnostics.SendBufferCount),
TotalReceiveQueueCount = peerSnapshots.Sum(peer => (long)peer.SessionDiagnostics.ReceiveQueueCount),
TotalReceiveBufferCount = peerSnapshots.Sum(peer => (long)peer.SessionDiagnostics.ReceiveBufferCount),
TotalRetransmittedSegmentsInFlight = peerSnapshots.Sum(peer => (long)peer.SessionDiagnostics.RetransmittedSegmentsInFlight),
PeakRetransmittedSegmentsInFlight = peerSnapshots.Count == 0 ? 0 : peerSnapshots.Max(peer => (long)peer.PeakRetransmittedSegmentsInFlight),
TotalObservedRetransmissionSends = peerSnapshots.Sum(peer => peer.ObservedRetransmissionSends),
TotalObservedLossSignals = peerSnapshots.Sum(peer => peer.ObservedLossSignals),
SessionStateCounts = sessionStateCounts,
ApplicationSessionsTracked = applicationSessionSnapshots.Count,
ApplicationSessionStateCounts = applicationSessionStateCounts,
ApplicationSessionSummaries = applicationSessionSnapshots,
ErrorCountsByStage = errorCountsByStage.ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.Ordinal),
PeerSummaries = peers.Values.Select(peer => peer.ToSnapshot()).OrderBy(peer => peer.RemoteEndPoint, StringComparer.Ordinal).ToList()
PeerSummaries = peerSnapshots
};
}
private TransportMetricsReadableSummary BuildReadableSummary()
{
var topPeers = peers.Values
.OrderByDescending(peer => peer.PayloadBytesSent + peer.PayloadBytesReceived)
.ThenBy(peer => peer.RemoteEndPoint, StringComparer.Ordinal)
.Take(Math.Max(maxPeerSummariesInTextReport, maxPeerSummariesInConsole))
.Select(peer => $"{peer.RemoteEndPoint}: payload={peer.PayloadMessagesSent + peer.PayloadMessagesReceived} msgs, datagram={peer.DatagramsSent + peer.DatagramsReceived} packets, errors={peer.ErrorCountsByStage.Values.Sum()}")
.ToList();
var totalErrors = sendErrors + receiveErrors + otherErrors;
var busiestErrorStage = errorCountsByStage.Count == 0
? "none"
: errorCountsByStage.OrderByDescending(pair => pair.Value).ThenBy(pair => pair.Key, StringComparer.Ordinal).First().Key;
return new TransportMetricsReadableSummary
{
Headline = $"{transportName} {mode} run {runId} finished in {Math.Max(0L, startedAtUtc.HasValue ? (long)((completedAtUtc ?? utcNowProvider()) - startedAtUtc.Value).TotalMilliseconds : 0L)} ms.",
SessionSummary = $"Sessions active={activeSessions}, peak={peakActiveSessions}, opened={sessionsCreated}, closed={sessionsClosed}, peers={peers.Count}.",
TrafficSummary = $"Payload tx/rx={payloadMessagesSent}/{payloadMessagesReceived} msgs ({payloadBytesSent}/{payloadBytesReceived} B), datagram tx/rx={datagramsSent}/{datagramsReceived} ({datagramBytesSent}/{datagramBytesReceived} B).",
ErrorSummary = totalErrors == 0
? "No transport errors were recorded."
: $"Errors total={totalErrors}, send={sendErrors}, receive={receiveErrors}, other={otherErrors}, busiestStage={busiestErrorStage}.",
LifecycleSummary = BuildLifecycleSummary(),
HealthSummary = BuildHealthSummary(),
TopPeerHighlights = topPeers
};
}
private string BuildLifecycleSummary()
{
if (applicationSessions.Count == 0)
{
return "No shared session lifecycle snapshots were captured.";
}
var stateSummary = string.Join(
", ",
applicationSessions.Values
.GroupBy(snapshot => string.IsNullOrWhiteSpace(snapshot.ConnectionState) ? "unknown" : snapshot.ConnectionState, StringComparer.Ordinal)
.OrderBy(group => group.Key, StringComparer.Ordinal)
.Select(group => $"{group.Key}={group.Count()}"));
var heartbeatReady = applicationSessions.Values.Count(snapshot => snapshot.CanSendHeartbeat);
var pendingReconnects = applicationSessions.Values.Count(snapshot => snapshot.NextReconnectAtUtc.HasValue);
return $"Lifecycle tracked={applicationSessions.Count}, heartbeatReady={heartbeatReady}, reconnectPending={pendingReconnects}, states={stateSummary}.";
}
private string BuildHealthSummary()
{
var diagnosticsPeers = peers.Values.Where(peer => peer.SessionDiagnostics?.ObservedAtUtc != null).ToList();
if (diagnosticsPeers.Count == 0)
{
return "No KCP session diagnostics were captured.";
}
var averageRtt = diagnosticsPeers.Average(peer => peer.SessionDiagnostics.SmoothedRttMs);
var peakRtt = diagnosticsPeers.Max(peer => peer.PeakSmoothedRttMs);
var totalWaitSend = diagnosticsPeers.Sum(peer => peer.SessionDiagnostics.WaitSendCount);
var totalRetransmittedSegments = diagnosticsPeers.Sum(peer => peer.SessionDiagnostics.RetransmittedSegmentsInFlight);
var totalObservedRetransmissions = diagnosticsPeers.Sum(peer => peer.ObservedRetransmissionSends);
var stateSummary = string.Join(
", ",
diagnosticsPeers
.GroupBy(peer => string.IsNullOrWhiteSpace(peer.SessionLifecycleState) ? "unknown" : peer.SessionLifecycleState, StringComparer.Ordinal)
.OrderBy(group => group.Key, StringComparer.Ordinal)
.Select(group => $"{group.Key}={group.Count()}"));
return $"Health avgRtt={averageRtt:F1} ms, peakRtt={peakRtt} ms, waitSnd={totalWaitSend}, retransInFlight={totalRetransmittedSegments}, observedRetransmissions={totalObservedRetransmissions}, states={stateSummary}.";
}
private string WriteJsonReport(TransportMetricsSnapshot snapshot)
{
if (!writeJsonReport)
{
return null;
}
try
{
Directory.CreateDirectory(reportDirectory);
@@ -361,6 +627,282 @@ namespace Network.NetworkTransport
}
}
private string WriteTextSummaryReport(TransportMetricsSnapshot snapshot)
{
if (!writeTextSummaryReport)
{
return null;
}
try
{
Directory.CreateDirectory(reportDirectory);
var timestamp = (snapshot.CompletedAtUtc ?? utcNowProvider()).ToString("yyyyMMdd-HHmmssfff");
var filePath = Path.Combine(reportDirectory, $"{snapshot.Mode}-{snapshot.TransportName}-{timestamp}-{snapshot.RunId}.summary.txt");
File.WriteAllText(filePath, BuildReadableSummaryText(snapshot), new UTF8Encoding(false));
return filePath;
}
catch (Exception exception)
{
consoleWriter.WriteLine($"[TransportMetrics] Failed to write summary: {exception.Message}");
return null;
}
}
private string WriteDiagnosisReport(TransportMetricsSnapshot snapshot)
{
if (!writeDiagnosisReport)
{
return null;
}
try
{
Directory.CreateDirectory(reportDirectory);
var timestamp = (snapshot.CompletedAtUtc ?? utcNowProvider()).ToString("yyyyMMdd-HHmmssfff");
var filePath = Path.Combine(reportDirectory, $"{snapshot.Mode}-{snapshot.TransportName}-{timestamp}-{snapshot.RunId}.diagnosis.txt");
File.WriteAllText(filePath, TransportMetricsDiagnosisFormatter.BuildChineseDiagnosis(snapshot), new UTF8Encoding(false));
return filePath;
}
catch (Exception exception)
{
consoleWriter.WriteLine($"[TransportMetrics] Failed to write diagnosis: {exception.Message}");
return null;
}
}
private void WriteConsoleSummary(TransportMetricsSnapshot snapshot)
{
consoleWriter.WriteLine("[TransportMetrics] English Summary");
consoleWriter.WriteLine($"[TransportMetrics] Run: {snapshot.RunId}");
consoleWriter.WriteLine($"[TransportMetrics] Transport: {snapshot.TransportName}");
consoleWriter.WriteLine($"[TransportMetrics] Mode: {snapshot.Mode}");
consoleWriter.WriteLine($"[TransportMetrics] StartedAtUtc: {snapshot.StartedAtUtc}");
consoleWriter.WriteLine($"[TransportMetrics] CompletedAtUtc: {snapshot.CompletedAtUtc}");
consoleWriter.WriteLine($"[TransportMetrics] DurationMs: {snapshot.DurationMs}");
consoleWriter.WriteLine($"[TransportMetrics] JsonReport: {snapshot.ReportPath ?? "none"}");
consoleWriter.WriteLine($"[TransportMetrics] SummaryReport: {snapshot.SummaryPath ?? "none"}");
consoleWriter.WriteLine($"[TransportMetrics] {snapshot.ReadableSummary.Headline}");
consoleWriter.WriteLine($"[TransportMetrics] {snapshot.ReadableSummary.SessionSummary}");
consoleWriter.WriteLine($"[TransportMetrics] {snapshot.ReadableSummary.TrafficSummary}");
consoleWriter.WriteLine($"[TransportMetrics] {snapshot.ReadableSummary.ErrorSummary}");
consoleWriter.WriteLine($"[TransportMetrics] {snapshot.ReadableSummary.LifecycleSummary}");
consoleWriter.WriteLine($"[TransportMetrics] {snapshot.ReadableSummary.HealthSummary}");
consoleWriter.WriteLine("[TransportMetrics] Top Peers:");
foreach (var line in snapshot.ReadableSummary.TopPeerHighlights.Take(maxPeerSummariesInConsole))
{
consoleWriter.WriteLine($"[TransportMetrics] Peer: {line}");
}
if (snapshot.ReadableSummary.TopPeerHighlights.Count == 0)
{
consoleWriter.WriteLine("[TransportMetrics] Peer: none");
}
consoleWriter.WriteLine("[TransportMetrics] Chinese Summary");
consoleWriter.WriteLine($"[TransportMetrics] 运行ID: {snapshot.RunId}");
consoleWriter.WriteLine($"[TransportMetrics] 传输实现: {snapshot.TransportName}");
consoleWriter.WriteLine($"[TransportMetrics] 运行模式: {TranslateMode(snapshot.Mode)}");
consoleWriter.WriteLine($"[TransportMetrics] 开始时间(UTC): {snapshot.StartedAtUtc}");
consoleWriter.WriteLine($"[TransportMetrics] 结束时间(UTC): {snapshot.CompletedAtUtc}");
consoleWriter.WriteLine($"[TransportMetrics] 总耗时(毫秒): {snapshot.DurationMs}");
consoleWriter.WriteLine($"[TransportMetrics] Json报告: {snapshot.ReportPath ?? "无"}");
consoleWriter.WriteLine($"[TransportMetrics] 摘要报告: {snapshot.SummaryPath ?? "无"}");
consoleWriter.WriteLine($"[TransportMetrics] {BuildChineseHeadline(snapshot)}");
consoleWriter.WriteLine($"[TransportMetrics] {BuildChineseSessionSummary(snapshot)}");
consoleWriter.WriteLine($"[TransportMetrics] {BuildChineseTrafficSummary(snapshot)}");
consoleWriter.WriteLine($"[TransportMetrics] {BuildChineseErrorSummary(snapshot)}");
consoleWriter.WriteLine($"[TransportMetrics] {BuildChineseLifecycleSummary(snapshot)}");
consoleWriter.WriteLine($"[TransportMetrics] {BuildChineseHealthSummary(snapshot)}");
consoleWriter.WriteLine("[TransportMetrics] 重点对端:");
foreach (var peer in snapshot.PeerSummaries
.OrderByDescending(item => item.PayloadBytesSent + item.PayloadBytesReceived)
.ThenBy(item => item.RemoteEndPoint, StringComparer.Ordinal)
.Take(maxPeerSummariesInConsole))
{
consoleWriter.WriteLine($"[TransportMetrics] 对端: {peer.RemoteEndPoint}: 业务消息={peer.PayloadMessagesSent + peer.PayloadMessagesReceived} 条, 数据报={peer.DatagramsSent + peer.DatagramsReceived} 个, 错误={peer.ErrorCountsByStage.Values.Sum()}");
}
if (snapshot.PeerSummaries.Count == 0)
{
consoleWriter.WriteLine("[TransportMetrics] 对端: 无");
}
}
private string BuildReadableSummaryText(TransportMetricsSnapshot snapshot)
{
var builder = new StringBuilder();
builder.AppendLine("Transport Metrics Summary");
builder.AppendLine();
builder.AppendLine("English Summary");
builder.AppendLine();
builder.AppendLine($"Run: {snapshot.RunId}");
builder.AppendLine($"Transport: {snapshot.TransportName}");
builder.AppendLine($"Mode: {snapshot.Mode}");
builder.AppendLine($"StartedAtUtc: {snapshot.StartedAtUtc}");
builder.AppendLine($"CompletedAtUtc: {snapshot.CompletedAtUtc}");
builder.AppendLine($"DurationMs: {snapshot.DurationMs}");
builder.AppendLine();
builder.AppendLine(snapshot.ReadableSummary.Headline);
builder.AppendLine(snapshot.ReadableSummary.SessionSummary);
builder.AppendLine(snapshot.ReadableSummary.TrafficSummary);
builder.AppendLine(snapshot.ReadableSummary.ErrorSummary);
builder.AppendLine(snapshot.ReadableSummary.LifecycleSummary);
builder.AppendLine(snapshot.ReadableSummary.HealthSummary);
builder.AppendLine();
builder.AppendLine("Top Peers:");
foreach (var line in snapshot.ReadableSummary.TopPeerHighlights.Take(maxPeerSummariesInTextReport))
{
builder.AppendLine($"- {line}");
}
if (snapshot.ReadableSummary.TopPeerHighlights.Count == 0)
{
builder.AppendLine("- none");
}
builder.AppendLine();
builder.AppendLine("Paths:");
builder.AppendLine($"- JsonReport: {snapshot.ReportPath ?? "disabled"}");
builder.AppendLine($"- SummaryReport: {snapshot.SummaryPath ?? "pending"}");
builder.AppendLine();
builder.AppendLine("Chinese Summary");
builder.AppendLine();
builder.AppendLine($"运行ID: {snapshot.RunId}");
builder.AppendLine($"传输实现: {snapshot.TransportName}");
builder.AppendLine($"运行模式: {TranslateMode(snapshot.Mode)}");
builder.AppendLine($"开始时间(UTC): {snapshot.StartedAtUtc}");
builder.AppendLine($"结束时间(UTC): {snapshot.CompletedAtUtc}");
builder.AppendLine($"总耗时(毫秒): {snapshot.DurationMs}");
builder.AppendLine();
builder.AppendLine(BuildChineseHeadline(snapshot));
builder.AppendLine(BuildChineseSessionSummary(snapshot));
builder.AppendLine(BuildChineseTrafficSummary(snapshot));
builder.AppendLine(BuildChineseErrorSummary(snapshot));
builder.AppendLine(BuildChineseLifecycleSummary(snapshot));
builder.AppendLine(BuildChineseHealthSummary(snapshot));
builder.AppendLine();
builder.AppendLine("重点对端:");
foreach (var peer in snapshot.PeerSummaries
.OrderByDescending(item => item.PayloadBytesSent + item.PayloadBytesReceived)
.ThenBy(item => item.RemoteEndPoint, StringComparer.Ordinal)
.Take(maxPeerSummariesInTextReport))
{
builder.AppendLine($"- {peer.RemoteEndPoint}: 业务消息={peer.PayloadMessagesSent + peer.PayloadMessagesReceived} 条, 数据报={peer.DatagramsSent + peer.DatagramsReceived} 个, 错误={peer.ErrorCountsByStage.Values.Sum()}");
}
if (snapshot.PeerSummaries.Count == 0)
{
builder.AppendLine("- 无");
}
builder.AppendLine();
builder.AppendLine("文件路径:");
builder.AppendLine($"- Json报告: {snapshot.ReportPath ?? "已禁用"}");
builder.AppendLine($"- 摘要报告: {snapshot.SummaryPath ?? "待写入"}");
return builder.ToString();
}
private static string TranslateMode(string mode)
{
return string.Equals(mode, "server", StringComparison.OrdinalIgnoreCase)
? "server / 服务端"
: string.Equals(mode, "client", StringComparison.OrdinalIgnoreCase)
? "client / 客户端"
: mode ?? "unknown / 未知";
}
private static string BuildChineseHeadline(TransportMetricsSnapshot snapshot)
{
return $"{snapshot.TransportName} {TranslateMode(snapshot.Mode)} 运行已完成,总耗时 {snapshot.DurationMs} 毫秒。";
}
private static string BuildChineseSessionSummary(TransportMetricsSnapshot snapshot)
{
return $"会话统计:当前活跃={snapshot.ActiveSessions},峰值={snapshot.PeakActiveSessions},建立={snapshot.SessionsCreated},关闭={snapshot.SessionsClosed},对端数={snapshot.PeerSummaries.Count}。";
}
private static string BuildChineseTrafficSummary(TransportMetricsSnapshot snapshot)
{
return $"流量统计:业务消息发送/接收={snapshot.PayloadMessagesSent}/{snapshot.PayloadMessagesReceived} 条({snapshot.PayloadBytesSent}/{snapshot.PayloadBytesReceived} B),数据报发送/接收={snapshot.DatagramsSent}/{snapshot.DatagramsReceived} 个({snapshot.DatagramBytesSent}/{snapshot.DatagramBytesReceived} B)。";
}
private static string BuildChineseErrorSummary(TransportMetricsSnapshot snapshot)
{
var totalErrors = snapshot.SendErrors + snapshot.ReceiveErrors + snapshot.OtherErrors;
if (totalErrors == 0)
{
return "错误统计:未记录到传输错误。";
}
var busiestErrorStage = snapshot.ErrorCountsByStage.Count == 0
? "无"
: snapshot.ErrorCountsByStage.OrderByDescending(pair => pair.Value).ThenBy(pair => pair.Key, StringComparer.Ordinal).First().Key;
return $"错误统计:总数={totalErrors},发送={snapshot.SendErrors},接收={snapshot.ReceiveErrors},其他={snapshot.OtherErrors},最高频阶段={busiestErrorStage}。";
}
private static string BuildChineseLifecycleSummary(TransportMetricsSnapshot snapshot)
{
if (snapshot.ApplicationSessionsTracked == 0)
{
return "生命周期摘要:未捕获到共享会话状态快照。";
}
var states = snapshot.ApplicationSessionStateCounts.Count == 0
? "无"
: string.Join(",", snapshot.ApplicationSessionStateCounts.OrderBy(pair => pair.Key, StringComparer.Ordinal).Select(pair => $"{pair.Key}={pair.Value}"));
var heartbeatReady = snapshot.ApplicationSessionSummaries.Count(session => session.CanSendHeartbeat);
var reconnectPending = snapshot.ApplicationSessionSummaries.Count(session => session.NextReconnectAtUtc.HasValue);
return $"生命周期摘要:已跟踪={snapshot.ApplicationSessionsTracked},可发送心跳={heartbeatReady},等待重连={reconnectPending},状态分布={states}。";
}
private static string BuildChineseHealthSummary(TransportMetricsSnapshot snapshot)
{
if (snapshot.SessionsWithDiagnostics == 0)
{
return "健康摘要:未捕获到 KCP 会话诊断数据。";
}
var states = snapshot.SessionStateCounts.Count == 0
? "无"
: string.Join(",", snapshot.SessionStateCounts.OrderBy(pair => pair.Key, StringComparer.Ordinal).Select(pair => $"{pair.Key}={pair.Value}"));
return $"健康摘要:平均 RTT={snapshot.AverageSmoothedRttMs:F1} 毫秒,峰值 RTT={snapshot.PeakSmoothedRttMs} 毫秒,WaitSnd={snapshot.TotalWaitSendCount},在途重传段={snapshot.TotalRetransmittedSegmentsInFlight},累计观察到的重传次数={snapshot.TotalObservedRetransmissionSends},状态分布={states}。";
}
private static string BuildApplicationSessionKey(string scope, string remoteEndPoint)
{
var normalizedScope = string.IsNullOrWhiteSpace(scope) ? "default" : scope;
var normalizedRemote = string.IsNullOrWhiteSpace(remoteEndPoint) ? "default" : remoteEndPoint;
return normalizedScope + "|" + normalizedRemote;
}
private static TransportApplicationSessionSnapshot Clone(TransportApplicationSessionSnapshot snapshot)
{
if (snapshot == null)
{
return null;
}
return new TransportApplicationSessionSnapshot
{
Scope = snapshot.Scope,
RemoteEndPoint = snapshot.RemoteEndPoint,
ConnectionState = snapshot.ConnectionState,
CanSendHeartbeat = snapshot.CanSendHeartbeat,
LastRoundTripTimeMs = snapshot.LastRoundTripTimeMs,
LastFailureReason = snapshot.LastFailureReason,
LastLivenessUtc = snapshot.LastLivenessUtc,
LastHeartbeatSentUtc = snapshot.LastHeartbeatSentUtc,
NextReconnectAtUtc = snapshot.NextReconnectAtUtc,
CurrentServerTick = snapshot.CurrentServerTick,
ObservedAtUtc = snapshot.ObservedAtUtc
};
}
private PeerAccumulator GetOrCreatePeer(IPEndPoint remoteEndPoint)
{
var key = FormatEndPoint(remoteEndPoint) ?? "unknown";
@@ -485,8 +1027,30 @@ namespace Network.NetworkTransport
public long DatagramBytesSent { get; set; }
public long DatagramsReceived { get; set; }
public long DatagramBytesReceived { get; set; }
public string SessionLifecycleState { get; set; } = "active";
public TransportSessionDiagnosticsSnapshot SessionDiagnostics { get; private set; } = new();
public int PeakSmoothedRttMs { get; private set; }
public int PeakRetransmissionTimeoutMs { get; private set; }
public int PeakWaitSendCount { get; private set; }
public int PeakRetransmittedSegmentsInFlight { get; private set; }
public long ObservedRetransmissionSends { get; private set; }
public long ObservedLossSignals { get; private set; }
public Dictionary<string, long> ErrorCountsByStage { get; } = new(StringComparer.Ordinal);
public void RecordDiagnostics(TransportSessionDiagnosticsSnapshot diagnostics)
{
SessionLifecycleState = string.IsNullOrWhiteSpace(diagnostics.LifecycleState)
? SessionLifecycleState
: diagnostics.LifecycleState;
SessionDiagnostics = diagnostics;
PeakSmoothedRttMs = Math.Max(PeakSmoothedRttMs, diagnostics.SmoothedRttMs);
PeakRetransmissionTimeoutMs = Math.Max(PeakRetransmissionTimeoutMs, diagnostics.RetransmissionTimeoutMs);
PeakWaitSendCount = Math.Max(PeakWaitSendCount, diagnostics.WaitSendCount);
PeakRetransmittedSegmentsInFlight = Math.Max(PeakRetransmittedSegmentsInFlight, diagnostics.RetransmittedSegmentsInFlight);
ObservedRetransmissionSends = Math.Max(ObservedRetransmissionSends, diagnostics.ObservedRetransmissionSends);
ObservedLossSignals = Math.Max(ObservedLossSignals, diagnostics.ObservedLossSignals);
}
public TransportPeerMetricsSnapshot ToSnapshot()
{
return new TransportPeerMetricsSnapshot
@@ -504,6 +1068,14 @@ namespace Network.NetworkTransport
DatagramBytesSent = DatagramBytesSent,
DatagramsReceived = DatagramsReceived,
DatagramBytesReceived = DatagramBytesReceived,
SessionLifecycleState = SessionLifecycleState,
SessionDiagnostics = SessionDiagnostics,
PeakSmoothedRttMs = PeakSmoothedRttMs,
PeakRetransmissionTimeoutMs = PeakRetransmissionTimeoutMs,
PeakWaitSendCount = PeakWaitSendCount,
PeakRetransmittedSegmentsInFlight = PeakRetransmittedSegmentsInFlight,
ObservedRetransmissionSends = ObservedRetransmissionSends,
ObservedLossSignals = ObservedLossSignals,
ErrorCountsByStage = ErrorCountsByStage.ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.Ordinal)
};
}
@@ -0,0 +1,26 @@
using System;
using System.IO;
namespace Network.NetworkTransport
{
public sealed class TransportMetricsOptions
{
public static TransportMetricsOptions Default { get; } = new();
public string ReportDirectory { get; set; }
public TextWriter ConsoleWriter { get; set; }
public bool WriteJsonReport { get; set; } = true;
public bool WriteTextSummaryReport { get; set; } = true;
public bool WriteDiagnosisReport { get; set; } = true;
public bool EmitConsoleSummary { get; set; } = true;
public int MaxPeerSummariesInTextReport { get; set; } = 5;
public int MaxPeerSummariesInConsole { get; set; } = 3;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 34979a7bd532d8e4b8f80a2f9fcfdea2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,44 @@
using System;
using System.IO;
using System.Linq;
namespace Network.NetworkTransport
{
public static class TransportMetricsReportLocator
{
public static string GetDefaultReportDirectory()
{
return Path.Combine(Directory.GetCurrentDirectory(), "Logs", "transport-metrics");
}
public static string TryGetLatestDiagnosisPath(string reportDirectory = null)
{
var directory = string.IsNullOrWhiteSpace(reportDirectory)
? GetDefaultReportDirectory()
: reportDirectory;
if (!Directory.Exists(directory))
{
return null;
}
return new DirectoryInfo(directory)
.GetFiles("*.diagnosis.txt", SearchOption.TopDirectoryOnly)
.OrderByDescending(file => file.LastWriteTimeUtc)
.ThenByDescending(file => file.Name, StringComparer.Ordinal)
.Select(file => file.FullName)
.FirstOrDefault();
}
public static string ReadLatestDiagnosisText(string reportDirectory = null)
{
var path = TryGetLatestDiagnosisPath(reportDirectory);
if (string.IsNullOrWhiteSpace(path) || !File.Exists(path))
{
return null;
}
return File.ReadAllText(path);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2858757643e325e409331d4c4df1f956
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: