From 70655ffa07ddaa614077f2bf3ef9d45aa2dd6c5a Mon Sep 17 00:00:00 2001 From: Roman Ettlinger Date: Mon, 19 Oct 2026 14:32:58 +0200 Subject: [PATCH] Client channel: decode a single-chunk response without a chunk collection A final chunk was saved into a new BufferCollection and taken back out even when it was the whole response. When no chunks of the message are saved, the partial message is open and the chunk is within the limits, the response is now decoded from the chunk directly and its buffer returned afterwards; anything else takes the saved-chunks path as before. Co-Authored-By: Claude Opus 5.5 --- .../Stack/Tcp/UaSCBinaryChannel.cs | 26 +++++++++++++++++++ .../Stack/Tcp/UaSCBinaryClientChannel.cs | 19 ++++++++++---- 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.cs b/src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.cs index 880d7a0af1..eb6ed265ad 100644 --- a/src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.cs +++ b/src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.cs @@ -820,6 +820,32 @@ namespace Opc.Ua.Bindings return TakeSavedChunks(); } + /// + /// Whether a final chunk is a whole message by itself, which most are: no + /// chunks of the message are saved, the partial message is not closed and + /// the chunk is within the message limits. The caller then processes the + /// chunk directly instead of passing it to , + /// and keeps owning its buffer. + /// + /// The body of the final chunk. + /// Whether the chunk is part of a request. + protected bool IsSingleChunkMessage(ArraySegment chunk, bool isServerContext) + { + if (chunk.Array == null) + { + return false; + } + lock (m_partialMessageLock) + { + if (m_partialMessageClosed || m_partialMessageChunks != null) + { + return false; + } + } + // a chunk over the limits takes the path that reports them. + return !MessageLimitsExceeded(isServerContext, chunk.Count, 1); + } + /// /// Detaches the chunks saved so far without offering another one. Used /// where the final chunk has already been saved and only the collection diff --git a/src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryClientChannel.cs b/src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryClientChannel.cs index 297e827beb..b39c58895d 100644 --- a/src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryClientChannel.cs +++ b/src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryClientChannel.cs @@ -2033,6 +2033,7 @@ namespace Opc.Ua.Bindings } BufferCollection? chunksToProcess = null; + ArraySegment singleChunk = default; try { // check for an abort. @@ -2062,11 +2063,18 @@ namespace Opc.Ua.Bindings return true; } - // get the chunks to process. - chunksToProcess = GetSavedChunks(requestId, messageBody, false, gateHeld: false); - - // get response. - operation.MessageBody = ParseResponse(chunksToProcess); + // get response: a response in one chunk, which most are, is decoded + // from that chunk without a collection of the chunks. + if (IsSingleChunkMessage(messageBody, isServerContext: false)) + { + singleChunk = messageBody; + operation.MessageBody = DecodeMessage(singleChunk, Quotas.MessageContext); + } + else + { + chunksToProcess = GetSavedChunks(requestId, messageBody, false, gateHeld: false); + operation.MessageBody = ParseResponse(chunksToProcess); + } if (operation.MessageBody == null) { @@ -2095,6 +2103,7 @@ namespace Opc.Ua.Bindings finally { chunksToProcess?.Release(BufferManager, "ProcessResponseMessage"); + ReturnBuffer(singleChunk, "ProcessResponseMessage"); } }