diff --git a/src/Agent/NewRelic/Agent/Core/AgentShim.cs b/src/Agent/NewRelic/Agent/Core/AgentShim.cs index e2302b91b0..276bf1adb8 100644 --- a/src/Agent/NewRelic/Agent/Core/AgentShim.cs +++ b/src/Agent/NewRelic/Agent/Core/AgentShim.cs @@ -109,11 +109,11 @@ public static object GetFinishTracerDelegateFunc() } /// - /// This method is used to work around .net framework ad .net < 6.0 not supporting generic delegates + /// This method is used to work around .net framework and .net < 6.0 not supporting generic delegates /// with 11 parameters. /// /// An array of boxed parameters for the GetFinishTracerDelegate method. - /// + /// The delegate to invoke when a method completes or results in an exception. public static Action GetFinishTracerDelegateParameterWrapper(object[] parameters) { return GetFinishTracerDelegate( diff --git a/src/Agent/NewRelic/Agent/Core/ProfilerAgentMethodCallCache.cs b/src/Agent/NewRelic/Agent/Core/ProfilerAgentMethodCallCache.cs index edd9a7a175..5dcbcb25f3 100644 --- a/src/Agent/NewRelic/Agent/Core/ProfilerAgentMethodCallCache.cs +++ b/src/Agent/NewRelic/Agent/Core/ProfilerAgentMethodCallCache.cs @@ -1,9 +1,10 @@ -// Copyright 2020 New Relic, Inc. All rights reserved. +// Copyright 2020 New Relic, Inc. All rights reserved. // SPDX-License-Identifier: Apache-2.0 using System; using System.Collections.Concurrent; -using System.Reflection; +using System.Collections.Generic; +using System.Linq.Expressions; namespace NewRelic.Agent.Core { @@ -12,50 +13,90 @@ namespace NewRelic.Agent.Core /// public class ProfilerAgentMethodCallCache { - private static readonly ConcurrentDictionary _methodInfoCache = new ConcurrentDictionary(); + private static readonly ConcurrentDictionary> _invokerCache = new ConcurrentDictionary>(); /// /// This method is invoked once using reflection by the byte-code injected by the profiler. This method is used to get a - /// reference to the method that is used to cache reflection lookups for Agent methods. These methods are either - /// Public Agent API methods, or AgentShim methods. + /// reference to the method that is used to cache reflection lookups and Expression compilation for Agent methods. + /// This is currently used for Agent API methods. /// /// - /// A reference to the method that can access the method info cache. The result is treated as an object + /// A reference to the method that is used to invoked the requrested agent method. The result is treated as an object /// to simplify the type definition injected by the profiler to store this reference. /// - public static object GetMethodCacheFunc() + public static object GetInvokerFromCache() { - return (Func)GetAgentMethodInfoFromCache; + return (Func)GetAndInvokeMethodFromCache; } - private static MethodInfo GetAgentMethodInfoFromCache(string key, string className, string methodName, Type[] types) + /// + /// This is the method that is invoked by the injected byte code for the Agent API. + /// It uses a cache so that the more expensive operations are only executed once. + /// This method will invoke the requested method using the specified parameters. + /// The method will always return a value. This value will either be the result of the + /// requested method call, or null if the requested method has a void return type. + /// + /// The cache key generated by the profiler. + /// The name of the type containing the static method to execute. + /// The name of the static method to execute. + /// An array of types that represent the type for each parameter in the method to invoke. + /// The type returned by the method to invoke. A null is used to represent a void return type. + /// The parameters to invoke the method with. + /// The value returned from invoking the requested method, or null if the method has a void return type. + public static object GetAndInvokeMethodFromCache(string key, string className, string methodName, Type[] originalTypes, Type returnType, object[] originalParameters) { - var methodInfoFactory = new MethodInfoCacheItemFactory(className, methodName, types); - return _methodInfoCache!.GetOrAdd(key, methodInfoFactory.GetMethodInfo); + var types = originalTypes ?? Array.Empty(); + var parameters = originalParameters ?? Array.Empty(); + + if (!_invokerCache.TryGetValue(key, out var invoker)) + { + var type = Type.GetType(className); + var mi = type.GetMethod(methodName, types); + var delegateType = returnType == null ? Expression.GetActionType(types) : Expression.GetFuncType(GetTypesForFunc(types, returnType)); + var methodDelegate = mi.CreateDelegate(delegateType); + + /* Create a function similar to the following code + * object InovkeMethod(object[] parameters) { + * return methodDelegate.Invoke((types[0])parameters[0]); + * } + */ + var parametersParam = Expression.Parameter(typeof(object[]), "parameters"); + var methodToCall = Expression.Constant(methodDelegate, delegateType); + IEnumerable parameterExpressions = GetInvokerParameterExpressions(parametersParam, types); + var invocationExpression = Expression.Invoke(methodToCall, parameterExpressions); + + var lambdaExpression = returnType != null ? + Expression.Lambda>(invocationExpression, parametersParam) : + Expression.Lambda>(Expression.Block(invocationExpression, Expression.Constant(null)), parametersParam); + + invoker = lambdaExpression.Compile(); + _invokerCache.TryAdd(key, invoker); + } + + return invoker(parameters); } - // Using a struct to try to take advantage of stack allocation to reduce overall allocations. - private struct MethodInfoCacheItemFactory + private static IEnumerable GetInvokerParameterExpressions(Expression parametersExpression, Type[] types) { - private string _className; - private string _methodName; - private Type[] _types; - - public MethodInfoCacheItemFactory(string className, string methodName, Type[] types) + for (var i = 0; i < types.Length; i++) { - _className = className; - _methodName = methodName; - _types = types; + var getParameterAtIndex = Expression.ArrayAccess(parametersExpression, Expression.Constant(i)); + yield return Expression.Convert(getParameterAtIndex, types[i]); } + } - public MethodInfo GetMethodInfo(string _) + private static Type[] GetTypesForFunc(Type[] parameterTypes, Type returnType) + { + if (parameterTypes.Length == 0) { - var type = Type.GetType(_className); + return new Type[] { returnType }; + } - var method = _types != null ? type.GetMethod(_methodName, _types) : type.GetMethod(_methodName); + var types = new Type[parameterTypes.Length + 1]; + Array.Copy(parameterTypes, types, parameterTypes.Length); + types[parameterTypes.Length] = returnType; - return method; - } + return types; } } } diff --git a/src/Agent/NewRelic/Profiler/MethodRewriter/ApiFunctionManipulator.h b/src/Agent/NewRelic/Profiler/MethodRewriter/ApiFunctionManipulator.h index c3d97ce5ea..0b11c9a87a 100644 --- a/src/Agent/NewRelic/Profiler/MethodRewriter/ApiFunctionManipulator.h +++ b/src/Agent/NewRelic/Profiler/MethodRewriter/ApiFunctionManipulator.h @@ -45,21 +45,68 @@ namespace NewRelic { namespace Profiler { namespace MethodRewriter TryCatch( [&]() { - // delegate = System.CannotUnloadAppDomainException.GetMethodFromAppDomainStorageOrReflectionOrThrow("NewRelic_Delegate_API_", "C:\path\to\NewRelic.Agent.Core", "NewRelic.Core.AgentApi", "", new object[] { }) - LoadMethodInfo(_instrumentationSettings->GetCorePath(), _X("NewRelic.Agent.Core.AgentApi"), _function->GetFunctionName(), _function->GetFunctionId(), GetArrayOfTypeParametersLamdba()); - - _instructions->Append(_X("ldnull")); - BuildObjectArrayOfParameters(); - _instructions->Append(_X("call instance object [") + _instructions->GetCoreLibAssemblyName() + _X("]System.Reflection.MethodBase::Invoke(object, object[])")); - - if (_methodSignature->_returnType->_kind == SignatureParser::ReturnType::Kind::VOID_RETURN_TYPE) + if (_agentCallStrategy == AgentCallStyle::Strategy::InAgentCache) { - _instructions->Append(_X("pop")); + // Ensure that the managed agent is loaded + _instructions->AppendString(_instrumentationSettings->GetCorePath()); + _instructions->Append(_X("call void [") + _instructions->GetCoreLibAssemblyName() + _X("]System.CannotUnloadAppDomainException::EnsureInitialized(string)")); + + // Get the Func holding a reference to the ProfilerAgentMethodCallCache.GetAndInvokeMethodFromCache method + _instructions->Append(_X("call object [") + _instructions->GetCoreLibAssemblyName() + _X("]System.CannotUnloadAppDomainException::GetMethodCacheLookupMethod()")); + _instructions->Append(CEE_CASTCLASS, _X("class [") + _instructions->GetCoreLibAssemblyName() + _X("]System.Func`7GetCoreLibAssemblyName() + _X("]System.Type[], class [") + _instructions->GetCoreLibAssemblyName() + _X("]System.Type, object[], object>")); + + xstring_t className = _X("NewRelic.Agent.Core.AgentApi"); + xstring_t methodName = _function->GetFunctionName(); + xstring_t keyName = className + _X(".") + methodName + _X("_") + to_xstring((unsigned long)_function->GetFunctionId()); + auto argumentTypesLambda = GetArrayOfTypeParametersLamdba(); + + _instructions->AppendString(keyName); + _instructions->AppendString(className); + _instructions->AppendString(methodName); + + if (argumentTypesLambda == NULL) + { + _instructions->Append(CEE_LDNULL); + } + else + { + argumentTypesLambda(); + } + + _instructions->AppendTypeOfArgument(_methodSignature->_returnType); + + BuildObjectArrayOfParameters(); + + _instructions->Append(CEE_CALLVIRT, _X("instance !6 class [") + _instructions->GetCoreLibAssemblyName() + _X("]System.Func`7GetCoreLibAssemblyName() + _X("]System.Type[], class [") + _instructions->GetCoreLibAssemblyName() + _X("]System.Type, object[], object>::Invoke(!0, !1, !2, !3, !4, !5)")); + + if (_methodSignature->_returnType->_kind == SignatureParser::ReturnType::Kind::VOID_RETURN_TYPE) + { + _instructions->Append(_X("pop")); + } + else { + // we can't leave an object on the stack and CEE_LEAVE a protected block. + // we have to store it in a local and reload it outside of the try..catch. + _instructions->AppendStoreLocal(resultLocalIndex); + } } - else { - // we can't leave an object on the stack and CEE_LEAVE a protected block. - // we have to store it in a local and reload it outside of the try..catch. - _instructions->AppendStoreLocal(resultLocalIndex); + else + { + // delegate = System.CannotUnloadAppDomainException.GetMethodFromAppDomainStorageOrReflectionOrThrow("NewRelic_Delegate_API_", "C:\path\to\NewRelic.Agent.Core", "NewRelic.Core.AgentApi", "", new object[] { }) + LoadMethodInfo(_instrumentationSettings->GetCorePath(), _X("NewRelic.Agent.Core.AgentApi"), _function->GetFunctionName(), _function->GetFunctionId(), GetArrayOfTypeParametersLamdba()); + + _instructions->Append(_X("ldnull")); + BuildObjectArrayOfParameters(); + _instructions->Append(_X("call instance object [") + _instructions->GetCoreLibAssemblyName() + _X("]System.Reflection.MethodBase::Invoke(object, object[])")); + + if (_methodSignature->_returnType->_kind == SignatureParser::ReturnType::Kind::VOID_RETURN_TYPE) + { + _instructions->Append(_X("pop")); + } + else { + // we can't leave an object on the stack and CEE_LEAVE a protected block. + // we have to store it in a local and reload it outside of the try..catch. + _instructions->AppendStoreLocal(resultLocalIndex); + } } }, [&]() diff --git a/src/Agent/NewRelic/Profiler/MethodRewriter/FunctionManipulator.h b/src/Agent/NewRelic/Profiler/MethodRewriter/FunctionManipulator.h index a1de849b1e..e838eee036 100644 --- a/src/Agent/NewRelic/Profiler/MethodRewriter/FunctionManipulator.h +++ b/src/Agent/NewRelic/Profiler/MethodRewriter/FunctionManipulator.h @@ -320,25 +320,7 @@ namespace NewRelic { namespace Profiler { namespace MethodRewriter // The function id is used as a tie-breaker for overloaded methods when computing the key name for the cache. void LoadMethodInfo(xstring_t assemblyPath, xstring_t className, xstring_t methodName, uintptr_t functionId, std::function argumentTypesLambda) { - if (_agentCallStrategy == AgentCallStyle::Strategy::InAgentCache) - { - auto keyName = className + _X(".") + methodName + _X("_") + to_xstring((unsigned long)functionId); - _instructions->AppendString(keyName); - _instructions->AppendString(assemblyPath); - _instructions->AppendString(className); - _instructions->AppendString(methodName); - if (argumentTypesLambda == NULL) - { - _instructions->Append(CEE_LDNULL); - } - else - { - argumentTypesLambda(); - } - - _instructions->Append(CEE_CALL, _X("class [") + _instructions->GetCoreLibAssemblyName() + _X("]System.Reflection.MethodInfo [") + _instructions->GetCoreLibAssemblyName() + _X("]System.CannotUnloadAppDomainException::GetMethodInfoFromAgentCache(string,string,string,string,class [") + _instructions->GetCoreLibAssemblyName() + _X("]System.Type[])")); - } - else if (_agentCallStrategy == AgentCallStyle::Strategy::AppDomainCache) + if (_agentCallStrategy == AgentCallStyle::Strategy::AppDomainCache) { auto keyName = className + _X(".") + methodName + _X("_") + to_xstring((unsigned long)functionId); _instructions->AppendString(keyName); @@ -356,11 +338,16 @@ namespace NewRelic { namespace Profiler { namespace MethodRewriter _instructions->Append(CEE_CALL, _X("class [") + _instructions->GetCoreLibAssemblyName() + _X("]System.Reflection.MethodInfo [") + _instructions->GetCoreLibAssemblyName() + _X("]System.CannotUnloadAppDomainException::GetMethodFromAppDomainStorageOrReflectionOrThrow(string,string,string,string,class [") + _instructions->GetCoreLibAssemblyName() + _X("]System.Type[])")); } - else + else if (_agentCallStrategy == AgentCallStyle::Strategy::Reflection) { LoadType(assemblyPath, className); LoadMethodInfoFromType(methodName, argumentTypesLambda); } + else + { + LogError(L"Attempting to call LoadMethodInfo when an AgentCallStyle::Strategy that does not support loading method info is configured."); + throw FunctionManipulatorException(); + } } // Creates an array of elementLoadLanbdas.size() and loads the elements into the array diff --git a/src/Agent/NewRelic/Profiler/MethodRewriter/HelperFunctionManipulator.h b/src/Agent/NewRelic/Profiler/MethodRewriter/HelperFunctionManipulator.h index 1b5fa6a6cf..d200b97573 100644 --- a/src/Agent/NewRelic/Profiler/MethodRewriter/HelperFunctionManipulator.h +++ b/src/Agent/NewRelic/Profiler/MethodRewriter/HelperFunctionManipulator.h @@ -211,7 +211,7 @@ namespace NewRelic { namespace Profiler { namespace MethodRewriter { _instructions->Append(CEE_LDARG_0); _instructions->AppendString(_X("NewRelic.Agent.Core.ProfilerAgentMethodCallCache")); - _instructions->AppendString(_X("GetMethodCacheFunc")); + _instructions->AppendString(_X("GetInvokerFromCache")); _instructions->Append(CEE_LDNULL); _instructions->Append(CEE_CALL, _X("class System.Reflection.MethodInfo System.CannotUnloadAppDomainException::GetMethodViaReflectionOrThrow(string,string,string,class System.Type[])")); diff --git a/src/Agent/NewRelic/Profiler/MethodRewriter/InstructionSet.h b/src/Agent/NewRelic/Profiler/MethodRewriter/InstructionSet.h index 08c9d44505..4da6b7cbf7 100644 --- a/src/Agent/NewRelic/Profiler/MethodRewriter/InstructionSet.h +++ b/src/Agent/NewRelic/Profiler/MethodRewriter/InstructionSet.h @@ -346,6 +346,22 @@ namespace NewRelic { namespace Profiler { namespace MethodRewriter } } + void AppendTypeOfArgument(SignatureParser::ReturnTypePtr returnType) + { + auto typeToken = GetTypeTokenForReturnType(returnType); + + if (typeToken != 0) + { + Append(CEE_LDTOKEN, typeToken); + Append(_X("call class [") + _coreLibAssemblyName + _X("]System.Type[") + _coreLibAssemblyName + _X("]System.Type::GetTypeFromHandle(valuetype[") + _coreLibAssemblyName + _X("]System.RuntimeTypeHandle)")); + } + else + { + // void return type + Append(CEE_LDNULL); + } + } + // append a load argument instruction void AppendLoadArgument(uint16_t argumentIndex) { @@ -544,6 +560,32 @@ namespace NewRelic { namespace Profiler { namespace MethodRewriter } } + uint32_t GetTypeTokenForReturnType(SignatureParser::ReturnTypePtr returnType) + { + switch (returnType->_kind) + { + case SignatureParser::ReturnType::Kind::VOID_RETURN_TYPE: + { + return 0; + } + case SignatureParser::ReturnType::Kind::TYPED_BY_REF_RETURN_TYPE: + { + auto token = _tokenizer->GetTypeRefToken(_coreLibAssemblyName, _X("System.TypedReference")); + return token; + } + case SignatureParser::ReturnType::Kind::TYPED_RETURN_TYPE: + { + auto typedReturnType = std::static_pointer_cast(returnType); + return GetTypeTokenForType(typedReturnType->_type); + } + default: + { + LogError(L"Unhandled return type encountered in InstructionSet::GetTypeTokenForReturnType. Kind: ", std::hex, std::showbase, returnType->_kind, std::resetiosflags(std::ios_base::basefield | std::ios_base::showbase)); + throw InstructionSetException(); + } + } + } + uint32_t GetTypeTokenForType(SignatureParser::TypePtr type) { switch (type->_kind) diff --git a/src/Agent/NewRelic/Profiler/ModuleInjector/ModuleInjector.h b/src/Agent/NewRelic/Profiler/ModuleInjector/ModuleInjector.h index 7adcdcd5e3..4acb433b2a 100644 --- a/src/Agent/NewRelic/Profiler/ModuleInjector/ModuleInjector.h +++ b/src/Agent/NewRelic/Profiler/ModuleInjector/ModuleInjector.h @@ -67,8 +67,8 @@ namespace NewRelic { namespace Profiler { namespace ModuleInjector ManagedMethodToInject(_X("[mscorlib]System.CannotUnloadAppDomainException"), _X("GetAgentShimFinishTracerDelegateMethod"), _X("object"), _X("")) }; - // When injecting HELPER METHODS into the System.Private.CoreLib assembly, theses references should be local. - // They cannot reference [System.Private.CoreLib] since these methods are being rewritten in System.Private.CoreLib. + // When injecting HELPER METHODS into the Core Lib assembly, theses references should be local. + // They cannot reference an assembly since these methods are being rewritten in the Core Lib so only types available in that lib can be used. constexpr std::array methodImplsToInject{ ManagedMethodToInject(_X("System.CannotUnloadAppDomainException"), _X("LoadAssemblyOrThrow"), _X("class System.Reflection.Assembly"), _X("string")), ManagedMethodToInject(_X("System.CannotUnloadAppDomainException"), _X("GetTypeViaReflectionOrThrow"), _X("class System.Type"), _X("string,string")), @@ -86,7 +86,7 @@ namespace NewRelic { namespace Profiler { namespace ModuleInjector const auto is_coreLib = module.GetIsThisTheCoreLibAssembly(); - // If instrumenting System.Private.CoreLib, use local (to the assembly) references + // If instrumenting Core Lib, use local (to the assembly) references // otherwise use external references auto methods = is_coreLib ? methodImplsToInject diff --git a/src/Agent/NewRelic/Profiler/ModuleInjectorTest/ModuleInjectorTest.cpp b/src/Agent/NewRelic/Profiler/ModuleInjectorTest/ModuleInjectorTest.cpp index 3134f0e278..b4fe3c360f 100644 --- a/src/Agent/NewRelic/Profiler/ModuleInjectorTest/ModuleInjectorTest.cpp +++ b/src/Agent/NewRelic/Profiler/ModuleInjectorTest/ModuleInjectorTest.cpp @@ -130,7 +130,7 @@ namespace NewRelic { namespace Profiler { namespace MethodRewriter { namespace T void AssertHelperMethodsWereInjected(const bool isCoreClr, const bool throwsException) { - std::array expectedMethods = { + std::array expectedMethods = { _X("System.CannotUnloadAppDomainException.LoadAssemblyOrThrow"), _X("System.CannotUnloadAppDomainException.GetTypeViaReflectionOrThrow"), _X("System.CannotUnloadAppDomainException.GetMethodViaReflectionOrThrow"), @@ -139,7 +139,10 @@ namespace NewRelic { namespace Profiler { namespace MethodRewriter { namespace T _X("System.CannotUnloadAppDomainException.StoreMethodInAppDomainStorageOrThrow"), _X("System.CannotUnloadAppDomainException.EnsureInitialized"), _X("System.CannotUnloadAppDomainException.GetMethodInfoFromAgentCache"), - _X("System.CannotUnloadAppDomainException.GetMethodCacheLookupMethod") + _X("System.CannotUnloadAppDomainException.GetMethodCacheLookupMethod"), + _X("System.CannotUnloadAppDomainException.GetAgentShimFinishTracerDelegateMethod"), + _X("System.CannotUnloadAppDomainException.StoreAgentShimFinishTracerDelegateMethod"), + _X("System.CannotUnloadAppDomainException.StoreMethodCacheLookupMethod") }; auto modulePtr = std::make_shared(); @@ -173,7 +176,7 @@ namespace NewRelic { namespace Profiler { namespace MethodRewriter { namespace T void AssertHelperMethodReferencesWereInjected(const bool isCoreClr, const bool throwsException) { const xstring_t expectedAssembly = isCoreClr ? _X("[System.Private.CoreLib]") : _X("[mscorlib]"); - std::array expectedMethods = { + std::array expectedMethods = { expectedAssembly + _X("System.CannotUnloadAppDomainException.LoadAssemblyOrThrow"), expectedAssembly + _X("System.CannotUnloadAppDomainException.GetTypeViaReflectionOrThrow"), expectedAssembly + _X("System.CannotUnloadAppDomainException.GetMethodViaReflectionOrThrow"), @@ -182,7 +185,10 @@ namespace NewRelic { namespace Profiler { namespace MethodRewriter { namespace T expectedAssembly + _X("System.CannotUnloadAppDomainException.StoreMethodInAppDomainStorageOrThrow"), expectedAssembly + _X("System.CannotUnloadAppDomainException.EnsureInitialized"), expectedAssembly + _X("System.CannotUnloadAppDomainException.GetMethodInfoFromAgentCache"), - expectedAssembly + _X("System.CannotUnloadAppDomainException.GetMethodCacheLookupMethod") + expectedAssembly + _X("System.CannotUnloadAppDomainException.GetMethodCacheLookupMethod"), + expectedAssembly + _X("System.CannotUnloadAppDomainException.GetAgentShimFinishTracerDelegateMethod"), + expectedAssembly + _X("System.CannotUnloadAppDomainException.StoreAgentShimFinishTracerDelegateMethod"), + expectedAssembly + _X("System.CannotUnloadAppDomainException.StoreMethodCacheLookupMethod") }; auto modulePtr = std::make_shared(); diff --git a/tests/Agent/UnitTests/Core.UnitTest/ProfilerAgentMethodCallCacheTests.cs b/tests/Agent/UnitTests/Core.UnitTest/ProfilerAgentMethodCallCacheTests.cs index 99082cb384..423afde1a3 100644 --- a/tests/Agent/UnitTests/Core.UnitTest/ProfilerAgentMethodCallCacheTests.cs +++ b/tests/Agent/UnitTests/Core.UnitTest/ProfilerAgentMethodCallCacheTests.cs @@ -1,7 +1,6 @@ // Copyright 2020 New Relic, Inc. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -using System.Reflection; using System; using NUnit.Framework; using NewRelic.Testing.Assertions; @@ -14,64 +13,82 @@ public class ProfilerAgentMethodCallCacheTests [Test] public void GetMethodCacheFuncShouldReturnAFuncAsAnObject() { - var methodReference = ProfilerAgentMethodCallCache.GetMethodCacheFunc(); - Assert.IsAssignableFrom(typeof(Func), methodReference); + var methodReference = ProfilerAgentMethodCallCache.GetInvokerFromCache(); + Assert.IsAssignableFrom(typeof(Func), methodReference); } [Test] - public void ShouldGetMethodWithNoParameters() + public void ShouldInvokeMethodWithNoParameters() { - var cacheGetter = (Func)ProfilerAgentMethodCallCache.GetMethodCacheFunc(); + var invoker = (Func)ProfilerAgentMethodCallCache.GetInvokerFromCache(); var typeName = typeof(TestingClass).AssemblyQualifiedName; var methodName = nameof(TestingClass.MethodWithNoParams); var cacheKey = string.Concat(typeName, "|", methodName); - var methodInfoFromCache = cacheGetter.Invoke(cacheKey, typeName, methodName, null); + var result1 = invoker.Invoke(cacheKey, typeName, methodName, null, typeof(string), null); + var result2 = invoker.Invoke(cacheKey, typeName, methodName, Array.Empty(), typeof(string), null); NrAssert.Multiple( - () => Assert.IsNotNull(methodInfoFromCache), - () => Assert.AreEqual(methodName, methodInfoFromCache.Invoke(null, null)) + () => Assert.IsNotNull(result1), + () => Assert.AreEqual(methodName, result1), + () => Assert.AreEqual(result1, result2) ); } [Test] - public void ShouldGetMethodWithParameters() + public void ShouldInvokeMethodWithParameters() { - var cacheGetter = (Func)ProfilerAgentMethodCallCache.GetMethodCacheFunc(); + var invoker = (Func)ProfilerAgentMethodCallCache.GetInvokerFromCache(); var typeName = typeof(TestingClass).AssemblyQualifiedName; var methodName = nameof(TestingClass.MethodWithParams); var cacheKey = string.Concat(typeName, "|", methodName); + var parameterValue = "param_value"; - var methodInfoFromCache = cacheGetter.Invoke(cacheKey, typeName, methodName, new Type[] { typeof(string) }); + var result = invoker.Invoke(cacheKey, typeName, methodName, new Type[] { typeof(string) }, typeof(string), new object[] { parameterValue }); - var expectedMethodResult = $"{methodName} + param_value"; + var expectedMethodResult = $"{methodName} + {parameterValue}"; NrAssert.Multiple( - () => Assert.IsNotNull(methodInfoFromCache), - () => Assert.AreEqual(expectedMethodResult, methodInfoFromCache.Invoke(null, new object[] { "param_value" })) + () => Assert.IsNotNull(result), + () => Assert.AreEqual(expectedMethodResult, result) ); } [Test] - public void ShouldGetWrongMethodIfCacheKeyNotUniqueEnough() + public void ShouldInvokeWrongMethodIfCacheKeyNotUniqueEnough() { - var cacheGetter = (Func)ProfilerAgentMethodCallCache.GetMethodCacheFunc(); + var invoker = (Func)ProfilerAgentMethodCallCache.GetInvokerFromCache(); var typeName = typeof(TestingClass).AssemblyQualifiedName; var methodName = nameof(TestingClass.MethodWithOverload); var cacheKey = string.Concat(typeName, "|", methodName); - var overloadWith1Param = cacheGetter.Invoke(cacheKey, typeName, methodName, new Type[] { typeof(string) }); - var overloadWith2Params = cacheGetter.Invoke(cacheKey, typeName, methodName, new Type[] { typeof(string), typeof(string) }); + var resultWithStringParam = invoker.Invoke(cacheKey, typeName, methodName, new Type[] { typeof(string) }, typeof(string), new object[] { "param" }); NrAssert.Multiple( - () => Assert.IsNotNull(overloadWith1Param), - () => Assert.IsNotNull(overloadWith2Params), - () => Assert.AreEqual(overloadWith2Params, overloadWith1Param), - // The call to the 2 parameter overload should fail because the 1 parameter overload was returned from the cache - () => Assert.Throws(typeof(TargetParameterCountException), () => overloadWith2Params.Invoke(null, new object[] { "param_1", "param_2" })) + () => Assert.IsNotNull(resultWithStringParam), + // The call to the bool parameter overload should fail because the string parameter overload was returned from the cache + () => Assert.Throws(typeof(InvalidCastException), () => invoker.Invoke(cacheKey, typeName, methodName, new Type[] { typeof(bool) }, typeof(string), new object[] { true })) + ); + } + + [Test] + public void ShouldHandleMethodsWithVoidReturnType() + { + var invoker = (Func)ProfilerAgentMethodCallCache.GetInvokerFromCache(); + + var typeName = typeof(TestingClass).AssemblyQualifiedName; + + var resultNoParam = invoker.Invoke(string.Concat(typeName, "|", nameof(TestingClass.ActionWithNoParams)), typeName, nameof(TestingClass.ActionWithNoParams), null, null, null); + var resultWithParam = invoker.Invoke(string.Concat(typeName, "|", nameof(TestingClass.ActionWithParam)), typeName, nameof(TestingClass.ActionWithParam), new Type[] { typeof(int) }, null, new object[] { 5 }); + + NrAssert.Multiple( + () => Assert.IsNull(resultNoParam), + () => Assert.IsNull(resultWithParam), + () => Assert.AreEqual(1, TestingClass.ActionWithNoParamsCallCount), + () => Assert.AreEqual(5, TestingClass.ActionWithParamLatestValue) ); } @@ -92,9 +109,21 @@ public static string MethodWithOverload(string param) return $"{nameof(MethodWithOverload)} + {param}"; } - public static string MethodWithOverload(string param1, string param2) + public static string MethodWithOverload(bool param1) + { + return $"{nameof(MethodWithOverload)} + {param1}"; + } + + public static int ActionWithNoParamsCallCount = 0; + public static void ActionWithNoParams() + { + ActionWithNoParamsCallCount++; + } + + public static int ActionWithParamLatestValue = 0; + public static void ActionWithParam(int value) { - return $"{nameof(MethodWithOverload)} + {param1} + {param2}"; + ActionWithParamLatestValue = value; } } }