Optimize serialization lookups; add SignalR binary toggle

- Cache wrapper/metadata in serialization bridge methods to avoid redundant GetType and GetWrapper calls, improving performance.
- Update source generator to combine null/depth checks and cache depthExceeded for collections.
- Add useAcBinaryProtocol option to AcSignalRClientBase, allowing binary protocol to be toggled via constructor and registered via DI.
- Update documentation to reflect new caching rules and performance improvements.
This commit is contained in:
Loretta 2026-04-04 23:22:47 +02:00
parent bbae524e8d
commit a120cd65ff
5 changed files with 40 additions and 10 deletions

View File

@ -1321,6 +1321,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
sb.AppendLine($"{i} var arr_{p.Name} = {a};"); sb.AppendLine($"{i} var arr_{p.Name} = {a};");
sb.AppendLine($"{i} context.WriteVarUInt((uint)arr_{p.Name}.Length);"); sb.AppendLine($"{i} context.WriteVarUInt((uint)arr_{p.Name}.Length);");
sb.AppendLine($"{i} var nextDepth_{p.Name} = depth + 2;"); sb.AppendLine($"{i} var nextDepth_{p.Name} = depth + 2;");
sb.AppendLine($"{i} var depthExceeded_{p.Name} = depth + 1 > context.MaxDepth;");
sb.AppendLine($"{i} for (var i_{p.Name} = 0; i_{p.Name} < arr_{p.Name}.Length; i_{p.Name}++)"); sb.AppendLine($"{i} for (var i_{p.Name} = 0; i_{p.Name} < arr_{p.Name}.Length; i_{p.Name}++)");
sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} {{");
sb.AppendLine($"{i} var elem_{p.Name} = arr_{p.Name}[i_{p.Name}];"); sb.AppendLine($"{i} var elem_{p.Name} = arr_{p.Name}[i_{p.Name}];");
@ -1330,6 +1331,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
sb.AppendLine($"{i} var col_{p.Name} = {a};"); sb.AppendLine($"{i} var col_{p.Name} = {a};");
sb.AppendLine($"{i} context.WriteVarUInt((uint)col_{p.Name}.Count);"); sb.AppendLine($"{i} context.WriteVarUInt((uint)col_{p.Name}.Count);");
sb.AppendLine($"{i} var nextDepth_{p.Name} = depth + 2;"); sb.AppendLine($"{i} var nextDepth_{p.Name} = depth + 2;");
sb.AppendLine($"{i} var depthExceeded_{p.Name} = depth + 1 > context.MaxDepth;");
sb.AppendLine($"{i} foreach (var elem_{p.Name} in col_{p.Name})"); sb.AppendLine($"{i} foreach (var elem_{p.Name} in col_{p.Name})");
sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} {{");
} }
@ -1338,6 +1340,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
sb.AppendLine($"{i} var col_{p.Name} = {a};"); sb.AppendLine($"{i} var col_{p.Name} = {a};");
sb.AppendLine($"{i} context.WriteVarUInt((uint)col_{p.Name}.Count);"); sb.AppendLine($"{i} context.WriteVarUInt((uint)col_{p.Name}.Count);");
sb.AppendLine($"{i} var nextDepth_{p.Name} = depth + 2;"); sb.AppendLine($"{i} var nextDepth_{p.Name} = depth + 2;");
sb.AppendLine($"{i} var depthExceeded_{p.Name} = depth + 1 > context.MaxDepth;");
sb.AppendLine($"{i} for (var i_{p.Name} = 0; i_{p.Name} < col_{p.Name}.Count; i_{p.Name}++)"); sb.AppendLine($"{i} for (var i_{p.Name} = 0; i_{p.Name} < col_{p.Name}.Count; i_{p.Name}++)");
sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} {{");
sb.AppendLine($"{i} var elem_{p.Name} = col_{p.Name}[i_{p.Name}];"); sb.AppendLine($"{i} var elem_{p.Name} = col_{p.Name}[i_{p.Name}];");
@ -1347,6 +1350,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
sb.AppendLine($"{i} var span_{p.Name} = CollectionsMarshal.AsSpan({a});"); sb.AppendLine($"{i} var span_{p.Name} = CollectionsMarshal.AsSpan({a});");
sb.AppendLine($"{i} context.WriteVarUInt((uint)span_{p.Name}.Length);"); sb.AppendLine($"{i} context.WriteVarUInt((uint)span_{p.Name}.Length);");
sb.AppendLine($"{i} var nextDepth_{p.Name} = depth + 2;"); sb.AppendLine($"{i} var nextDepth_{p.Name} = depth + 2;");
sb.AppendLine($"{i} var depthExceeded_{p.Name} = depth + 1 > context.MaxDepth;");
sb.AppendLine($"{i} for (var i_{p.Name} = 0; i_{p.Name} < span_{p.Name}.Length; i_{p.Name}++)"); sb.AppendLine($"{i} for (var i_{p.Name} = 0; i_{p.Name} < span_{p.Name}.Length; i_{p.Name}++)");
sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} {{");
sb.AppendLine($"{i} var elem_{p.Name} = span_{p.Name}[i_{p.Name}];"); sb.AppendLine($"{i} var elem_{p.Name} = span_{p.Name}[i_{p.Name}];");
@ -1354,8 +1358,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
// Per-element write // Per-element write
var e = $"elem_{p.Name}"; var e = $"elem_{p.Name}";
sb.AppendLine($"{i} if ({e} == null) {{ context.WriteByte(BinaryTypeCode.Null); continue; }}"); sb.AppendLine($"{i} if ({e} == null || depthExceeded_{p.Name}) {{ context.WriteByte(BinaryTypeCode.Null); continue; }}");
sb.AppendLine($"{i} if (depth + 1 > context.MaxDepth) {{ context.WriteByte(BinaryTypeCode.Null); continue; }}");
var elemRefSuffix = p.ElementIsIId ? "IId" : "All"; var elemRefSuffix = p.ElementIsIId ? "IId" : "All";

View File

@ -358,10 +358,12 @@ public static partial class AcBinarySerializer
{ {
var useMetadata = UseMetadata; var useMetadata = UseMetadata;
bool isFirstMeta = false; bool isFirstMeta = false;
BinarySerializeTypeMetadata? metadata = null;
if (useMetadata) if (useMetadata)
{ {
var wrapper = GetWrapper(value.GetType(), wrapperSlot); var wrapper = GetWrapper(value.GetType(), wrapperSlot);
isFirstMeta = RegisterMetadataType(wrapper); isFirstMeta = RegisterMetadataType(wrapper);
metadata = wrapper.Metadata;
} }
if (HasRefHandling && TryConsumeWritePlanEntry(out var pe)) if (HasRefHandling && TryConsumeWritePlanEntry(out var pe))
@ -377,7 +379,7 @@ public static partial class AcBinarySerializer
{ {
WriteByte(BinaryTypeCode.ObjectWithMetadataRefFirst); WriteByte(BinaryTypeCode.ObjectWithMetadataRefFirst);
WriteVarUInt((uint)pe.CacheMapIndex); WriteVarUInt((uint)pe.CacheMapIndex);
WriteInlineMetadata(GetWrapper(value.GetType(), wrapperSlot).Metadata, isFirstMeta); WriteInlineMetadata(metadata!, isFirstMeta);
} }
else else
{ {
@ -391,7 +393,7 @@ public static partial class AcBinarySerializer
if (useMetadata) if (useMetadata)
{ {
WriteByte(BinaryTypeCode.ObjectWithMetadata); WriteByte(BinaryTypeCode.ObjectWithMetadata);
WriteInlineMetadata(GetWrapper(value.GetType(), wrapperSlot).Metadata, isFirstMeta); WriteInlineMetadata(metadata!, isFirstMeta);
} }
else else
{ {
@ -409,10 +411,12 @@ public static partial class AcBinarySerializer
{ {
var useMetadata = UseMetadata; var useMetadata = UseMetadata;
bool isFirstMeta = false; bool isFirstMeta = false;
BinarySerializeTypeMetadata? metadata = null;
if (useMetadata) if (useMetadata)
{ {
var wrapper = GetWrapper(value.GetType(), wrapperSlot); var wrapper = GetWrapper(value.GetType(), wrapperSlot);
isFirstMeta = RegisterMetadataType(wrapper); isFirstMeta = RegisterMetadataType(wrapper);
metadata = wrapper.Metadata;
} }
if (HasAllRefHandling && TryConsumeWritePlanEntry(out var pe)) if (HasAllRefHandling && TryConsumeWritePlanEntry(out var pe))
@ -428,7 +432,7 @@ public static partial class AcBinarySerializer
{ {
WriteByte(BinaryTypeCode.ObjectWithMetadataRefFirst); WriteByte(BinaryTypeCode.ObjectWithMetadataRefFirst);
WriteVarUInt((uint)pe.CacheMapIndex); WriteVarUInt((uint)pe.CacheMapIndex);
WriteInlineMetadata(GetWrapper(value.GetType(), wrapperSlot).Metadata, isFirstMeta); WriteInlineMetadata(metadata!, isFirstMeta);
} }
else else
{ {
@ -442,7 +446,7 @@ public static partial class AcBinarySerializer
if (useMetadata) if (useMetadata)
{ {
WriteByte(BinaryTypeCode.ObjectWithMetadata); WriteByte(BinaryTypeCode.ObjectWithMetadata);
WriteInlineMetadata(GetWrapper(value.GetType(), wrapperSlot).Metadata, isFirstMeta); WriteInlineMetadata(metadata!, isFirstMeta);
} }
else else
{ {

View File

@ -132,3 +132,11 @@ Two-phase:
- `BinarySerializationContextPool<BufferWriterBinaryOutput>` — IBufferWriter path - `BinarySerializationContextPool<BufferWriterBinaryOutput>` — IBufferWriter path
- `options.UseAsync``ReturnAsync` (ThreadPool enqueue) to avoid lock contention - `options.UseAsync``ReturnAsync` (ThreadPool enqueue) to avoid lock contention
- Pooled contexts retain wrapper caches, buffer instances across serializations - Pooled contexts retain wrapper caches, buffer instances across serializations
### 6. Avoid Redundant Wrapper/GetType Lookups
**Rule:** When a bridge method calls `GetWrapper(value.GetType(), slot)` for metadata, cache the result in a local. Never call `GetWrapper` + `value.GetType()` twice in the same method.
- `WriteObjectFullMarkerIId` / `WriteObjectFullMarkerAll`: `wrapper.Metadata` cached at entry, reused in ref-handling and non-ref branches
- `GetWrapper(type, slot)` is O(1) array index after first call, but `value.GetType()` is a virtual call — avoid repeating it

View File

@ -115,6 +115,7 @@ void WriteProperties<TOutput>(object value, BinarySerializationContext<TOutput>
AcBinarySerializer.WriteValueGenerated(obj.Other, typeof(OtherType), ctx, depth + 1); AcBinarySerializer.WriteValueGenerated(obj.Other, typeof(OtherType), ctx, depth + 1);
} }
``` ```
```
### ScanObject (generated) ### ScanObject (generated)
@ -133,6 +134,10 @@ void ScanObject<TOutput>(object value, BinarySerializationContext<TOutput> ctx,
} }
``` ```
## Object Marker Bridge — Metadata Caching
`WriteObjectFullMarkerIId` / `WriteObjectFullMarkerAll` in `PropertyWriters.cs`: when `UseMetadata=true`, `GetWrapper` result and `wrapper.Metadata` are cached in a local variable at the method entry. This avoids redundant `GetWrapper` + `value.GetType()` calls in the ref-handling and non-ref branches.
## Performance Characteristics ## Performance Characteristics
| Aspect | SGen | Runtime | | Aspect | SGen | Runtime |

View File

@ -7,6 +7,8 @@ using AyCode.Core.Serializers;
using AyCode.Interfaces.Entities; using AyCode.Interfaces.Entities;
using Microsoft.AspNetCore.Http.Connections; using Microsoft.AspNetCore.Http.Connections;
using Microsoft.AspNetCore.SignalR.Client; using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.AspNetCore.SignalR.Protocol;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace AyCode.Services.SignalRs namespace AyCode.Services.SignalRs
@ -14,6 +16,7 @@ namespace AyCode.Services.SignalRs
public abstract class AcSignalRClientBase : IAcSignalRHubClient public abstract class AcSignalRClientBase : IAcSignalRHubClient
{ {
private readonly ConcurrentDictionary<int, SignalRRequestModel> _responseByRequestId = new(); private readonly ConcurrentDictionary<int, SignalRRequestModel> _responseByRequestId = new();
private readonly bool _useAcBinaryProtocol;
protected readonly HubConnection? HubConnection; protected readonly HubConnection? HubConnection;
protected readonly AcLoggerBase Logger; protected readonly AcLoggerBase Logger;
@ -27,12 +30,13 @@ namespace AyCode.Services.SignalRs
public int TransportSendTimeout = 60000; public int TransportSendTimeout = 60000;
private const string TagsName = "SignalRTags"; private const string TagsName = "SignalRTags";
protected AcSignalRClientBase(string fullHubName, AcLoggerBase logger) protected AcSignalRClientBase(string fullHubName, AcLoggerBase logger, bool useAcBinaryProtocol = true)
{ {
_useAcBinaryProtocol = useAcBinaryProtocol;
Logger = logger; Logger = logger;
Logger.Detail(fullHubName); Logger.Detail(fullHubName);
HubConnection = new HubConnectionBuilder() var hubBuilder = new HubConnectionBuilder()
.WithUrl(fullHubName, HttpTransportType.WebSockets, .WithUrl(fullHubName, HttpTransportType.WebSockets,
options => options =>
{ {
@ -56,8 +60,14 @@ namespace AyCode.Services.SignalRs
.WithAutomaticReconnect() .WithAutomaticReconnect()
.WithStatefulReconnect() .WithStatefulReconnect()
.WithKeepAliveInterval(TimeSpan.FromSeconds(60)) .WithKeepAliveInterval(TimeSpan.FromSeconds(60))
.WithServerTimeout(TimeSpan.FromSeconds(180)) .WithServerTimeout(TimeSpan.FromSeconds(180));
.Build();
if (useAcBinaryProtocol)
{
hubBuilder.Services.AddSingleton<IHubProtocol, AcBinaryHubProtocol>();
}
HubConnection = hubBuilder.Build();
HubConnection.Closed += HubConnection_Closed; HubConnection.Closed += HubConnection_Closed;
_ = HubConnection.On<int, byte[], int?>(nameof(IAcSignalRHubClient.OnReceiveMessage), OnReceiveMessage); _ = HubConnection.On<int, byte[], int?>(nameof(IAcSignalRHubClient.OnReceiveMessage), OnReceiveMessage);