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:
parent
b1cdf80fad
commit
4d75599988
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue