Tech Musings Just another tip's, tricks, and how-to's blog

31May/115

Shape Method Caching

So we're preparing to launch our Orchard site, and one of the final steps is tuning the performance of the site. The Orchard team realizes that caching is a huge part of a CMS, but has left the framework open in such a way that a caching module should be relatively simple to write, so we did just that.

The way we use Orchard may be a little different than most, in that our site is almost entirely composed of little widgets of different pieces of content, but we don't use widgets at all because we found they did not suit our needs properly. Instead we use [Shape] methods to define a shape name, and then a corresponding view to go behind, as so:

        [Shape]
        public IHtmlString SidebarAd(dynamic Display) {
            var ads = ContentManager.Query<HouseAdPart>(VersionOptions.Latest).List().ToList();
            return Display.SidebarAdView(Ads: ads);

        }

This allows us to do Display.SidebarAd() from anywhere else on the site to plop this particular shape in wherever, and we maintain our separation of concerns by using a shape method as a sort of controller, with a view behind it called SidebarAdView (which is a razor view).

As I started investigating how we might go about caching, I decided that we would get about 90% cache coverage on our site by just being able to add an attribute to these shape methods designating that they should be cached. (The other 10% are just razor views that don't have a shape method in front of them, but will likely be refactored to have one soon).

To start out, I basically duplicated the structure of the Orchard team's ShapeAttribute, and called it CacheAttribute:

public class CacheAttribute : Attribute
    {
        public CacheAttribute() {
            Duration = 60 * 5;
        }
        public CacheAttribute(int duration)
        {
            Duration = duration;
        }

        public int Duration
        {
            get;
            private set;
        }
    }
public class CacheAttributeOccurence {

        public CacheAttributeOccurence(CacheAttribute cacheAttribute, MethodInfo methodInfo, IComponentRegistration registration) {
            CacheAttribute = cacheAttribute;
            MethodInfo = methodInfo;
            Registration = registration;
        }

        public CacheAttribute CacheAttribute { get; private set; }
        public MethodInfo MethodInfo { get; private set; }
        public IComponentRegistration Registration { get; private set; }

    }
public class CacheAttributeBindingModule : Module {
        readonly List<CacheAttributeOccurence> _occurences = new List<CacheAttributeOccurence>();

        protected override void Load(ContainerBuilder builder) {
            builder.RegisterInstance(_occurences).As<IEnumerable<CacheAttributeOccurence>>();
        }
        protected override void AttachToComponentRegistration(IComponentRegistry componentRegistry, IComponentRegistration registration) {
            var occurences = registration.Activator.LimitType.GetMethods()
                .SelectMany(mi => mi.GetCustomAttributes(typeof (CacheAttribute), false).OfType<CacheAttribute>()
                    .Select(ca => new CacheAttributeOccurence(ca,mi,registration))
                ).ToArray();
            if (occurences.Any())
                _occurences.AddRange(occurences);
        }
    }

So now I can implement a binding strategy that enumerates each CacheAttributeOccurence and configures caching for each shape method. I started out by just configuring a standard IShapeTableProvider like so:

public class CacheAttributeBindingStrategy : IShapeTableProvider
    {
        private readonly IEnumerable<CacheAttributeOccurence> _occurrences;
        private readonly Cache<string, IHtmlString> _cache;

        private readonly IWorkContextAccessor _workContextAccessor;
        private readonly IHttpContextAccessor _httpContextAccessor;
        public CacheAttributeBindingStrategy(IEnumerable<CacheAttributeOccurence> occurences, ICacheManager cache, IWorkContextAccessor workContextAccessor, IHttpContextAccessor httpContextAccessor)
        {
            _workContextAccessor = workContextAccessor;
            _httpContextAccessor = httpContextAccessor;
            _occurrences = occurences;

            _cache = cache.GetCache<string, IHtmlString>() as Cache<string, IHtmlString>;
        }

        public void Discover(ShapeTableBuilder builder)
        {
            foreach (var occurence in _occurrences)
            {
                var shape = occurence.MethodInfo.GetCustomAttributes(typeof(ShapeAttribute), false).OfType<ShapeAttribute>().FirstOrDefault();
                if (shape != null)
                {
                    var shapeType = shape.ShapeType ?? occurence.MethodInfo.Name;
                    CacheAttributeOccurence occurence1 = occurence;
                    builder.Describe(shapeType)
                        .OnDisplaying(displaying =>
                        {

                        })
                        .OnDisplayed(displayed =>
                        {

                        });
                }
            }
        }

I had read a comment in the forums where Bertrand explained how caching could be just as simple as implementing OnDisplaying and OnDisplayed for a shape and populating the ChildContent if a shape exists in cache already.
The trouble was, how could I construct a key for my cache that can be autogenerated based on the parameter names and values for a shape method execution context. As it turns out, my answer again lay in the implementation of the ShapeAttribute. So I grabbed a few methods from their implementation, modified them a bit to fit my needs, and implemented them into my CacheAttributeBindingStrategy:

public static bool TypeImplementsInterface(Type type, Type interfaceType)
        {
            string interfaceFullName = interfaceType.FullName;
            return type.GetInterface(interfaceFullName) != null;
        }
        private static KeyValuePair<string, string> BindParameter(dynamic shape, ParameterInfo parameter)
        {
            if (parameter.Name == "Shape") return new KeyValuePair<string, string>(null, null);

            if (parameter.Name == "Display") return new KeyValuePair<string, string>(null, null);

            if (parameter.Name == "Output" && parameter.ParameterType == typeof(TextWriter))
                return new KeyValuePair<string, string>(null, null);

            // meh--
            if (parameter.Name == "Html")
            {
                return new KeyValuePair<string, string>(null, null);
            }

            var getter = _getters.GetOrAdd(parameter.Name, n =>
                CallSite<Func<CallSite, object, object>>.Create(
                Microsoft.CSharp.RuntimeBinder.Binder.GetMember(
                CSharpBinderFlags.None, n, null, new[] { CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null) })));

            var result = getter.Target(getter, shape);

            if (result == null)
                return new KeyValuePair<string, string>(null, null);

            switch (Type.GetTypeCode(parameter.ParameterType))
            {
                case TypeCode.Boolean:
                case TypeCode.String:
                case TypeCode.Char:
                case TypeCode.DateTime:
                case TypeCode.Int16:
                case TypeCode.Int32:
                case TypeCode.Int64:
                case TypeCode.UInt16:
                case TypeCode.UInt32:
                case TypeCode.UInt64:
                case TypeCode.Single:
                case TypeCode.Double:
                case TypeCode.Decimal:
                case TypeCode.SByte:
                case TypeCode.Byte:
                    var converter = _converters.GetOrAdd(parameter.ParameterType, CompileConverter);
                    var argument = converter.Invoke(result);
                    return new KeyValuePair<string, string>(parameter.Name, argument.ToString());
                    break;
                case TypeCode.Object:
                    if (TypeImplementsInterface(parameter.ParameterType, typeof(IContent)))
                    {
                        var converter2 = _converters.GetOrAdd(parameter.ParameterType, CompileConverter);
                        var argument2 = converter2.Invoke(result) as IContent;
                        if (argument2 != null)
                            return new KeyValuePair<string, string>(parameter.Name, argument2.Id.ToString());
                        else
                            return new KeyValuePair<string, string>(parameter.Name, "NULL");

                    }
                    return new KeyValuePair<string, string>(null, null);
                case TypeCode.DBNull:
                case TypeCode.Empty:
                default:
                    return new KeyValuePair<string, string>(null, null);
            }

            return new KeyValuePair<string, string>(null, null);

        }
        private static string ConstructCacheKey(string type, Shape shape, CacheContext cacheContext)
        {
            var arguments = cacheContext.MethodInfo.GetParameters()
                .Select(parameter => BindParameter(shape, parameter));

            var strings = new List<string>(arguments.Count() * 2);
            foreach (var p in arguments)
            {
                if (p.Key == null)
                    continue;

                strings.Add(p.Key);
                strings.Add(p.Value);
            }
            return type + string.Join("_", strings);
        }
        static readonly ConcurrentDictionary<string, CallSite<Func<CallSite, object, object>>> _getters =
            new ConcurrentDictionary<string, CallSite<Func<CallSite, object, object>>>();

        static readonly ConcurrentDictionary<Type, Func<object, object>> _converters =
            new ConcurrentDictionary<Type, Func<object, object>>();
        static Func<object, object> CompileConverter(Type targetType)
        {
            var valueParameter = Expression.Parameter(typeof(object), "value");

            return Expression.Lambda<Func<object, object>>(
                Expression.Convert(
                    Expression.Dynamic(
                        Microsoft.CSharp.RuntimeBinder.Binder.Convert(CSharpBinderFlags.ConvertExplicit, targetType, null),
                        targetType,
                        valueParameter),
                    typeof(object)),
                valueParameter).Compile();
        }

While I didn't have access to the context they used in the ShapeAttribute strategy, I found I could just as easily bind to the shape I was given to get the parameters for the shape method. Now to implement the actual caching code:

public void Discover(ShapeTableBuilder builder)
        {
            foreach (var occurence in _occurrences)
            {
                var shape = occurence.MethodInfo.GetCustomAttributes(typeof(ShapeAttribute), false).OfType<ShapeAttribute>().FirstOrDefault();
                if (shape != null)
                {
                    var shapeType = shape.ShapeType ?? occurence.MethodInfo.Name;
                    CacheAttributeOccurence occurence1 = occurence;
                    builder.Describe(shapeType)
                        .OnDisplaying(displaying =>
                        {
                            var cacheContext = new CacheContext { MethodInfo = occurence1.MethodInfo, CacheHit = false };
                            displaying.Shape._cacheContext = cacheContext;
                            string key = ConstructCacheKey(shapeType, displaying.Shape, cacheContext).ToString();
                            if (!string.IsNullOrWhiteSpace(key)) {
                                var result = _cache.TryGet(key);
                                if (result != null)
                                {
                                    displaying.ChildContent = result;
                                    cacheContext.CacheHit = true;
                                }
                            }
                        })
                        .OnDisplayed(displayed =>
                        {
                            var cacheContext = displayed.Shape._cacheContext as CacheContext;
                            if (cacheContext == null)
                                return;
                            if (cacheContext.CacheHit)
                            {
                                return;
                            }
                            string key = ConstructCacheKey(shapeType, displayed.Shape, cacheContext).ToString();
                            if (!string.IsNullOrWhiteSpace(key))
                            {

                                _cache.Get(key, acquire =>
                                {
                                    acquire.Monitor(Clock.When(TimeSpan.FromSeconds(occurence1.CacheAttribute.Duration)));
                                    return displayed.ChildContent;
                                });

                            }
                        });
                }
            }
        }

You'll notice in my code that I'm using a method that doesn't exist in the default cache implementation: .TryGet(). The reason being is that .Get() forces you to have a way to populate the cache at execution time if the item doesn't exist in cache, which I'm unable to do so because the shape hasn't been displayed yet. Instead I had to suppress the default Orchard CacheHolder and construct my own:

 [OrchardSuppressDependency("Orchard.Caching.DefaultCacheHolder")]
    public class CacheHolder : ICacheHolder
    {
        private readonly ConcurrentDictionary<CacheKey, object> _caches = new ConcurrentDictionary<CacheKey, object>();
        class CacheKey : Tuple<Type, Type, Type>
        {
            public CacheKey(Type component, Type key, Type result)
                : base(component, key, result)
            {
            }
        }
        public ICache<TKey, TResult> GetCache<TKey, TResult>(Type component) {
            var cacheKey = new CacheKey(component, typeof(TKey), typeof(TResult));
            var result = _caches.GetOrAdd(cacheKey, k => new ACLJ.Caching.Cache<TKey, TResult>());
            return (Cache<TKey, TResult>)result;
        }
    }
public class Cache<TKey, TResult> : ICache<TKey, TResult>
    {
        private readonly ConcurrentDictionary<TKey, CacheEntry> _entries;

        public Cache()
        {
            _entries = new ConcurrentDictionary<TKey, CacheEntry>();
        }
        public TResult TryGet(TKey key) {
            CacheEntry value = null;
            var entry = _entries.TryGetValue(key, out value);
            if (value == null)
                return default(TResult);

            return value.Result;
        }
        public TResult Get(TKey key, Func<AcquireContext<TKey>, TResult> acquire)
        {
            var entry = _entries.AddOrUpdate(key,
                // "Add" lambda
                k => CreateEntry(k, acquire),
                // "Update" lamdba
                (k, currentEntry) => (currentEntry.Tokens.All(t => t.IsCurrent) ? currentEntry : CreateEntry(k, acquire)));

            // Bubble up volatile tokens to parent context
            if (CacheAquireContext.ThreadInstance != null)
            {
                foreach (var token in entry.Tokens)
                    CacheAquireContext.ThreadInstance.Monitor(token);
            }

            return entry.Result;
        }

        private static CacheEntry CreateEntry(TKey k, Func<AcquireContext<TKey>, TResult> acquire)
        {
            var entry = new CacheEntry { Tokens = new List<IVolatileToken>() };
            var context = new AcquireContext<TKey>(k, volatileItem => entry.Tokens.Add(volatileItem));

            IAcquireContext parentContext = null;
            try
            {
                // Push context
                parentContext = CacheAquireContext.ThreadInstance;
                CacheAquireContext.ThreadInstance = context;

                entry.Result = acquire(context);
            }
            finally
            {
                // Pop context
                CacheAquireContext.ThreadInstance = parentContext;
            }
            return entry;
        }

        private class CacheEntry
        {
            public TResult Result { get; set; }
            public IList<IVolatileToken> Tokens { get; set; }
        }
    }

    /// <summary>
    /// Keep track of nested caches contexts on a given thread
    /// </summary>
    internal static class CacheAquireContext
    {
        [ThreadStatic]
        public static IAcquireContext ThreadInstance;
    }

Voila. Now I can just apply my CacheAttribute to any shape methods I want to cache like so:


        [Shape, Cache]
        public IHtmlString ContentImage(dynamic Display, ContentItem item, string suffix) {
            var image = item.As<ImageAttachmentPart>();

            if (image != null && image.Image != null)
            {
                var imageUrl = ImageService.GetUrl(image.Image.ContentItem.As<ImagePart>(), suffix);
                var dim = ImageService.GetDimensions().Where(d => d.Suffix == suffix).First();
                return Display.ImageView(ImageUrl: imageUrl, Width: dim.Width, Height: dim.Height);
            }

            return T("");
        }

And the result of this shape method will be stored in the cache based on the parameters specified to the shape method.

My next step is to make the parameter analysis when constructing the cache key a little bit more foolproof, as it won't handle any complex types. I'd also like to add some options to the CacheAttribute that allow it to specify which parameters should be used to construct the cache key.

I'm also trying to think of how to easily cache a shape that is defined only in a razor view, so if any one has any ideas on how to go about that, hit me up. If I can solve those problems, I'll release the module to the community.

Comments (5) Trackbacks (0)
  1. Cool stuff. An idea for Razor shapes: let the template tag itself, something like @{Model.Cache = true;}. Then, you can catch that after display and get it into the cache.

  2. That’s a great idea. I could also specify ‘parameters’ within the template to construct the cache key. But doing this I would have to piggy back on to the razor shape scraping to register events for each razor shape, correct?

  3. Would really like to see how the Cache module would behave on your site. http://orchardproject.net/gallery/List/Modules/Orchard.Module.Contrib.Cache

  4. Thanks Sebastien, I’ll check it out. I suspect we may have issues with it due to us using a non-standard way of member authentication (not the Orchard member system, because we have several million members).

  5. Nice post Chris.
    I am big fan of PostSharp and that’s why I would thing of an implementation of caching mechanism using aspects. It might break the easy reconfiguration since it will be hard coded. One place where it can help you, is the discovery of method call parameters – see this ppost http://www.sharpcrafters.com/blog/post/solid-caching.aspx.
    At the same time you can avoid reflection when discovering all the method decorated with the Cache attribute and bring simplicity into your code.


Leave a comment

(required)

No trackbacks yet.