From b3b8313e9cf8b2691e95c45d7ef6b85d3d60a067 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 14 May 2026 09:30:48 -0400 Subject: [PATCH] =?UTF-8?q?mbproxy:=20Wave=206=20=E2=80=94=20wire=20ProxyC?= =?UTF-8?q?ounters.AddBytes=20(bytes=20counters=20were=20always=200)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 10-min stress test (1.46 M PDUs through the proxy) revealed that status.json's bytes.upstreamIn / bytes.upstreamOut counters always read 0 because ProxyCounters.AddBytes was defined but never called from anywhere. Same shape as the original review's W2.22 finding (counter wired in DTO + HTML but no increment site), missed for the bytes counters specifically. Wired five increment sites in PlcMultiplexer: OnUpstreamFrameAsync (request side, parsed frame) AddBytes(up: frame.Length, down: 0) — counted ONCE per parsed frame regardless of subsequent routing (cache hit, coalesce, backend round-trip, exception). RunBackendReaderAsync fan-out (response side, after TrySendResponse=true) AddBytes(up: 0, down: outFrame.Length) per delivered party. With coalescing, one backend response fans out to N parties and produces N × frame.Length bytes leaving the proxy upstream-side. Drops (TrySendResponse=false) increment ResponseDropForFullUpstream instead. Cache hit path AddBytes(up: 0, down: hitFrame.Length) for the BuildCacheHitFrame response (no backend round-trip but still bytes leaving the proxy). Saturation cleanup (W1.2 path, both branches) AddBytes(up: 0, down: excFrame.Length) per delivered exception 0x04. Non-coalescing-path saturation AddBytes(up: 0, down: excFrame.Length) for the single exception 0x04. Watchdog timeout exception delivery AddBytes(up: 0, down: excFrame.Length) per delivered exception 0x0B. Backend-side bytes (proxy ↔ PLC) are NOT counted by these counters — the field name is `BytesUpstreamIn/Out` which is upstream-only by contract. Tests: 387 pass / 0 fail. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Proxy/Multiplexing/PlcMultiplexer.cs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) 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 {