From a120cd65ff5862325334e315ba441b39dcb5d7ce Mon Sep 17 00:00:00 2001 From: Loretta Date: Sat, 4 Apr 2026 23:22:47 +0200 Subject: [PATCH] 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. --- .../AcBinarySourceGenerator.cs | 7 +++++-- ...narySerializationContext.PropertyWriters.cs | 12 ++++++++---- AyCode.Core/docs/BINARY_IMPLEMENTATION.md | 8 ++++++++ AyCode.Core/docs/BINARY_SGEN.md | 5 +++++ .../SignalRs/AcSignalRClientBase.cs | 18 ++++++++++++++---- 5 files changed, 40 insertions(+), 10 deletions(-) diff --git a/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs b/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs index fc85f56..20f870c 100644 --- a/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs +++ b/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs @@ -1321,6 +1321,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator sb.AppendLine($"{i} var arr_{p.Name} = {a};"); sb.AppendLine($"{i} context.WriteVarUInt((uint)arr_{p.Name}.Length);"); 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} {{"); 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} context.WriteVarUInt((uint)col_{p.Name}.Count);"); 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} {{"); } @@ -1338,6 +1340,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator sb.AppendLine($"{i} var col_{p.Name} = {a};"); sb.AppendLine($"{i} context.WriteVarUInt((uint)col_{p.Name}.Count);"); 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} {{"); 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} context.WriteVarUInt((uint)span_{p.Name}.Length);"); 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} {{"); sb.AppendLine($"{i} var elem_{p.Name} = span_{p.Name}[i_{p.Name}];"); @@ -1354,8 +1358,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator // Per-element write var e = $"elem_{p.Name}"; - sb.AppendLine($"{i} if ({e} == null) {{ context.WriteByte(BinaryTypeCode.Null); continue; }}"); - sb.AppendLine($"{i} if (depth + 1 > context.MaxDepth) {{ context.WriteByte(BinaryTypeCode.Null); continue; }}"); + sb.AppendLine($"{i} if ({e} == null || depthExceeded_{p.Name}) {{ context.WriteByte(BinaryTypeCode.Null); continue; }}"); var elemRefSuffix = p.ElementIsIId ? "IId" : "All"; diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.PropertyWriters.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.PropertyWriters.cs index 4292360..1319745 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.PropertyWriters.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.PropertyWriters.cs @@ -358,10 +358,12 @@ public static partial class AcBinarySerializer { var useMetadata = UseMetadata; bool isFirstMeta = false; + BinarySerializeTypeMetadata? metadata = null; if (useMetadata) { var wrapper = GetWrapper(value.GetType(), wrapperSlot); isFirstMeta = RegisterMetadataType(wrapper); + metadata = wrapper.Metadata; } if (HasRefHandling && TryConsumeWritePlanEntry(out var pe)) @@ -377,7 +379,7 @@ public static partial class AcBinarySerializer { WriteByte(BinaryTypeCode.ObjectWithMetadataRefFirst); WriteVarUInt((uint)pe.CacheMapIndex); - WriteInlineMetadata(GetWrapper(value.GetType(), wrapperSlot).Metadata, isFirstMeta); + WriteInlineMetadata(metadata!, isFirstMeta); } else { @@ -391,7 +393,7 @@ public static partial class AcBinarySerializer if (useMetadata) { WriteByte(BinaryTypeCode.ObjectWithMetadata); - WriteInlineMetadata(GetWrapper(value.GetType(), wrapperSlot).Metadata, isFirstMeta); + WriteInlineMetadata(metadata!, isFirstMeta); } else { @@ -409,10 +411,12 @@ public static partial class AcBinarySerializer { var useMetadata = UseMetadata; bool isFirstMeta = false; + BinarySerializeTypeMetadata? metadata = null; if (useMetadata) { var wrapper = GetWrapper(value.GetType(), wrapperSlot); isFirstMeta = RegisterMetadataType(wrapper); + metadata = wrapper.Metadata; } if (HasAllRefHandling && TryConsumeWritePlanEntry(out var pe)) @@ -428,7 +432,7 @@ public static partial class AcBinarySerializer { WriteByte(BinaryTypeCode.ObjectWithMetadataRefFirst); WriteVarUInt((uint)pe.CacheMapIndex); - WriteInlineMetadata(GetWrapper(value.GetType(), wrapperSlot).Metadata, isFirstMeta); + WriteInlineMetadata(metadata!, isFirstMeta); } else { @@ -442,7 +446,7 @@ public static partial class AcBinarySerializer if (useMetadata) { WriteByte(BinaryTypeCode.ObjectWithMetadata); - WriteInlineMetadata(GetWrapper(value.GetType(), wrapperSlot).Metadata, isFirstMeta); + WriteInlineMetadata(metadata!, isFirstMeta); } else { diff --git a/AyCode.Core/docs/BINARY_IMPLEMENTATION.md b/AyCode.Core/docs/BINARY_IMPLEMENTATION.md index 0b0d5a9..a6242d8 100644 --- a/AyCode.Core/docs/BINARY_IMPLEMENTATION.md +++ b/AyCode.Core/docs/BINARY_IMPLEMENTATION.md @@ -132,3 +132,11 @@ Two-phase: - `BinarySerializationContextPool` — IBufferWriter path - `options.UseAsync` → `ReturnAsync` (ThreadPool enqueue) to avoid lock contention - 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 + diff --git a/AyCode.Core/docs/BINARY_SGEN.md b/AyCode.Core/docs/BINARY_SGEN.md index ebff6d5..ca87114 100644 --- a/AyCode.Core/docs/BINARY_SGEN.md +++ b/AyCode.Core/docs/BINARY_SGEN.md @@ -115,6 +115,7 @@ void WriteProperties(object value, BinarySerializationContext AcBinarySerializer.WriteValueGenerated(obj.Other, typeof(OtherType), ctx, depth + 1); } ``` +``` ### ScanObject (generated) @@ -133,6 +134,10 @@ void ScanObject(object value, BinarySerializationContext 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 | Aspect | SGen | Runtime | diff --git a/AyCode.Services/SignalRs/AcSignalRClientBase.cs b/AyCode.Services/SignalRs/AcSignalRClientBase.cs index a1fcd11..159dd9c 100644 --- a/AyCode.Services/SignalRs/AcSignalRClientBase.cs +++ b/AyCode.Services/SignalRs/AcSignalRClientBase.cs @@ -7,6 +7,8 @@ using AyCode.Core.Serializers; using AyCode.Interfaces.Entities; using Microsoft.AspNetCore.Http.Connections; using Microsoft.AspNetCore.SignalR.Client; +using Microsoft.AspNetCore.SignalR.Protocol; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace AyCode.Services.SignalRs @@ -14,6 +16,7 @@ namespace AyCode.Services.SignalRs public abstract class AcSignalRClientBase : IAcSignalRHubClient { private readonly ConcurrentDictionary _responseByRequestId = new(); + private readonly bool _useAcBinaryProtocol; protected readonly HubConnection? HubConnection; protected readonly AcLoggerBase Logger; @@ -27,12 +30,13 @@ namespace AyCode.Services.SignalRs public int TransportSendTimeout = 60000; 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.Detail(fullHubName); - HubConnection = new HubConnectionBuilder() + var hubBuilder = new HubConnectionBuilder() .WithUrl(fullHubName, HttpTransportType.WebSockets, options => { @@ -56,8 +60,14 @@ namespace AyCode.Services.SignalRs .WithAutomaticReconnect() .WithStatefulReconnect() .WithKeepAliveInterval(TimeSpan.FromSeconds(60)) - .WithServerTimeout(TimeSpan.FromSeconds(180)) - .Build(); + .WithServerTimeout(TimeSpan.FromSeconds(180)); + + if (useAcBinaryProtocol) + { + hubBuilder.Services.AddSingleton(); + } + + HubConnection = hubBuilder.Build(); HubConnection.Closed += HubConnection_Closed; _ = HubConnection.On(nameof(IAcSignalRHubClient.OnReceiveMessage), OnReceiveMessage);