diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport/Cip/CipConnection.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport/Cip/CipConnection.cs index c9c87752..5004d731 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport/Cip/CipConnection.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport/Cip/CipConnection.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Eip; namespace ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Cip; @@ -23,9 +24,55 @@ public sealed class CipConnection : ICipChannel, IAsyncDisposable /// Forward Open frames sent by every connection in the process (fallback included). Gate diagnostics. internal static int ForwardOpenCount; + /// Forward Close requests put on the wire by every connection in the process. Gate diagnostics. + internal static int ForwardClosesSent; + + /// Forward Close requests that got a reply within their budget. Gate diagnostics. + internal static int ForwardClosesAcked; + /// Channel sends by every connection in the process (keep-alives included). Gate diagnostics. internal static int RequestsSent; + // Round-trip statistics of completed exchanges, process-wide (gate diagnostics: T21 latency attribution). + // Two Stopwatch reads and four interlocked ops per request; reset by the harness between timed passes. + private static long _exchangeCount; + private static long _exchangeTotalTicks; + private static long _exchangeMinTicks = long.MaxValue; + private static long _exchangeMaxTicks; + + /// Round-trip statistics of the exchanges completed since the last . + internal readonly record struct ExchangeStats(long Count, double MinMs, double MaxMs, double MeanMs); + + /// Clears the process-wide round-trip statistics. Gate diagnostics. + internal static void ResetExchangeStats() + { + Interlocked.Exchange(ref _exchangeCount, 0); + Interlocked.Exchange(ref _exchangeTotalTicks, 0); + Interlocked.Exchange(ref _exchangeMinTicks, long.MaxValue); + Interlocked.Exchange(ref _exchangeMaxTicks, 0); + } + + /// Snapshot of the process-wide round-trip statistics. Gate diagnostics. + internal static ExchangeStats ReadExchangeStats() + { + var count = Interlocked.Read(ref _exchangeCount); + if (count == 0) return new ExchangeStats(0, 0, 0, 0); + static double Ms(long ticks) => ticks * 1000.0 / Stopwatch.Frequency; + return new ExchangeStats(count, Ms(Interlocked.Read(ref _exchangeMinTicks)), Ms(Interlocked.Read(ref _exchangeMaxTicks)), + Ms(Interlocked.Read(ref _exchangeTotalTicks)) / count); + } + + private static void RecordExchange(long ticks) + { + Interlocked.Increment(ref _exchangeCount); + Interlocked.Add(ref _exchangeTotalTicks, ticks); + long seen; + while (ticks < (seen = Interlocked.Read(ref _exchangeMinTicks)) + && Interlocked.CompareExchange(ref _exchangeMinTicks, ticks, seen) != seen) { } + while (ticks > (seen = Interlocked.Read(ref _exchangeMaxTicks)) + && Interlocked.CompareExchange(ref _exchangeMaxTicks, ticks, seen) != seen) { } + } + /// Open attempts (TCP connect + RegisterSession + Forward Open, a fallback open included) started by /// every connection in the process — counted before any I/O, so a refused attempt counts too. Gate diagnostics. internal static int OpenAttempts; @@ -246,14 +293,17 @@ public sealed class CipConnection : ICipChannel, IAsyncDisposable Interlocked.Exchange(ref _lastSendTimestamp, _time.GetTimestamp()); using var deadline = new CancellationTokenSource(ExchangeBudget, _time); using var linked = CancellationTokenSource.CreateLinkedTokenSource(ct, deadline.Token); + var started = Stopwatch.GetTimestamp(); try { - return _o.Routing switch + var reply = _o.Routing switch { CipRouting.Connected => await open.Client.SendUnitDataAsync(open.Conn!.OTConnectionId, cip, _o.Timeout, linked.Token).ConfigureAwait(false), CipRouting.UnconnectedSend => await open.Client.SendRRDataAsync(ConnectionManagerServices.UnconnectedSend(cip.Span, _route), _o.Timeout, linked.Token).ConfigureAwait(false), _ => await open.Client.SendRRDataAsync(cip, _o.Timeout, linked.Token).ConfigureAwait(false), }; + RecordExchange(Stopwatch.GetTimestamp() - started); + return reply; } catch (OperationCanceledException) when (!ct.IsCancellationRequested && deadline.IsCancellationRequested) { @@ -346,9 +396,11 @@ public sealed class CipConnection : ICipChannel, IAsyncDisposable { var budget = TimeSpan.FromSeconds(1); using var cts = new CancellationTokenSource(budget, _time); + Interlocked.Increment(ref ForwardClosesSent); await open.Client.SendRRDataAsync( ConnectionManagerServices.ForwardClose(open.ConnectionSerial, _o.VendorId, _o.OriginatorSerial, _route), budget, cts.Token).ConfigureAwait(false); + Interlocked.Increment(ref ForwardClosesAcked); } catch (Exception ex) when (ex is AbTransportException or OperationCanceledException) {