Refactor SignalR dynamic method lookup with static registry
Introduce AcDynamicMethodRegistry<TAttribute> for efficient, static caching and lookup of SignalR methods by messageTag and type. Replace per-instance method lists with a high-performance registry, update all registration and invocation logic to use the new approach, and make method metadata caching type-based and immutable. Also expand Bash permissions in settings.local.json and rename ReadVarUIntFromBytes for consistency. This improves performance, maintainability, and code clarity for dynamic SignalR method invocation.
This commit is contained in:
parent
f388afcede
commit
f875738b08
|
|
@ -4,7 +4,13 @@
|
|||
"Bash(dotnet build:*)",
|
||||
"Bash(dotnet test:*)",
|
||||
"Bash(dotnet run:*)",
|
||||
"Bash(dir /B /O-D *.log)"
|
||||
"Bash(dir /B /O-D *.log)",
|
||||
"Bash(dir /B /O-D \"H:\\Applications\\Aycode\\Source\\AyCode.Core\\Test_Benchmark_Results\\Benchmark\")",
|
||||
"Bash(tasklist:*)",
|
||||
"Bash(findstr:*)",
|
||||
"Bash(cmd /c \"cd /d H:\\Applications\\Aycode\\Source\\AyCode.Core\\Test_Benchmark_Results\\Benchmark && dir /B /O-D *.log\")",
|
||||
"Bash(ls:*)",
|
||||
"Bash(git checkout:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -239,7 +239,7 @@ public class BenchmarkSignalRHub : AcWebSignalRHubBase<BenchmarkSignalRTags, Tes
|
|||
public void RegisterService(object service, IAcSignalRHubItemServer client)
|
||||
{
|
||||
_callerClient = client;
|
||||
DynamicMethodCallModels.Add(new AcDynamicMethodCallModel<SignalRAttribute>(service));
|
||||
DynamicMethodRegistry.Register(service);
|
||||
}
|
||||
|
||||
protected override string GetConnectionId() => "benchmark-connection";
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Collections.Concurrent;
|
||||
using System.Collections.Frozen;
|
||||
using System.Reflection;
|
||||
using AyCode.Services.SignalRs;
|
||||
|
||||
|
|
@ -6,9 +7,18 @@ namespace AyCode.Models.Server.DynamicMethods;
|
|||
|
||||
public class AcDynamicMethodCallModel<TAttribute> where TAttribute : TagAttribute
|
||||
{
|
||||
public object InstanceObject { get; init; }
|
||||
public ConcurrentDictionary<int, AcMethodInfoModel<TAttribute>> MethodsByMessageTag { get; init; } = new();
|
||||
/// <summary>
|
||||
/// Statikus cache a típusok metódus metaadataihoz - a reflection csak egyszer fut le típusonként.
|
||||
/// Key: Instance típus -> Value: immutable frozen dictionary a metódusokkal messageTag szerint
|
||||
/// </summary>
|
||||
private static readonly ConcurrentDictionary<Type, FrozenDictionary<int, AcMethodInfoModel<TAttribute>>> _typeMethodCache = new();
|
||||
|
||||
public object InstanceObject { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Immutable dictionary a metódusokkal messageTag szerint. Csak olvasásra használatos.
|
||||
/// </summary>
|
||||
public FrozenDictionary<int, AcMethodInfoModel<TAttribute>> MethodsByMessageTag { get; init; }
|
||||
|
||||
public AcDynamicMethodCallModel(Type instanceObjectType) : this(instanceObjectType, null!)
|
||||
{
|
||||
|
|
@ -21,15 +31,30 @@ public class AcDynamicMethodCallModel<TAttribute> where TAttribute : TagAttribut
|
|||
public AcDynamicMethodCallModel(object instanceObject)
|
||||
{
|
||||
InstanceObject = instanceObject;
|
||||
MethodsByMessageTag = GetOrCreateMethodCache(instanceObject.GetType());
|
||||
}
|
||||
|
||||
foreach (var methodInfo in instanceObject.GetType().GetMethods())
|
||||
/// <summary>
|
||||
/// Visszaadja a cache-elt metódus metaadatokat, vagy létrehozza őket ha még nincsenek.
|
||||
/// A reflection csak egyszer fut le típusonként.
|
||||
/// </summary>
|
||||
private static FrozenDictionary<int, AcMethodInfoModel<TAttribute>> GetOrCreateMethodCache(Type instanceType)
|
||||
{
|
||||
return _typeMethodCache.GetOrAdd(instanceType, type =>
|
||||
{
|
||||
var methods = new Dictionary<int, AcMethodInfoModel<TAttribute>>();
|
||||
|
||||
foreach (var methodInfo in type.GetMethods())
|
||||
{
|
||||
if (methodInfo.GetCustomAttribute(typeof(TAttribute)) is not TAttribute attribute) continue;
|
||||
|
||||
if (MethodsByMessageTag.ContainsKey(attribute.MessageTag))
|
||||
if (methods.ContainsKey(attribute.MessageTag))
|
||||
throw new Exception($"Multiple SignaRMessageTag! messageTag: {attribute.MessageTag}; methodName: {methodInfo.Name}");
|
||||
|
||||
MethodsByMessageTag[attribute.MessageTag] = new AcMethodInfoModel<TAttribute>(attribute, methodInfo!);
|
||||
}
|
||||
methods[attribute.MessageTag] = new AcMethodInfoModel<TAttribute>(attribute, methodInfo!);
|
||||
}
|
||||
|
||||
return methods.ToFrozenDictionary();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
using System.Collections.Concurrent;
|
||||
using System.Reflection;
|
||||
using AyCode.Services.SignalRs;
|
||||
|
||||
namespace AyCode.Models.Server.DynamicMethods;
|
||||
|
||||
/// <summary>
|
||||
/// Registry for dynamic method lookups with lazy initialization.
|
||||
/// Caches method metadata statically (by messageTag), resolves instances per-request.
|
||||
/// </summary>
|
||||
/// <typeparam name="TAttribute">The attribute type used to mark methods (e.g., SignalRAttribute)</typeparam>
|
||||
public class AcDynamicMethodRegistry<TAttribute> where TAttribute : TagAttribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Statikus cache: messageTag → (DeclaringType, MethodInfo)
|
||||
/// A reflection eredménye, nem változik runtime-ban.
|
||||
/// null érték = már kerestük, de nem találtuk.
|
||||
/// </summary>
|
||||
private static readonly ConcurrentDictionary<int, (Type DeclaringType, AcMethodInfoModel<TAttribute> Method)?> _methodLookupCache = new();
|
||||
|
||||
/// <summary>
|
||||
/// Instance array - NEM statikus, request/Hub-specifikus.
|
||||
/// Array is faster than List for small fixed-size collections (2-5 elements).
|
||||
/// </summary>
|
||||
private object[] _instances = [];
|
||||
private int _count;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the capacity of the instance array.
|
||||
/// Set this before calling Register() to avoid allocations.
|
||||
/// </summary>
|
||||
public int CahcheSizeCapacity
|
||||
{
|
||||
get => _instances.Length;
|
||||
set => _instances = new object[value];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers an instance for method lookup.
|
||||
/// No reflection happens here - just stores the instance reference.
|
||||
/// </summary>
|
||||
public void Register(object instance)
|
||||
{
|
||||
if (_count >= _instances.Length)
|
||||
{
|
||||
// CahcheSizeCapacity not set or exceeded - resize
|
||||
var newSize = _instances.Length == 0 ? 4 : _instances.Length * 2;
|
||||
Array.Resize(ref _instances, newSize);
|
||||
}
|
||||
_instances[_count++] = instance;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the method and instance for a given messageTag.
|
||||
/// Uses cached lookup when possible, falls back to lazy search.
|
||||
/// </summary>
|
||||
public (object Instance, AcMethodInfoModel<TAttribute> Method)? GetMethodByMessageTag(int messageTag)
|
||||
{
|
||||
// 1. Check cache first
|
||||
if (_methodLookupCache.TryGetValue(messageTag, out var cached))
|
||||
{
|
||||
if (cached == null)
|
||||
return null; // Already searched, not found
|
||||
|
||||
// Find the instance of the cached type
|
||||
var instance = FindInstanceByType(cached.Value.DeclaringType);
|
||||
if (instance != null)
|
||||
return (instance, cached.Value.Method);
|
||||
}
|
||||
|
||||
// 2. Lazy search through registered instances
|
||||
for (var i = 0; i < _count; i++)
|
||||
{
|
||||
var instance = _instances[i];
|
||||
var type = instance.GetType();
|
||||
|
||||
// Search methods with TAttribute
|
||||
foreach (var methodInfo in type.GetMethods())
|
||||
{
|
||||
if (methodInfo.GetCustomAttribute(typeof(TAttribute)) is not TAttribute attribute)
|
||||
continue;
|
||||
|
||||
if (attribute.MessageTag == messageTag)
|
||||
{
|
||||
var method = new AcMethodInfoModel<TAttribute>(attribute, methodInfo);
|
||||
_methodLookupCache[messageTag] = (type, method);
|
||||
return (instance, method);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Not found - cache this result too
|
||||
_methodLookupCache[messageTag] = null;
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds an instance by its type from the registered instances.
|
||||
/// </summary>
|
||||
private object? FindInstanceByType(Type type)
|
||||
{
|
||||
for (var i = 0; i < _count; i++)
|
||||
{
|
||||
var instance = _instances[i];
|
||||
if (instance.GetType() == type)
|
||||
return instance;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of registered instances.
|
||||
/// </summary>
|
||||
public int Count => _count;
|
||||
}
|
||||
|
|
@ -60,7 +60,7 @@ public class TestableSignalRHub2 : AcWebSignalRHubBase<TestSignalRTags, TestLogg
|
|||
public void RegisterService(object service, IAcSignalRHubItemServer callerClient)
|
||||
{
|
||||
_callerClient = callerClient;
|
||||
DynamicMethodCallModels.Add(new AcDynamicMethodCallModel<SignalRAttribute>(service));
|
||||
DynamicMethodRegistry.Register(service);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -16,7 +16,11 @@ namespace AyCode.Services.Server.SignalRs;
|
|||
public abstract class AcWebSignalRHubBase<TSignalRTags, TLogger>(IConfiguration configuration, TLogger logger)
|
||||
: Hub<IAcSignalRHubItemServer>, IAcSignalRHubServer where TSignalRTags : AcSignalRTags where TLogger : AcLoggerBase
|
||||
{
|
||||
protected readonly List<AcDynamicMethodCallModel<SignalRAttribute>> DynamicMethodCallModels = [];
|
||||
/// <summary>
|
||||
/// Registry for dynamic method lookups with lazy initialization.
|
||||
/// </summary>
|
||||
protected readonly AcDynamicMethodRegistry<SignalRAttribute> DynamicMethodRegistry = new();
|
||||
|
||||
protected TLogger Logger = logger;
|
||||
protected IConfiguration Configuration = configuration;
|
||||
|
||||
|
|
@ -111,7 +115,7 @@ public abstract class AcWebSignalRHubBase<TSignalRTags, TLogger>(IConfiguration
|
|||
/// <summary>
|
||||
/// Reads a VarUInt from byte array at given position.
|
||||
/// </summary>
|
||||
private static (uint value, int bytesRead) ReadVarUIntFromBytes(byte[] data, int startPos)
|
||||
private static (uint value, int bytesRead) ReadVarUINTFromBytes(byte[] data, int startPos)
|
||||
{
|
||||
uint value = 0;
|
||||
var shift = 0;
|
||||
|
|
@ -235,7 +239,7 @@ public abstract class AcWebSignalRHubBase<TSignalRTags, TLogger>(IConfiguration
|
|||
{
|
||||
// Read property count as VarUInt
|
||||
var pos = 2;
|
||||
var (propCount, bytesRead) = ReadVarUIntFromBytes(responseData, pos);
|
||||
var (propCount, bytesRead) = ReadVarUINTFromBytes(responseData, pos);
|
||||
pos += bytesRead;
|
||||
|
||||
Logger.Info($"Header property count: {propCount}");
|
||||
|
|
@ -243,7 +247,7 @@ public abstract class AcWebSignalRHubBase<TSignalRTags, TLogger>(IConfiguration
|
|||
for (var i = 0; i < (int)propCount && pos < responseData.Length; i++)
|
||||
{
|
||||
// Read string length as VarUInt
|
||||
var (strLen, strLenBytes) = ReadVarUIntFromBytes(responseData, pos);
|
||||
var (strLen, strLenBytes) = ReadVarUINTFromBytes(responseData, pos);
|
||||
pos += strLenBytes;
|
||||
|
||||
if (pos + (int)strLen <= responseData.Length)
|
||||
|
|
@ -290,19 +294,20 @@ public abstract class AcWebSignalRHubBase<TSignalRTags, TLogger>(IConfiguration
|
|||
{
|
||||
responseData = null;
|
||||
|
||||
foreach (var methodsByDeclaringObject in DynamicMethodCallModels)
|
||||
{
|
||||
if (!methodsByDeclaringObject.MethodsByMessageTag.TryGetValue(messageTag, out var methodInfoModel))
|
||||
continue;
|
||||
var result = DynamicMethodRegistry.GetMethodByMessageTag(messageTag);
|
||||
if (!result.HasValue)
|
||||
return false;
|
||||
|
||||
var methodName = $"{methodsByDeclaringObject.InstanceObject.GetType().Name}.{methodInfoModel.MethodInfo.Name}";
|
||||
var (instance, methodInfoModel) = result.Value;
|
||||
var methodName = $"{instance.GetType().Name}.{methodInfoModel.MethodInfo.Name}";
|
||||
var paramValues = DeserializeParameters(message, methodInfoModel, tagName, methodName);
|
||||
|
||||
Logger.Debug(paramValues == null ? $"Found dynamic method for the tag! method: {methodName}(); {tagName}" : $"Found dynamic method for the tag! method: {methodName}({string.Join(", ", methodInfoModel.ParamInfos.Select(x => x.Name))}); {tagName}");
|
||||
Logger.Debug(paramValues == null
|
||||
? $"Found dynamic method for the tag! method: {methodName}(); {tagName}"
|
||||
: $"Found dynamic method for the tag! method: {methodName}({string.Join(", ", methodInfoModel.ParamInfos.Select(x => x.Name))}); {tagName}");
|
||||
|
||||
responseData = methodInfoModel.MethodInfo.InvokeMethod(methodsByDeclaringObject.InstanceObject, paramValues);
|
||||
responseData = methodInfoModel.MethodInfo.InvokeMethod(instance, paramValues);
|
||||
|
||||
// Log type information if diagnostics enabled
|
||||
if (EnableBinaryDiagnostics && responseData != null)
|
||||
{
|
||||
LogResponseDataTypeInfo(responseData);
|
||||
|
|
@ -314,9 +319,6 @@ public abstract class AcWebSignalRHubBase<TSignalRTags, TLogger>(IConfiguration
|
|||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes parameters from the message based on method signature.
|
||||
/// Supports optional parameters with default values.
|
||||
|
|
|
|||
Loading…
Reference in New Issue