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.
This commit is contained in:
parent
26c8cd85ce
commit
d060508bd8
|
|
@ -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;
|
|||
/// </summary>
|
||||
public static partial class AcBinaryDeserializer
|
||||
{
|
||||
/// <summary>
|
||||
/// Diagnostic logger for deserializer-level debugging (DEBUG builds only).
|
||||
/// Set to non-null to log SequenceBinaryInput vs ArrayBinaryInput verification results.
|
||||
/// </summary>
|
||||
public static Action<string>? DiagnosticLogger { get; set; }
|
||||
|
||||
private static readonly ConcurrentDictionary<Type, TypeConversionInfo> TypeConversionCache = new();
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -260,6 +267,7 @@ public static partial class AcBinaryDeserializer
|
|||
if (data.IsSingleSegment && MemoryMarshal.TryGetArray(data.First, out var seg))
|
||||
return Deserialize<T>(seg.Array!, seg.Offset, seg.Count, options);
|
||||
|
||||
VerifyAgainstLinearized(data, typeof(T), options);
|
||||
return DeserializeSequence<T, SequenceBinaryInput>(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<SequenceBinaryInput>(new SequenceBinaryInput(data), targetType, options);
|
||||
}
|
||||
|
||||
|
|
@ -357,6 +366,36 @@ public static partial class AcBinaryDeserializer
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
[Conditional("DEBUG")]
|
||||
private static void VerifyAgainstLinearized(ReadOnlySequence<byte> 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}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserialize Expression from binary data.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -23,6 +23,22 @@ public abstract class AcWebSignalRHubBase<TSignalRTags, TLogger>(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();
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -35,6 +51,7 @@ public abstract class AcWebSignalRHubBase<TSignalRTags, TLogger>(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<TSignalRTags, TLogger>(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<byte>, options).
|
||||
// Set isRawBytes = true so client deserializes via ArrayBinaryInput. Remove when BWO bug is fixed.
|
||||
var signalParams = new SignalParams
|
||||
{
|
||||
Status = status,
|
||||
|
|
|
|||
|
|
@ -395,8 +395,11 @@ public class AcBinaryHubProtocol : IHubProtocol
|
|||
|
||||
private void WriteArgument(ref BufferWriterBinaryOutput bw, IBufferWriter<byte> 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<byte>(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)
|
||||
|
|
|
|||
|
|
@ -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<IHubProtocol, AyCodeBinaryHubProtocol>();
|
||||
AcBinaryHubProtocol.DiagnosticLogger = msg => Logger.Debug(msg);
|
||||
AcBinaryDeserializer.DiagnosticLogger = msg => Logger.Debug(msg);
|
||||
}
|
||||
|
||||
HubConnection = hubBuilder.Build();
|
||||
|
|
|
|||
Loading…
Reference in New Issue