vendredi 19 avril 2019

Caching MethodInfo doesn't improve performance?

We've recently hit some performance issues by excessive/improper use of some reflection API's in our application. I've solved one of the issues which had to do with GetCustomAttribute<T> by caching some stuff. But I'm struggling with improving the performance of another part of our code which uses MethodInfo.Invoke(...).

To explain the whole situation here's some stuff for context. We have a generic interface we use for all our mappers:

public interface IMapper<in TIn, out TOut>
{
    TOut Map(TIn instance);
}

And we have lots of implementations of this interface, like this example:

public class SomeEntityMapper : IMapper<SomeObject, SomeEntity>
{
    public SomeEntity Map(SomeObject instance)
    {
        //Create an instance of SomeEntity and map stuff from the SomeObject instance
    }
}

All the objects that get mapped share an interface:

[ACustomAttribute("somestring")]
public class ConcreteEntity : IEntity
{
    // Properties
}

We have a class that has to find the correct mapper, get the instance from IServiceProvider and invoke the Map method and return the result:

public class MapHelper : IMapHelper
{
    private readonly IServiceProvider serviceProvider;
    private static readonly IEnumerable<Type> types = AppDomain.CurrentDomain.GetAssemblies().SelectMany(a => a.GetTypes()).ToList();
    private static readonly ConcurrentDictionary<string, Type> entityTypeCache = new ConcurrentDictionary<string, Type>();
    private static readonly ConcurrentDictionary<Type, MethodInfo> methodInfoCache = new ConcurrentDictionary<Type, MethodInfo>();

    public TridionComponentHelper(IServiceProvider serviceProvider)
    {
        this.serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
    }

    public IEntity GetEntity(SomeObject instance)
    {
        if (instance == null) throw new ArgumentNullException(nameof(instance));

        Type entityType = GetEntityTypeByName(instance.Name);

        Type mapperType = types.Single(t => t.IsClass && t.Name == $"{entityType.Name}Mapper");

        Type mapperInterfaceType = mapperType.GetInterfaces().Single(i => i.Name.Contains("IMapper"));

        var mapperInstance = serviceProvider.GetRequiredService(mapperInterfaceType);
        MethodInfo mapMethod = GetMethodInfo(mapperInstance.GetType());
        var entity = mapMethod.Invoke(mapperInstance, new[] { instance } ) as IEntity;

        return entity;
    }

    private static Type GetEntityTypeByName(string name)
    {
        if (entityTypeCache.TryGetValue(name, out Type cachedType))
        {
            return cachedType;
        }
        else
        {
            var type = types.Single(t => t.IsClass && t.GetCustomAttribute<ACustomAttribute>()?.Name == name);
            entityTypeCache.TryAdd(name, type);
            return type;
        }
    }

    private static MethodInfo GetMethodInfo(Type type)
    {
        if (methodInfoCache.TryGetValue(type, out MethodInfo cachedMethodInfo))
        {
            return cachedMethodInfo;
        }
        else
        {
            var methodInfo = type.GetMethod("Map");
            methodInfoCache.TryAdd(type, methodInfo);
            return methodInfo;
        }
    }
}

We use this MapHelper in another class where we loop through a list of SomeObject and return the mapped entities. I do not know beforehand what the type of the mapper is supposed to be or else I could've used serviceProvider.GetRequiredService<T>() and just called Map directly. So instead I was using the following code at first to retrieve the right instance of the mapper (registered by the Microsoft.Extensions.DependencyInjection DI framework) and changed to the code above after I noticed it caused some performance issues:

///GetEntity method
...
var mapperInstance = serviceProvider.GetRequiredService(mapperInterfaceType);
MethodInfo mapMethod = mapperInstance.GetType().GetMethod("Map");
var entity = mapMethod.Invoke(mapperInstance, new[] { instance } ) as IEntity;
...

As you can see in the full implementation of MapHelper, I cache all the MethodInfo's I retrieve in a ConcurrentDictionary so any subsequent calls can reuse them. But I haven't noticed any (significant) difference in performance between the code caching the MethodInfo's and the one that doesn't. Am I doing something wrong?





Aucun commentaire:

Enregistrer un commentaire