Refactor: use byte[]-output AcBinarySerializer overloads

Refactored binary serialization and cloning to use pool-backed byte[]-output overloads of AcBinarySerializer, reducing allocations and improving performance. Updated CloneTo and CopyTo to use explicit runtime types. SignalParams now emits a Null marker for nulls. Updated SignalRSerializationHelper to use lighter overloads. Added DebugLogArgument for deserialization debug logging. Improved comments and code clarity.
This commit is contained in:
Loretta 2026-05-27 12:30:07 +02:00
parent b1cdf80fad
commit 4d75599988
4 changed files with 30 additions and 30 deletions

View File

@ -681,31 +681,31 @@ public static class SerializeObjectExtensions
#region Clone and Copy (Binary-based, zero intermediate allocation) #region Clone and Copy (Binary-based, zero intermediate allocation)
/// <summary> /// <summary>
/// Clone object via binary serialization (zero intermediate byte[] allocation). /// Clone object via binary serialization. Uses the byte[]-output overload (pool-context internally,
/// Uses ArrayBufferWriter to serialize directly into a buffer, then deserializes from the span. /// 1 final allocation) — lighter than the IBufferWriter path (2 allocations: ArrayBufferWriter + buffer).
/// </summary> /// </summary>
public static TDestination? CloneTo<TDestination>(this object? src) where TDestination : class public static TDestination? CloneTo<TDestination>(this object? src) where TDestination : class
{ {
if (src == null) return null; if (src == null) return null;
var buffer = new ArrayBufferWriter<byte>(256); // Pass explicit runtime type — src is statically object?, so the generic Serialize<T> overload
AcBinarySerializer.Serialize(src, buffer, AcBinarySerializerOptions.Default); // would infer T = object and emit an object-typed wire payload, losing all concrete DTO properties.
MemoryMarshal.TryGetArray<byte>(buffer.WrittenMemory, out var seg); var bytes = AcBinarySerializer.Serialize(src, src.GetType(), AcBinarySerializerOptions.Default);
return AcBinaryDeserializer.Deserialize<TDestination>(seg.Array!, seg.Offset, seg.Count); return AcBinaryDeserializer.Deserialize<TDestination>(bytes);
} }
/// <summary> /// <summary>
/// Copy object properties to target via binary serialization (zero intermediate byte[] allocation). /// Copy object properties to target via binary serialization. Uses the byte[]-output overload
/// Uses ArrayBufferWriter to serialize directly into a buffer, then populates target from the backing array. /// (pool-context internally, 1 final allocation) — lighter than the IBufferWriter path.
/// </summary> /// </summary>
public static void CopyTo(this object? src, object target) public static void CopyTo(this object? src, object target)
{ {
if (src == null) return; if (src == null) return;
var buffer = new ArrayBufferWriter<byte>(256); // Pass explicit runtime type — src is statically object?, so the generic Serialize<T> overload
AcBinarySerializer.Serialize(src, buffer, AcBinarySerializerOptions.Default); // would infer T = object and emit an object-typed wire payload, losing all concrete DTO properties.
MemoryMarshal.TryGetArray<byte>(buffer.WrittenMemory, out var seg); var bytes = AcBinarySerializer.Serialize(src, src.GetType(), AcBinarySerializerOptions.Default);
AcBinaryDeserializer.Populate(seg.Array!, seg.Offset, seg.Count, target); AcBinaryDeserializer.Populate(bytes, 0, bytes.Length, target);
} }
#endregion #endregion

View File

@ -1379,6 +1379,13 @@ public class AcBinaryHubProtocol : IHubProtocol
Console.WriteLine($"[DEBUG] WriteArgument runtimeType={runtimeType.FullName} argBytes={argBytes} valueIsNull={value == null} kind={kind}"); Console.WriteLine($"[DEBUG] WriteArgument runtimeType={runtimeType.FullName} argBytes={argBytes} valueIsNull={value == null} kind={kind}");
} }
[Conditional("DEBUG")]
protected void DebugLogArgument(Type targetType, int argLength, long remaining)
{
_logger?.LogDebug("ReadSingleArgument targetType={TargetType} argLength={ArgLength} remaining={Remaining}", targetType.FullName, argLength, remaining);
Console.WriteLine($"[DEBUG] ReadSingleArgument targetType={targetType.FullName} argLength={argLength} remaining={remaining}");
}
private object?[] ReadArguments(ref SequenceReader<byte> r, IReadOnlyList<Type> paramTypes, object? headerContext) private object?[] ReadArguments(ref SequenceReader<byte> r, IReadOnlyList<Type> paramTypes, object? headerContext)
{ {
var count = (int)ReadVarUInt(ref r); var count = (int)ReadVarUInt(ref r);

View File

@ -64,9 +64,10 @@ public class SignalParams : ISignalParams
{ {
// Pass explicit runtime type — parameters[i] is statically object, so the generic // Pass explicit runtime type — parameters[i] is statically object, so the generic
// ToBinary<T>() overload would infer T = object and emit an object-typed wire payload // ToBinary<T>() overload would infer T = object and emit an object-typed wire payload
// (instead of the concrete int/string/DTO type). The server's GetParameterValues then // (instead of the concrete int/string/DTO type). Explicit null-handling avoids the
// deserializes to default(targetType) → 0 for int, null for reference types, etc. // null-conditional + fallback indirection and produces the Null marker directly.
paramBytes[i] = parameters[i].ToBinary(parameters[i]?.GetType() ?? typeof(object)); var p = parameters[i];
paramBytes[i] = p == null ? [BinaryTypeCode.Null] : p.ToBinary(p.GetType());
} }
_parameterValues = paramBytes; _parameterValues = paramBytes;
@ -93,9 +94,7 @@ public class SignalParams : ISignalParams
return null; return null;
// Deserialize byte[] → byte[][] (cached) // Deserialize byte[] → byte[][] (cached)
_parameterValues ??= Parameters is { Length: > 0 } _parameterValues ??= Parameters is { Length: > 0 } ? Parameters.BinaryTo<byte[][]>() : null;
? Parameters.BinaryTo<byte[][]>()
: null;
if (_parameterValues is null or { Length: 0 }) if (_parameterValues is null or { Length: 0 })
return null; return null;

View File

@ -62,18 +62,16 @@ public static class SignalRSerializationHelper
#region Binary Serialization #region Binary Serialization
/// <summary> /// <summary>
/// Serialize object to binary using pooled ArrayBufferWriter. /// Serialize object to binary via the pool-backed byte[]-output overload (1 final allocation).
/// Lighter than the IBufferWriter path (2 allocations: ArrayBufferWriter + buffer + ToArray copy).
/// </summary> /// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static byte[] SerializeToBinary<T>(T value, AcBinarySerializerOptions? options = null) public static byte[] SerializeToBinary<T>(T value, AcBinarySerializerOptions? options = null)
{ => value.ToBinary(options ?? AcBinarySerializerOptions.Default);
var writer = new ArrayBufferWriter<byte>(256);
value.ToBinary(writer, options ?? AcBinarySerializerOptions.Default);
return writer.WrittenSpan.ToArray();
}
/// <summary> /// <summary>
/// Serialize object to binary and write to existing ArrayBufferWriter. /// Serialize object to binary and write to existing ArrayBufferWriter. Caller owns the writer
/// — useful for buffer-reuse scenarios where the same writer accepts multiple writes.
/// </summary> /// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void SerializeToBinary<T>(T value, ArrayBufferWriter<byte> writer, AcBinarySerializerOptions? options = null) public static void SerializeToBinary<T>(T value, ArrayBufferWriter<byte> writer, AcBinarySerializerOptions? options = null)
@ -89,11 +87,7 @@ public static class SignalRSerializationHelper
/// </summary> /// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static byte[] SerializeToBinary(object? value, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type type, AcBinarySerializerOptions? options = null) public static byte[] SerializeToBinary(object? value, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type type, AcBinarySerializerOptions? options = null)
{ => value.ToBinary(type, options ?? AcBinarySerializerOptions.Default);
var writer = new ArrayBufferWriter<byte>(256);
value.ToBinary(type, writer, options ?? AcBinarySerializerOptions.Default);
return writer.WrittenSpan.ToArray();
}
/// <summary> /// <summary>
/// Deserialize binary data to object. /// Deserialize binary data to object.