From 939ce9c39b30b0b0494ab3ca30da6aee7a622774 Mon Sep 17 00:00:00 2001 From: Loretta Date: Mon, 20 Apr 2026 14:20:34 +0200 Subject: [PATCH] Refactor logging and unify argument deserialization - Simplified logging with null-conditional operators - Temporarily disabled WASM AsyncSegment guard for testing - Unified argument deserialization via GetArgBytes for zero-copy and pooled buffer support - Removed DeserializeFromSequence in favor of new approach - Applied improvements to AyCodeBinaryHubProtocol - Updated comments and performed minor code cleanup --- .../SignalRs/AcBinaryHubProtocol.cs | 110 ++++++++++-------- .../SignalRs/AyCodeBinaryHubProtocol.cs | 16 ++- 2 files changed, 69 insertions(+), 57 deletions(-) diff --git a/AyCode.Services/SignalRs/AcBinaryHubProtocol.cs b/AyCode.Services/SignalRs/AcBinaryHubProtocol.cs index 782054d..e26b953 100644 --- a/AyCode.Services/SignalRs/AcBinaryHubProtocol.cs +++ b/AyCode.Services/SignalRs/AcBinaryHubProtocol.cs @@ -115,10 +115,14 @@ public class AcBinaryHubProtocol : IHubProtocol // Send-side guard: AsyncSegment uses AsyncPipeWriterOutput whose sync-over-async flush // would block the browser's single UI thread. The receive side converts chunked wire // to a synchronous deserialize on WASM automatically (see TryParseChunkData). - if (IsBrowser && protocolMode == BinaryProtocolMode.AsyncSegment) - throw new PlatformNotSupportedException( - "BinaryProtocolMode.AsyncSegment is not supported on WebAssembly. " + - "Use BinaryProtocolMode.Bytes or BinaryProtocolMode.Segment instead."); + // + // TEMP: commented out to test AsyncSegment on both Windows app and WASM without rebuild. + // Small WASM payloads work; larger ones may deadlock on sync-over-async FlushAsync. + // Restore once BinaryProtocolMode is runtime-configurable in Program.cs. + //if (IsBrowser && protocolMode == BinaryProtocolMode.AsyncSegment) + // throw new PlatformNotSupportedException( + // "BinaryProtocolMode.AsyncSegment is not supported on WebAssembly. " + + // "Use BinaryProtocolMode.Bytes or BinaryProtocolMode.Segment instead."); _options = options; _options.BufferWriterChunkSize = 4096; @@ -208,10 +212,7 @@ public class AcBinaryHubProtocol : IHubProtocol public void WriteMessage(HubMessage message, IBufferWriter output) { - if (_logger != null) - { - _logger.LogInformation("Serialize start"); - } + _logger?.LogInformation("Serialize start"); // AsyncSegment: chunked protocol framing for messages with streamable arguments if (_protocolMode == BinaryProtocolMode.AsyncSegment @@ -277,13 +278,10 @@ public class AcBinaryHubProtocol : IHubProtocol bw.Flush(); Unsafe.WriteUnaligned(ref lengthSpan[0], totalPayload); - if (_logger != null) - { - _logger.LogInformation("Serialize end totalSentSize={TotalSentSize}", LengthPrefixSize + totalPayload); + _logger?.LogInformation("Serialize end totalSentSize={TotalSentSize}", LengthPrefixSize + totalPayload); - if (_logger.IsEnabled(LogLevel.Debug)) - _logger.LogDebug("WriteMessage {MessageType} payloadSize={PayloadSize}", message.GetType().Name, totalPayload); - } + if (_logger?.IsEnabled(LogLevel.Debug) == true) + _logger.LogDebug("WriteMessage {MessageType} payloadSize={PayloadSize}", message.GetType().Name, totalPayload); } private void WriteInvocation(ref BufferWriterBinaryOutput bw, IBufferWriter output, InvocationMessage m, ref int externalBytes) @@ -493,11 +491,8 @@ public class AcBinaryHubProtocol : IHubProtocol var chunkCount = dataBytes > 0 ? (dataBytes + chunkSize - 1) / chunkSize : 0; var totalSentSize = LengthPrefixSize + chunkStartPayload + chunkCount * 3 + dataBytes + 1; - if (_logger != null) - { - _logger.LogInformation("Serialize end (chunked) dataBytes={DataBytes} chunkCount={ChunkCount} totalSentSize={TotalSentSize}", - dataBytes, chunkCount, totalSentSize); - } + _logger?.LogInformation("Serialize end (chunked) dataBytes={DataBytes} chunkCount={ChunkCount} totalSentSize={TotalSentSize}", + dataBytes, chunkCount, totalSentSize); } /// @@ -569,11 +564,7 @@ public class AcBinaryHubProtocol : IHubProtocol return false; _logger?.LogTrace("TryParseMessage parsing payloadLength={PayloadLength} inputLength={InputLength}", payloadLength, input.Length); - - if (_logger != null) - { - _logger.LogInformation("Deserialize start"); - } + _logger?.LogInformation("Deserialize start"); message = ParseMessage(ref reader, payloadLength, binder); @@ -581,13 +572,9 @@ public class AcBinaryHubProtocol : IHubProtocol { input = input.Slice(LengthPrefixSize + payloadLength); - if (_logger != null) - { - _logger.LogInformation("Deserialize end"); - - if (_logger.IsEnabled(LogLevel.Debug)) - _logger.LogDebug("TryParseMessage parsed {MessageType}", message.GetType().Name); - } + _logger?.LogInformation("Deserialize end"); + if (_logger?.IsEnabled(LogLevel.Debug) == true) _logger.LogDebug("TryParseMessage parsed {MessageType}", message.GetType().Name); + return true; } @@ -884,20 +871,22 @@ public class AcBinaryHubProtocol : IHubProtocol } else { - // Browser (WASM) fallback: all chunks are buffered, state.Buffer.Complete() - // has been called above, so the synchronous deserializer reads through the - // completed buffer without any Monitor.Wait. + // Browser (WASM) fallback: all chunks are buffered into a single contiguous byte[] + // inside SegmentBufferReader. Use ArrayBinaryInput via the offset-aware overload — + // strictly faster than SegmentBufferReaderInput here (JIT eliminates + // TryAdvanceSegment, no volatile reads, no cross-boundary branching). deserializedArg = AcBinaryDeserializer.Deserialize( - state.Buffer, state.StreamedArgType, _options); + state.Buffer.Buffer, + 0, + state.Buffer.WritePos, + state.StreamedArgType, + _options); } - if (_logger != null) - { - _logger.LogInformation("Deserialize end (chunked)"); + _logger?.LogInformation("Deserialize end (chunked)"); - if (_logger.IsEnabled(LogLevel.Debug)) - _logger.LogDebug("TryParseChunkData deserialization complete resultType={ResultType}", deserializedArg?.GetType().Name ?? "null"); - } + if (_logger?.IsEnabled(LogLevel.Debug) == true) + _logger.LogDebug("TryParseChunkData deserialization complete resultType={ResultType}", deserializedArg?.GetType().Name ?? "null"); } catch (Exception ex) { @@ -1198,14 +1187,18 @@ public class AcBinaryHubProtocol : IHubProtocol return SequenceToByteArray(argSlice.Slice(1)); } - // Bytes mode: linearize to byte[] → ArrayBinaryInput (fastest deser, no segment overhead) - if (_protocolMode == BinaryProtocolMode.Bytes) + // Unified non-chunked receive path: always ArrayBinaryInput via offset-aware overload. + // Single-segment: zero-copy on the pipe's slab. Multi-segment: pool-rented copy. + // _protocolMode no longer affects the receive side — it is only a send-side strategy. + var (arr, offset, length, rented) = GetArgBytes(argSlice); + try { - var bytes = SequenceToByteArray(argSlice); - return AcBinaryDeserializer.Deserialize(bytes, targetType, _options); + return AcBinaryDeserializer.Deserialize(arr, offset, length, targetType, _options); + } + finally + { + if (rented) ArrayPool.Shared.Return(arr); } - - return DeserializeFromSequence(argSlice, targetType, _options); } /// @@ -1222,12 +1215,27 @@ public class AcBinaryHubProtocol : IHubProtocol } /// - /// Deserializes from a ReadOnlySequence via AcBinaryDeserializer. - /// Single-segment: zero-copy via ArrayBinaryInput. Multi-segment: SequenceBinaryInput (no copy). + /// Exposes argSlice bytes as (array, offset, length) for offset-aware + /// . + /// + /// Single-segment: zero-copy via — no allocation, no copy. + /// Multi-segment: -rented contiguous copy; caller MUST return + /// the array via when rented is true. + /// + /// Enables ArrayBinaryInput (fastest — JIT-eliminates the TryAdvanceSegment branch) regardless + /// of whether the pipe delivered the payload as a single slab or multiple. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - protected static object? DeserializeFromSequence(ReadOnlySequence data, Type targetType, AcBinarySerializerOptions options) - => AcBinaryDeserializer.Deserialize(data, targetType, options); + protected static (byte[] array, int offset, int length, bool rented) GetArgBytes(ReadOnlySequence argSlice) + { + if (argSlice.IsSingleSegment && MemoryMarshal.TryGetArray(argSlice.First, out var seg)) + return (seg.Array!, seg.Offset, seg.Count, rented: false); + + var length = (int)argSlice.Length; + var rentedBuf = ArrayPool.Shared.Rent(length); + argSlice.CopyTo(rentedBuf); + return (rentedBuf, 0, length, rented: true); + } #endregion diff --git a/AyCode.Services/SignalRs/AyCodeBinaryHubProtocol.cs b/AyCode.Services/SignalRs/AyCodeBinaryHubProtocol.cs index c379d3a..b34b495 100644 --- a/AyCode.Services/SignalRs/AyCodeBinaryHubProtocol.cs +++ b/AyCode.Services/SignalRs/AyCodeBinaryHubProtocol.cs @@ -196,13 +196,17 @@ public class AyCodeBinaryHubProtocol : AcBinaryHubProtocol if (targetType == typeof(object) && hctx?.Type != null) targetType = hctx.Type; - // 4. Deserialize — Bytes mode linearizes, Segment/AsyncSegment uses the sequence directly - if (_protocolMode == BinaryProtocolMode.Bytes) + // 4. Deserialize — unified ArrayBinaryInput path via GetArgBytes. + // Single-segment: zero-copy on the pipe's slab. Multi-segment: ArrayPool-rented copy. + // _protocolMode no longer affects receive — it is only a send-side strategy. + var (arr, offset, length, rented) = GetArgBytes(argSlice); + try { - var bytes = SequenceToByteArray(argSlice); - return AcBinaryDeserializer.Deserialize(bytes, targetType, Options); + return AcBinaryDeserializer.Deserialize(arr, offset, length, targetType, Options); + } + finally + { + if (rented) ArrayPool.Shared.Return(arr); } - - return DeserializeFromSequence(argSlice, targetType, Options); } }