1085 lines
55 KiB
C#
1085 lines
55 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.IO;
|
||
using System.Linq;
|
||
using System.Net;
|
||
using System.Runtime.Serialization;
|
||
using System.Runtime.Serialization.Json;
|
||
using System.Text;
|
||
|
||
namespace Network.NetworkTransport
|
||
{
|
||
public interface ITransportMetricsModule
|
||
{
|
||
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);
|
||
void RecordDatagramReceived(IPEndPoint remoteEndPoint, int bytes);
|
||
void RecordError(string stage, IPEndPoint remoteEndPoint, string detail = null);
|
||
TransportMetricsSnapshot GetCurrentSnapshot();
|
||
TransportMetricsSnapshot CompleteRun();
|
||
}
|
||
|
||
public interface ITransportMetricsSink
|
||
{
|
||
void RecordApplicationSessionSnapshot(TransportApplicationSessionSnapshot snapshot);
|
||
}
|
||
|
||
public sealed class TransportRunDescriptor
|
||
{
|
||
public TransportRunDescriptor(string transportName, bool isServer, IPEndPoint defaultRemoteEndPoint = null)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(transportName))
|
||
{
|
||
throw new ArgumentException("Transport name is required.", nameof(transportName));
|
||
}
|
||
|
||
TransportName = transportName;
|
||
IsServer = isServer;
|
||
DefaultRemoteEndPoint = defaultRemoteEndPoint == null
|
||
? null
|
||
: new IPEndPoint(defaultRemoteEndPoint.Address, defaultRemoteEndPoint.Port);
|
||
}
|
||
|
||
public string TransportName { get; }
|
||
public bool IsServer { get; }
|
||
public IPEndPoint DefaultRemoteEndPoint { get; }
|
||
}
|
||
|
||
[DataContract]
|
||
public sealed class TransportMetricsSnapshot
|
||
{
|
||
[DataMember(Order = 1)] public string RunId { get; set; }
|
||
[DataMember(Order = 2)] public string TransportName { get; set; }
|
||
[DataMember(Order = 3)] public string Mode { get; set; }
|
||
[DataMember(Order = 4)] public string DefaultRemoteEndPoint { get; set; }
|
||
[DataMember(Order = 5)] public DateTimeOffset? StartedAtUtc { get; set; }
|
||
[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 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]
|
||
public sealed class TransportPeerMetricsSnapshot
|
||
{
|
||
[DataMember(Order = 1)] public string RemoteEndPoint { get; set; }
|
||
[DataMember(Order = 2)] public DateTimeOffset? FirstSeenUtc { get; set; }
|
||
[DataMember(Order = 3)] public DateTimeOffset? LastActivityUtc { get; set; }
|
||
[DataMember(Order = 4)] public long SessionOpens { get; set; }
|
||
[DataMember(Order = 5)] public long SessionCloses { get; set; }
|
||
[DataMember(Order = 6)] public long PayloadMessagesSent { get; set; }
|
||
[DataMember(Order = 7)] public long PayloadBytesSent { get; set; }
|
||
[DataMember(Order = 8)] public long PayloadMessagesReceived { get; set; }
|
||
[DataMember(Order = 9)] public long PayloadBytesReceived { get; set; }
|
||
[DataMember(Order = 10)] public long DatagramsSent { get; set; }
|
||
[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 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
|
||
{
|
||
private readonly object gate = new();
|
||
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;
|
||
private string transportName;
|
||
private string mode;
|
||
private string defaultRemoteEndPoint;
|
||
private DateTimeOffset? startedAtUtc;
|
||
private DateTimeOffset? completedAtUtc;
|
||
private string reportPath;
|
||
private bool hasRun;
|
||
private bool completed;
|
||
private long sessionsCreated;
|
||
private long sessionsClosed;
|
||
private int activeSessions;
|
||
private int peakActiveSessions;
|
||
private long payloadMessagesSent;
|
||
private long payloadBytesSent;
|
||
private long payloadMessagesReceived;
|
||
private long payloadBytesReceived;
|
||
private long datagramsSent;
|
||
private long datagramBytesSent;
|
||
private long datagramsReceived;
|
||
private long datagramBytesReceived;
|
||
private long sendErrors;
|
||
private long receiveErrors;
|
||
private long otherErrors;
|
||
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)
|
||
{
|
||
if (descriptor == null)
|
||
{
|
||
throw new ArgumentNullException(nameof(descriptor));
|
||
}
|
||
|
||
lock (gate)
|
||
{
|
||
peers.Clear();
|
||
applicationSessions.Clear();
|
||
errorCountsByStage.Clear();
|
||
runId = Guid.NewGuid().ToString("N");
|
||
transportName = descriptor.TransportName;
|
||
mode = descriptor.IsServer ? "server" : "client";
|
||
defaultRemoteEndPoint = FormatEndPoint(descriptor.DefaultRemoteEndPoint);
|
||
startedAtUtc = utcNowProvider();
|
||
completedAtUtc = null;
|
||
reportPath = null;
|
||
hasRun = true;
|
||
completed = false;
|
||
sessionsCreated = 0;
|
||
sessionsClosed = 0;
|
||
activeSessions = 0;
|
||
peakActiveSessions = 0;
|
||
payloadMessagesSent = 0;
|
||
payloadBytesSent = 0;
|
||
payloadMessagesReceived = 0;
|
||
payloadBytesReceived = 0;
|
||
datagramsSent = 0;
|
||
datagramBytesSent = 0;
|
||
datagramsReceived = 0;
|
||
datagramBytesReceived = 0;
|
||
sendErrors = 0;
|
||
receiveErrors = 0;
|
||
otherErrors = 0;
|
||
completedSnapshot = null;
|
||
}
|
||
}
|
||
|
||
public void RecordSessionOpened(IPEndPoint remoteEndPoint) => Update(remoteEndPoint, peer =>
|
||
{
|
||
sessionsCreated++;
|
||
activeSessions++;
|
||
peakActiveSessions = Math.Max(peakActiveSessions, activeSessions);
|
||
peer.SessionOpens++;
|
||
peer.SessionLifecycleState = "active";
|
||
});
|
||
|
||
public void RecordSessionClosed(IPEndPoint remoteEndPoint) => Update(remoteEndPoint, peer =>
|
||
{
|
||
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++;
|
||
payloadBytesSent += bytes;
|
||
peer.PayloadMessagesSent++;
|
||
peer.PayloadBytesSent += bytes;
|
||
});
|
||
|
||
public void RecordPayloadReceived(IPEndPoint remoteEndPoint, int bytes) => Update(remoteEndPoint, peer =>
|
||
{
|
||
payloadMessagesReceived++;
|
||
payloadBytesReceived += bytes;
|
||
peer.PayloadMessagesReceived++;
|
||
peer.PayloadBytesReceived += bytes;
|
||
});
|
||
|
||
public void RecordDatagramSent(IPEndPoint remoteEndPoint, int bytes) => Update(remoteEndPoint, peer =>
|
||
{
|
||
datagramsSent++;
|
||
datagramBytesSent += bytes;
|
||
peer.DatagramsSent++;
|
||
peer.DatagramBytesSent += bytes;
|
||
});
|
||
|
||
public void RecordDatagramReceived(IPEndPoint remoteEndPoint, int bytes) => Update(remoteEndPoint, peer =>
|
||
{
|
||
datagramsReceived++;
|
||
datagramBytesReceived += bytes;
|
||
peer.DatagramsReceived++;
|
||
peer.DatagramBytesReceived += bytes;
|
||
});
|
||
|
||
public void RecordError(string stage, IPEndPoint remoteEndPoint, string detail = null)
|
||
{
|
||
lock (gate)
|
||
{
|
||
if (!hasRun)
|
||
{
|
||
return;
|
||
}
|
||
|
||
Increment(errorCountsByStage, stage);
|
||
if (remoteEndPoint != null)
|
||
{
|
||
var peer = GetOrCreatePeer(remoteEndPoint);
|
||
Increment(peer.ErrorCountsByStage, stage);
|
||
peer.LastActivityUtc = utcNowProvider();
|
||
}
|
||
|
||
if (!string.IsNullOrWhiteSpace(stage) && stage.IndexOf("send", StringComparison.OrdinalIgnoreCase) >= 0)
|
||
{
|
||
sendErrors++;
|
||
}
|
||
else if (!string.IsNullOrWhiteSpace(stage) && (stage.IndexOf("receive", StringComparison.OrdinalIgnoreCase) >= 0 || stage.IndexOf("recv", StringComparison.OrdinalIgnoreCase) >= 0 || stage.IndexOf("input", StringComparison.OrdinalIgnoreCase) >= 0))
|
||
{
|
||
receiveErrors++;
|
||
}
|
||
else
|
||
{
|
||
otherErrors++;
|
||
}
|
||
|
||
if (!string.IsNullOrWhiteSpace(detail))
|
||
{
|
||
consoleWriter.WriteLine($"[TransportMetrics] {stage}: {detail}");
|
||
}
|
||
}
|
||
}
|
||
|
||
public TransportMetricsSnapshot GetCurrentSnapshot()
|
||
{
|
||
lock (gate)
|
||
{
|
||
return completed && completedSnapshot != null ? completedSnapshot : BuildSnapshot();
|
||
}
|
||
}
|
||
|
||
public TransportMetricsSnapshot CompleteRun()
|
||
{
|
||
lock (gate)
|
||
{
|
||
if (!hasRun)
|
||
{
|
||
return completedSnapshot ?? new TransportMetricsSnapshot();
|
||
}
|
||
|
||
if (completed && completedSnapshot != null)
|
||
{
|
||
return completedSnapshot;
|
||
}
|
||
|
||
completedAtUtc = utcNowProvider();
|
||
completed = true;
|
||
completedSnapshot = BuildSnapshot();
|
||
completedSnapshot.ReportPath = WriteJsonReport(completedSnapshot);
|
||
completedSnapshot.SummaryPath = WriteTextSummaryReport(completedSnapshot);
|
||
WriteDiagnosisReport(completedSnapshot);
|
||
reportPath = completedSnapshot.ReportPath;
|
||
if (emitConsoleSummary)
|
||
{
|
||
WriteConsoleSummary(completedSnapshot);
|
||
}
|
||
|
||
return completedSnapshot;
|
||
}
|
||
}
|
||
|
||
private void Update(IPEndPoint remoteEndPoint, Action<PeerAccumulator> update)
|
||
{
|
||
lock (gate)
|
||
{
|
||
if (!hasRun)
|
||
{
|
||
return;
|
||
}
|
||
|
||
var peer = GetOrCreatePeer(remoteEndPoint);
|
||
update(peer);
|
||
peer.LastActivityUtc = utcNowProvider();
|
||
}
|
||
}
|
||
|
||
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,
|
||
TransportName = transportName,
|
||
Mode = mode,
|
||
DefaultRemoteEndPoint = defaultRemoteEndPoint,
|
||
StartedAtUtc = startedAtUtc,
|
||
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,
|
||
SessionsClosed = sessionsClosed,
|
||
PayloadMessagesSent = payloadMessagesSent,
|
||
PayloadBytesSent = payloadBytesSent,
|
||
PayloadMessagesReceived = payloadMessagesReceived,
|
||
PayloadBytesReceived = payloadBytesReceived,
|
||
DatagramsSent = datagramsSent,
|
||
DatagramBytesSent = datagramBytesSent,
|
||
DatagramsReceived = datagramsReceived,
|
||
DatagramBytesReceived = datagramBytesReceived,
|
||
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 = 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);
|
||
var timestamp = (snapshot.CompletedAtUtc ?? utcNowProvider()).ToString("yyyyMMdd-HHmmssfff");
|
||
var filePath = Path.Combine(reportDirectory, $"{snapshot.Mode}-{snapshot.TransportName}-{timestamp}-{snapshot.RunId}.json");
|
||
using var stream = new MemoryStream();
|
||
var serializer = new DataContractJsonSerializer(typeof(TransportMetricsSnapshot));
|
||
serializer.WriteObject(stream, snapshot);
|
||
var json = Encoding.UTF8.GetString(stream.ToArray());
|
||
File.WriteAllText(filePath, FormatJson(json), new UTF8Encoding(false));
|
||
return filePath;
|
||
}
|
||
catch (Exception exception)
|
||
{
|
||
consoleWriter.WriteLine($"[TransportMetrics] Failed to write report: {exception.Message}");
|
||
return null;
|
||
}
|
||
}
|
||
|
||
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";
|
||
if (!peers.TryGetValue(key, out var peer))
|
||
{
|
||
peer = new PeerAccumulator(key, utcNowProvider());
|
||
peers.Add(key, peer);
|
||
}
|
||
|
||
return peer;
|
||
}
|
||
|
||
private static string FormatEndPoint(IPEndPoint remoteEndPoint)
|
||
{
|
||
return remoteEndPoint == null ? null : new IPEndPoint(remoteEndPoint.Address, remoteEndPoint.Port).ToString();
|
||
}
|
||
|
||
private static void Increment(IDictionary<string, long> counts, string stage)
|
||
{
|
||
var key = string.IsNullOrWhiteSpace(stage) ? "unknown" : stage;
|
||
counts.TryGetValue(key, out var current);
|
||
counts[key] = current + 1;
|
||
}
|
||
|
||
private static string FormatJson(string json)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(json))
|
||
{
|
||
return json;
|
||
}
|
||
|
||
var builder = new StringBuilder(json.Length + 128);
|
||
var indentation = 0;
|
||
var inString = false;
|
||
var isEscaped = false;
|
||
|
||
foreach (var character in json)
|
||
{
|
||
if (isEscaped)
|
||
{
|
||
builder.Append(character);
|
||
isEscaped = false;
|
||
continue;
|
||
}
|
||
|
||
if (character == '\\' && inString)
|
||
{
|
||
builder.Append(character);
|
||
isEscaped = true;
|
||
continue;
|
||
}
|
||
|
||
if (character == '"')
|
||
{
|
||
inString = !inString;
|
||
builder.Append(character);
|
||
continue;
|
||
}
|
||
|
||
if (inString)
|
||
{
|
||
builder.Append(character);
|
||
continue;
|
||
}
|
||
|
||
switch (character)
|
||
{
|
||
case '{':
|
||
case '[':
|
||
builder.Append(character);
|
||
builder.AppendLine();
|
||
indentation++;
|
||
builder.Append(new string(' ', indentation * 2));
|
||
break;
|
||
case '}':
|
||
case ']':
|
||
builder.AppendLine();
|
||
indentation = Math.Max(0, indentation - 1);
|
||
builder.Append(new string(' ', indentation * 2));
|
||
builder.Append(character);
|
||
break;
|
||
case ',':
|
||
builder.Append(character);
|
||
builder.AppendLine();
|
||
builder.Append(new string(' ', indentation * 2));
|
||
break;
|
||
case ':':
|
||
builder.Append(": ");
|
||
break;
|
||
default:
|
||
if (!char.IsWhiteSpace(character))
|
||
{
|
||
builder.Append(character);
|
||
}
|
||
|
||
break;
|
||
}
|
||
}
|
||
|
||
return builder.ToString();
|
||
}
|
||
|
||
private sealed class PeerAccumulator
|
||
{
|
||
public PeerAccumulator(string remoteEndPoint, DateTimeOffset now)
|
||
{
|
||
RemoteEndPoint = remoteEndPoint;
|
||
FirstSeenUtc = now;
|
||
LastActivityUtc = now;
|
||
}
|
||
|
||
public string RemoteEndPoint { get; }
|
||
public DateTimeOffset FirstSeenUtc { get; }
|
||
public DateTimeOffset LastActivityUtc { get; set; }
|
||
public long SessionOpens { get; set; }
|
||
public long SessionCloses { get; set; }
|
||
public long PayloadMessagesSent { get; set; }
|
||
public long PayloadBytesSent { get; set; }
|
||
public long PayloadMessagesReceived { get; set; }
|
||
public long PayloadBytesReceived { get; set; }
|
||
public long DatagramsSent { get; set; }
|
||
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
|
||
{
|
||
RemoteEndPoint = RemoteEndPoint,
|
||
FirstSeenUtc = FirstSeenUtc,
|
||
LastActivityUtc = LastActivityUtc,
|
||
SessionOpens = SessionOpens,
|
||
SessionCloses = SessionCloses,
|
||
PayloadMessagesSent = PayloadMessagesSent,
|
||
PayloadBytesSent = PayloadBytesSent,
|
||
PayloadMessagesReceived = PayloadMessagesReceived,
|
||
PayloadBytesReceived = PayloadBytesReceived,
|
||
DatagramsSent = DatagramsSent,
|
||
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)
|
||
};
|
||
}
|
||
}
|
||
}
|
||
}
|