From d060508bd8ed230f8280bce13c30b08c55ec402b Mon Sep 17 00:00:00 2001 From: Loretta Date: Tue, 7 Apr 2026 20:53:20 +0200 Subject: [PATCH] Add diagnostics for binary SignalR serialization bugs Enhances debugging of custom binary serialization/deserialization in SignalR by introducing DiagnosticLogger hooks in both AcBinaryDeserializer and AcBinaryHubProtocol. Adds DEBUG-only verification methods to compare array-based and multi-segment deserialization, as well as IBufferWriter and byte[] serialization outputs, logging mismatches for easier bug isolation. Diagnostic loggers are automatically integrated with the hub and client loggers. Also includes extra debug output and a commented workaround for a known serialization issue. Diagnostics are opt-in and only active in DEBUG builds. --- .../Binaries/AcBinaryDeserializer.cs | 39 +++++++++++ .../SignalRs/AcWebSignalRHubBase.cs | 21 ++++++ .../SignalRs/AcBinaryHubProtocol.cs | 70 +++++++++++++++++++ .../SignalRs/AcSignalRClientBase.cs | 2 + 4 files changed, 132 insertions(+) diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs index e9f491c..cd7e4bc 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs @@ -3,6 +3,7 @@ using System.Buffers; using System.Collections; using System.Collections.Concurrent; using System.Collections.Frozen; +using System.Diagnostics; using System.Linq.Expressions; using System.Reflection; using System.Runtime.CompilerServices; @@ -27,6 +28,12 @@ namespace AyCode.Core.Serializers.Binaries; /// public static partial class AcBinaryDeserializer { + /// + /// Diagnostic logger for deserializer-level debugging (DEBUG builds only). + /// Set to non-null to log SequenceBinaryInput vs ArrayBinaryInput verification results. + /// + public static Action? DiagnosticLogger { get; set; } + private static readonly ConcurrentDictionary TypeConversionCache = new(); /// @@ -260,6 +267,7 @@ public static partial class AcBinaryDeserializer if (data.IsSingleSegment && MemoryMarshal.TryGetArray(data.First, out var seg)) return Deserialize(seg.Array!, seg.Offset, seg.Count, options); + VerifyAgainstLinearized(data, typeof(T), options); return DeserializeSequence(new SequenceBinaryInput(data), typeof(T), options); } @@ -279,6 +287,7 @@ public static partial class AcBinaryDeserializer if (data.IsSingleSegment && MemoryMarshal.TryGetArray(data.First, out var seg2)) return Deserialize(seg2.Array!, seg2.Offset, seg2.Count, targetType, options); + VerifyAgainstLinearized(data, targetType, options); return DeserializeSequence(new SequenceBinaryInput(data), targetType, options); } @@ -357,6 +366,36 @@ public static partial class AcBinaryDeserializer } } + /// + /// DEBUG-only verification: linearizes multi-segment data to byte[] and deserializes + /// with ArrayBinaryInput to determine if drift is caused by SequenceBinaryInput or by + /// the serialized data itself (generated reader / serializer bug). + /// + /// Result: VERIFY_OK → SequenceBinaryInput is the culprit. + /// VERIFY_FAIL → bug is in serializer or generated reader (data itself is bad). + /// + [Conditional("DEBUG")] + private static void VerifyAgainstLinearized(ReadOnlySequence data, Type targetType, AcBinarySerializerOptions options) + { + if (DiagnosticLogger == null) return; + + var bytes = data.ToArray(); + var segmentCount = 0; + foreach (var _ in data) segmentCount++; + + try + { + var result = Deserialize(bytes, targetType, options); + DiagnosticLogger($"[VERIFY_OK] ArrayBinaryInput succeeded for {targetType.Name}, " + + $"{bytes.Length} bytes, {segmentCount} segments → SequenceBinaryInput is suspect"); + } + catch (Exception ex) + { + DiagnosticLogger($"[VERIFY_FAIL] ArrayBinaryInput ALSO FAILED for {targetType.Name}, " + + $"{bytes.Length} bytes, {segmentCount} segments: {ex.Message}"); + } + } + /// /// Deserialize Expression from binary data. /// diff --git a/AyCode.Services.Server/SignalRs/AcWebSignalRHubBase.cs b/AyCode.Services.Server/SignalRs/AcWebSignalRHubBase.cs index b4f362f..8e13d7f 100644 --- a/AyCode.Services.Server/SignalRs/AcWebSignalRHubBase.cs +++ b/AyCode.Services.Server/SignalRs/AcWebSignalRHubBase.cs @@ -23,6 +23,22 @@ public abstract class AcWebSignalRHubBase(IConfiguration protected TLogger Logger = logger; protected IConfiguration Configuration = configuration; + // Static init: route protocol & deserializer diagnostics through AcLoggerBase + private static readonly object _diagnosticInitLock = new(); + private static bool _diagnosticInitialized; + + protected void InitDiagnosticLoggerIfNeeded() + { + if (_diagnosticInitialized) return; + lock (_diagnosticInitLock) + { + if (_diagnosticInitialized) return; + AcBinaryHubProtocol.DiagnosticLogger ??= msg => Logger.Debug(msg); + AcBinaryDeserializer.DiagnosticLogger ??= msg => Logger.Debug(msg); + _diagnosticInitialized = true; + } + } + protected AcSerializerOptions SerializerOptions = new AcBinarySerializerOptions(); /// @@ -35,6 +51,7 @@ public abstract class AcWebSignalRHubBase(IConfiguration public override async Task OnConnectedAsync() { + InitDiagnosticLoggerIfNeeded(); Logger.Debug($"Server OnConnectedAsync; ConnectionId: {GetConnectionId()}; UserIdentifier: {GetUserIdentifier()}"); LogContextUserNameAndId(); await base.OnConnectedAsync(); @@ -301,6 +318,10 @@ public abstract class AcWebSignalRHubBase(IConfiguration : AyCode.Core.Compression.GzipHelper.Compress(responseData.ToJson()); } + //responseData = AcBinarySerializer.Serialize(responseData); + // TODO: BWO serialize bug workaround — pre-serialize responseData to byte[] here so protocol uses + // byte[] fast-path, bypassing AcBinarySerializer.Serialize(value, IBufferWriter, options). + // Set isRawBytes = true so client deserializes via ArrayBinaryInput. Remove when BWO bug is fixed. var signalParams = new SignalParams { Status = status, diff --git a/AyCode.Services/SignalRs/AcBinaryHubProtocol.cs b/AyCode.Services/SignalRs/AcBinaryHubProtocol.cs index 18e0058..8c5d14e 100644 --- a/AyCode.Services/SignalRs/AcBinaryHubProtocol.cs +++ b/AyCode.Services/SignalRs/AcBinaryHubProtocol.cs @@ -395,8 +395,11 @@ public class AcBinaryHubProtocol : IHubProtocol private void WriteArgument(ref BufferWriterBinaryOutput bw, IBufferWriter output, object? value, ref int externalBytes) { + Debug.WriteLine($"WriteArgument invoked"); if (value is byte[] byteArray) { + Debug.WriteLine($"WriteArgument value is byte[] byteArray"); + // byte[] fast-path: size known upfront, write entirely through BWO var argPayload = 1 + VarUIntSize((uint)byteArray.Length) + byteArray.Length; bw.WriteRaw(argPayload); @@ -414,6 +417,7 @@ public class AcBinaryHubProtocol : IHubProtocol output.Advance(LengthPrefixSize); var argBytes = AcBinarySerializer.Serialize(value, output, _options); + VerifyBwoAgainstArray(value, _options, argBytes); Unsafe.WriteUnaligned(ref argLenSpan[0], argBytes); externalBytes += LengthPrefixSize + argBytes; @@ -650,6 +654,72 @@ public class AcBinaryHubProtocol : IHubProtocol #endregion + #region Diagnostics + + [Conditional("DEBUG")] + private static void VerifyBwoAgainstArray(object? value, AcBinarySerializerOptions options, int bwoBytes) + { + if (DiagnosticLogger == null) return; + + // Reference serialization via byte[] path + byte[] referenceBytes; + try + { + referenceBytes = AcBinarySerializer.Serialize(value, options); + } + catch (Exception ex) + { + DiagnosticLogger($"[VERIFY_WRITE] Array serialize FAILED for {value?.GetType().Name}: {ex.Message}"); + return; + } + + // BWO serialization to a temp ArrayBufferWriter to get comparable bytes + var tempWriter = new ArrayBufferWriter(Math.Max(Math.Max(referenceBytes.Length, bwoBytes) + 256, 4096)); + var tempBytes = AcBinarySerializer.Serialize(value, tempWriter, options); + + if (bwoBytes != referenceBytes.Length) + { + DiagnosticLogger($"[VERIFY_WRITE_MISMATCH] SIZE: Array={referenceBytes.Length}, BWO_pipe={bwoBytes}, BWO_temp={tempBytes} for {value?.GetType().Name}"); + } + + // Compare temp BWO output against Array reference byte-by-byte + var bwoSpan = tempWriter.WrittenSpan; + var refSpan = referenceBytes.AsSpan(); + var minLen = Math.Min(bwoSpan.Length, refSpan.Length); + var mismatchCount = 0; + for (int i = 0; i < minLen; i++) + { + if (bwoSpan[i] != refSpan[i]) + { + if (mismatchCount < 5) // log first 5 mismatches + { + var start = Math.Max(0, i - 8); + var end = Math.Min(minLen, i + 16); + var refHex = Convert.ToHexString(refSpan.Slice(start, end - start)); + var bwoHex = Convert.ToHexString(bwoSpan.Slice(start, end - start)); + DiagnosticLogger($"[VERIFY_WRITE_MISMATCH] CONTENT #{mismatchCount} at byte {i}/{minLen}: ref={refHex} bwo={bwoHex} type={value?.GetType().Name}"); + } + mismatchCount++; + } + } + + if (mismatchCount > 0) + { + DiagnosticLogger($"[VERIFY_WRITE_MISMATCH] Total {mismatchCount} differing bytes out of {minLen} compared (Array={referenceBytes.Length}, BWO_temp={tempBytes})"); + return; + } + + if (bwoSpan.Length != refSpan.Length) + { + DiagnosticLogger($"[VERIFY_WRITE_MISMATCH] Bytes match up to {minLen} but lengths differ: Array={referenceBytes.Length}, BWO_temp={tempBytes}"); + return; + } + + DiagnosticLogger($"[VERIFY_WRITE_OK] {referenceBytes.Length} bytes match for {value?.GetType().Name}"); + } + + #endregion + #region Helpers private static InvocationMessage ApplyInvocationId(InvocationMessage msg, string? invocationId) diff --git a/AyCode.Services/SignalRs/AcSignalRClientBase.cs b/AyCode.Services/SignalRs/AcSignalRClientBase.cs index abe2ac8..1c2ce6a 100644 --- a/AyCode.Services/SignalRs/AcSignalRClientBase.cs +++ b/AyCode.Services/SignalRs/AcSignalRClientBase.cs @@ -4,6 +4,7 @@ using AyCode.Core.Extensions; using AyCode.Core.Helpers; using AyCode.Core.Loggers; using AyCode.Core.Serializers; +using AyCode.Core.Serializers.Binaries; using AyCode.Interfaces.Entities; using Microsoft.AspNetCore.Http.Connections; using Microsoft.AspNetCore.SignalR.Client; @@ -71,6 +72,7 @@ namespace AyCode.Services.SignalRs { hubBuilder.Services.AddSingleton(); AcBinaryHubProtocol.DiagnosticLogger = msg => Logger.Debug(msg); + AcBinaryDeserializer.DiagnosticLogger = msg => Logger.Debug(msg); } HubConnection = hubBuilder.Build();