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:
parent
bbae524e8d
commit
a120cd65ff
|
|
@ -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";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 |
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue