diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 02b9ce9..c1e4355 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -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:*)" ] } } diff --git a/AyCode.Benchmark/SignalRRoundTripBenchmarks.cs b/AyCode.Benchmark/SignalRRoundTripBenchmarks.cs index 71f269d..7ee8980 100644 --- a/AyCode.Benchmark/SignalRRoundTripBenchmarks.cs +++ b/AyCode.Benchmark/SignalRRoundTripBenchmarks.cs @@ -239,7 +239,7 @@ public class BenchmarkSignalRHub : AcWebSignalRHubBase(service)); + DynamicMethodRegistry.Register(service); } protected override string GetConnectionId() => "benchmark-connection"; diff --git a/AyCode.Models.Server/DynamicMethods/AcDynamicMethodCallModel.cs b/AyCode.Models.Server/DynamicMethods/AcDynamicMethodCallModel.cs index f43f128..0ea5e5e 100644 --- a/AyCode.Models.Server/DynamicMethods/AcDynamicMethodCallModel.cs +++ b/AyCode.Models.Server/DynamicMethods/AcDynamicMethodCallModel.cs @@ -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 where TAttribute : TagAttribute { - public object InstanceObject { get; init; } - public ConcurrentDictionary> MethodsByMessageTag { get; init; } = new(); + /// + /// 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 + /// + private static readonly ConcurrentDictionary>> _typeMethodCache = new(); + public object InstanceObject { get; init; } + + /// + /// Immutable dictionary a metódusokkal messageTag szerint. Csak olvasásra használatos. + /// + public FrozenDictionary> MethodsByMessageTag { get; init; } public AcDynamicMethodCallModel(Type instanceObjectType) : this(instanceObjectType, null!) { @@ -21,15 +31,30 @@ public class AcDynamicMethodCallModel where TAttribute : TagAttribut public AcDynamicMethodCallModel(object instanceObject) { InstanceObject = instanceObject; + MethodsByMessageTag = GetOrCreateMethodCache(instanceObject.GetType()); + } - foreach (var methodInfo in instanceObject.GetType().GetMethods()) + /// + /// Visszaadja a cache-elt metódus metaadatokat, vagy létrehozza őket ha még nincsenek. + /// A reflection csak egyszer fut le típusonként. + /// + private static FrozenDictionary> GetOrCreateMethodCache(Type instanceType) + { + return _typeMethodCache.GetOrAdd(instanceType, type => { - if (methodInfo.GetCustomAttribute(typeof(TAttribute)) is not TAttribute attribute) continue; + var methods = new Dictionary>(); - if (MethodsByMessageTag.ContainsKey(attribute.MessageTag)) - throw new Exception($"Multiple SignaRMessageTag! messageTag: {attribute.MessageTag}; methodName: {methodInfo.Name}"); + foreach (var methodInfo in type.GetMethods()) + { + if (methodInfo.GetCustomAttribute(typeof(TAttribute)) is not TAttribute attribute) continue; - MethodsByMessageTag[attribute.MessageTag] = new AcMethodInfoModel(attribute, methodInfo!); - } + if (methods.ContainsKey(attribute.MessageTag)) + throw new Exception($"Multiple SignaRMessageTag! messageTag: {attribute.MessageTag}; methodName: {methodInfo.Name}"); + + methods[attribute.MessageTag] = new AcMethodInfoModel(attribute, methodInfo!); + } + + return methods.ToFrozenDictionary(); + }); } } \ No newline at end of file diff --git a/AyCode.Models.Server/DynamicMethods/AcDynamicMethodRegistry.cs b/AyCode.Models.Server/DynamicMethods/AcDynamicMethodRegistry.cs new file mode 100644 index 0000000..3114798 --- /dev/null +++ b/AyCode.Models.Server/DynamicMethods/AcDynamicMethodRegistry.cs @@ -0,0 +1,115 @@ +using System.Collections.Concurrent; +using System.Reflection; +using AyCode.Services.SignalRs; + +namespace AyCode.Models.Server.DynamicMethods; + +/// +/// Registry for dynamic method lookups with lazy initialization. +/// Caches method metadata statically (by messageTag), resolves instances per-request. +/// +/// The attribute type used to mark methods (e.g., SignalRAttribute) +public class AcDynamicMethodRegistry where TAttribute : TagAttribute +{ + /// + /// 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. + /// + private static readonly ConcurrentDictionary Method)?> _methodLookupCache = new(); + + /// + /// Instance array - NEM statikus, request/Hub-specifikus. + /// Array is faster than List for small fixed-size collections (2-5 elements). + /// + private object[] _instances = []; + private int _count; + + /// + /// Gets or sets the capacity of the instance array. + /// Set this before calling Register() to avoid allocations. + /// + public int CahcheSizeCapacity + { + get => _instances.Length; + set => _instances = new object[value]; + } + + /// + /// Registers an instance for method lookup. + /// No reflection happens here - just stores the instance reference. + /// + 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; + } + + /// + /// Finds the method and instance for a given messageTag. + /// Uses cached lookup when possible, falls back to lazy search. + /// + public (object Instance, AcMethodInfoModel 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(attribute, methodInfo); + _methodLookupCache[messageTag] = (type, method); + return (instance, method); + } + } + } + + // 3. Not found - cache this result too + _methodLookupCache[messageTag] = null; + return null; + } + + /// + /// Finds an instance by its type from the registered instances. + /// + private object? FindInstanceByType(Type type) + { + for (var i = 0; i < _count; i++) + { + var instance = _instances[i]; + if (instance.GetType() == type) + return instance; + } + return null; + } + + /// + /// Gets the number of registered instances. + /// + public int Count => _count; +} diff --git a/AyCode.Services.Server.Tests/SignalRs/TestableSignalRHub2.cs b/AyCode.Services.Server.Tests/SignalRs/TestableSignalRHub2.cs index 41c5d1e..7f666e1 100644 --- a/AyCode.Services.Server.Tests/SignalRs/TestableSignalRHub2.cs +++ b/AyCode.Services.Server.Tests/SignalRs/TestableSignalRHub2.cs @@ -60,7 +60,7 @@ public class TestableSignalRHub2 : AcWebSignalRHubBase(service)); + DynamicMethodRegistry.Register(service); } /// diff --git a/AyCode.Services.Server/SignalRs/AcWebSignalRHubBase.cs b/AyCode.Services.Server/SignalRs/AcWebSignalRHubBase.cs index 8a711e3..eef6086 100644 --- a/AyCode.Services.Server/SignalRs/AcWebSignalRHubBase.cs +++ b/AyCode.Services.Server/SignalRs/AcWebSignalRHubBase.cs @@ -16,7 +16,11 @@ namespace AyCode.Services.Server.SignalRs; public abstract class AcWebSignalRHubBase(IConfiguration configuration, TLogger logger) : Hub, IAcSignalRHubServer where TSignalRTags : AcSignalRTags where TLogger : AcLoggerBase { - protected readonly List> DynamicMethodCallModels = []; + /// + /// Registry for dynamic method lookups with lazy initialization. + /// + protected readonly AcDynamicMethodRegistry DynamicMethodRegistry = new(); + protected TLogger Logger = logger; protected IConfiguration Configuration = configuration; @@ -111,7 +115,7 @@ public abstract class AcWebSignalRHubBase(IConfiguration /// /// Reads a VarUInt from byte array at given position. /// - 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(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(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,31 +294,29 @@ public abstract class AcWebSignalRHubBase(IConfiguration { responseData = null; - foreach (var methodsByDeclaringObject in DynamicMethodCallModels) + var result = DynamicMethodRegistry.GetMethodByMessageTag(messageTag); + if (!result.HasValue) + return false; + + 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}"); + + responseData = methodInfoModel.MethodInfo.InvokeMethod(instance, paramValues); + + if (EnableBinaryDiagnostics && responseData != null) { - if (!methodsByDeclaringObject.MethodsByMessageTag.TryGetValue(messageTag, out var methodInfoModel)) - continue; - - var methodName = $"{methodsByDeclaringObject.InstanceObject.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}"); - - responseData = methodInfoModel.MethodInfo.InvokeMethod(methodsByDeclaringObject.InstanceObject, paramValues); - - // Log type information if diagnostics enabled - if (EnableBinaryDiagnostics && responseData != null) - { - LogResponseDataTypeInfo(responseData); - } - - if (methodInfoModel.Attribute.SendToOtherClientType != SendToClientType.None) - SendMessageToOthers(methodInfoModel.Attribute.SendToOtherClientTag, responseData).Forget(); - - return true; + LogResponseDataTypeInfo(responseData); } - return false; + if (methodInfoModel.Attribute.SendToOtherClientType != SendToClientType.None) + SendMessageToOthers(methodInfoModel.Attribute.SendToOtherClientTag, responseData).Forget(); + + return true; } ///