diff --git a/mbproxy/src/Mbproxy/Proxy/Multiplexing/PlcMultiplexer.cs b/mbproxy/src/Mbproxy/Proxy/Multiplexing/PlcMultiplexer.cs index 29b8551..b1cb7e2 100644 --- a/mbproxy/src/Mbproxy/Proxy/Multiplexing/PlcMultiplexer.cs +++ b/mbproxy/src/Mbproxy/Proxy/Multiplexing/PlcMultiplexer.cs @@ -690,6 +690,13 @@ internal sealed class PlcMultiplexer : IAsyncDisposable, IMultiplexCountersProvi { _ctx.Counters.IncrementResponseDropForFullUpstream(); } + else + { + // Phase 12 (W6) — count outbound bytes per delivered party. + // With coalescing, one backend response fans out to N parties and + // produces N × frame.Length bytes leaving the proxy upstream-side. + _ctx.Counters.AddBytes(up: 0, down: outFrame.Length); + } } } @@ -723,6 +730,11 @@ internal sealed class PlcMultiplexer : IAsyncDisposable, IMultiplexCountersProvi out ushort originalTxId, out _, out _, out byte unitId)) return; + // Phase 12 (W6) — count inbound bytes from the upstream client. Surfaces in + // bytes.upstreamIn on the status page. Counted ONCE per parsed frame regardless + // of subsequent routing (cache hit, coalesce, backend round-trip, exception). + _ctx.Counters.AddBytes(up: frame.Length, down: 0); + // Parse the PDU FC + start/qty. FC03/FC04 reads use start/qty for the coalescing key // and (Phase 11) for the cache lookup. FC06 writes carry [addr][value]; we treat qty // as 1 for invalidation. FC16 carries [start][qty][byteCount]...; qty is the write @@ -771,6 +783,8 @@ internal sealed class PlcMultiplexer : IAsyncDisposable, IMultiplexCountersProvi byte[] hitFrame = BuildCacheHitFrame(originalTxId, unitId, cached.PduBytes); await pipe.SendResponseAsync(hitFrame, ct).ConfigureAwait(false); + // Phase 12 (W6) — outbound bytes for cache-hit response. + _ctx.Counters.AddBytes(up: 0, down: hitFrame.Length); return; } @@ -905,6 +919,8 @@ internal sealed class PlcMultiplexer : IAsyncDisposable, IMultiplexCountersProvi byte[] excFrame = BuildExceptionFrame(party.OriginalTxId, unitId, fcByte, exceptionCode: 4); if (!party.Pipe.TrySendResponse(excFrame)) _ctx.Counters.IncrementResponseDropForFullUpstream(); + else + _ctx.Counters.AddBytes(up: 0, down: excFrame.Length); // W6 } } else @@ -914,6 +930,8 @@ internal sealed class PlcMultiplexer : IAsyncDisposable, IMultiplexCountersProvi byte[] excFrame = BuildExceptionFrame(originalTxId, unitId, fcByte, exceptionCode: 4); if (!pipe.TrySendResponse(excFrame)) _ctx.Counters.IncrementResponseDropForFullUpstream(); + else + _ctx.Counters.AddBytes(up: 0, down: excFrame.Length); // W6 } return; } @@ -953,6 +971,7 @@ internal sealed class PlcMultiplexer : IAsyncDisposable, IMultiplexCountersProvi MultiplexerLogEvents.Saturated(_logger, _plc.Name, pipe.RemoteEp?.ToString() ?? "?"); byte[] excFrame = BuildExceptionFrame(originalTxId, unitId, fcByte, exceptionCode: 4); await pipe.SendResponseAsync(excFrame, ct).ConfigureAwait(false); + _ctx.Counters.AddBytes(up: 0, down: excFrame.Length); // W6 return; } @@ -1078,6 +1097,7 @@ internal sealed class PlcMultiplexer : IAsyncDisposable, IMultiplexCountersProvi try { await party.Pipe.SendResponseAsync(excFrame, ct).ConfigureAwait(false); + _ctx.Counters.AddBytes(up: 0, down: excFrame.Length); // W6 } catch {