Caching Driver result good or bad thing?

Topics: General, Troubleshooting, Writing modules
Nov 8, 2011 at 11:09 AM

Is it ok to cache the driver result? of is it better to only cache the service results?

Nov 8, 2011 at 11:21 AM

Can you provide some example code to show exactly what you're doing? Depending on how you're going about this, it could actually be detrimental to performance. Driver results are factory methods that will not necessarily execute, so as long as your caching is performed within the driver result then there is no problem.

Nov 8, 2011 at 11:48 AM

Just to give some further information on this: if you want to cache templates, you probably don't want to be doing it in the driver. The driver result objects are fairly complex and include dynamic (Clay) objects which probably won't cache very well, and at the very least won't be the optimal performance strategy, at worst could blow things up or start causing very strange errors.

I've experimented with some caching of template regions in my modules and there are decent ways to do it, but I'd recommend waiting for the next version of Sebastien's Cache module which will handle this in a robust and consistent way.

Jan 16, 2012 at 8:10 AM
Edited Jan 16, 2012 at 8:10 AM

When can we expect the next version of Sebastien's cache module? I have a Part that I want to render to HTML once (as its creation is expensive), and cache that content until it changes. I was thinking I would make my own DriverResult as a wrapper to accomplish this. I haven't looked into it too much yet (was surfing forums to get ideas) so maybe I can't actually do what I was intending to do, but...

Coordinator
Jan 16, 2012 at 8:12 AM

You don't need Sébastien's cache module for that. You can use the displaying and displayed events on shapes for that already.

Jan 16, 2012 at 8:35 AM
Edited Jan 16, 2012 at 8:40 AM

Bertrand, I don't believe you have access to the HTML during displaying or displayed shape events; just the dynamic shape, which only gets rendered to HTML in the view (using an IDisplayHelperFactory which requires a ViewContext and an IViewDataContainer to instance an actual helper...)

Jan 16, 2012 at 8:44 AM

The idea was to make a mechanism similar to how you'd do caching with MVC ActionResults. Basically, the driver Display method would therefore become a no-op if the data was cached. I've used the displaying/displayed events to modify shape content before, but I didn't think those events allowed for short circuiting displaying entirely like this? I guess I maybe could replace the context.ChildContent to the cached HTML, but how would I prevent the shape from being created entirely (and the display built)? (The shape is a complex tree which is expensive to create and render, so I want to avoid both steps entirely after the first iteration.) I'm probably missing some fundamental point about the event hooks...

Jan 16, 2012 at 11:12 AM
Edited Jan 16, 2012 at 11:13 AM

Actually I already did a similar thing in my Mechanics module, you can cache a Socket shape (which renders the list of connected items on a particular relationship' a sometimes quite slow operation, especially if you have recursive levels of relationships, so caching became very desirable here). It required a specific implementation to handle a Socket, however, and while I've been thinking about how to handle this in a generic way for all shapes/drivers it's very difficult, due to various internal details about drivers, shapes, and DriverResult.

One big problem is defining cache conditions. The only piece of code that can really understand the unique properties of a particular shape to determine whether you have a cached version already, is the driver itself. So any generic implementation would probably require rewriting any driver that you wished to support caching (or providing an injectable interface to describe uniqueness for particular shapes and parts).

There are many other things that make it tricky, and as I said and you also were I think aware, you can't at all easily get the HTML until the view is rendered (although, once you have the HTML already cached, you could inject this in the shape events; the problem is still prevent the driver code from being executed).

The only way to inject new behaviour into how driver results are handled is to bind a new placement delegate (this is the FindPlacement property on ShapeDisplayContext), it's extremely limited what you can do there, but you can prevent a driver result executed by returning the null placement "-". The problem here is that some of the display logic is actually performed in DriverResult, which is a simple class and not a dependency, and there is no way to override this behaviour. DriverResult is created from a private method of the ContentDriver base class, so again it can't be changed because drivers all inherit this behaviour, and this is how the vast majority of parts generate their shape factories. I've spent quite some time looking into the implementation of drivers, content display, etc. and understand it pretty deeply. We need changes in Orchard Core to make this more flexible and make situations like this even possible, and some aspects of it much easier.

This will have to wait until I've got a few other projects cleared up; but perhaps after 1.4 is out I'll figure out some changes which would add a load more extensibility into content display without either breaking anything or affecting performance (and the possibility of caching templates emitted from drivers would be a clear performance helper in many situations). And then hope those changes get accepted :)

Jan 16, 2012 at 11:44 AM

Randompete, thanks for the nice explanation. I'll take a look at your Mechanics module (http://gallery.orchardproject.net/List/Modules/Orchard.Module.Downplay.Mechanics, http://scienceproject.codeplex.com/wikipage?title=Mechanics) to see how you did it. I just spent the last bit digging into Orchard more, only to realize that elegantly caching the rendered HTML wasn't quite so simple. 

As an alternative to the shape events idea (I was trying to find a way to proactively render the HTML and cache it (before actual first page-load), so I was poking around), in Orchard.Tests.DisplayManagement.SubsystemTests I found a test case (below) that looked like I could possibly mimic part of to force render my shape within the driver itself and then cache that HTML, but it was feeling a bit like hammering in a nail by ramming it with my car (i.e., maybe effective, but not very elegant).

public void RenderingSomething() {
    dynamic displayHelperFactory = _container.Resolve<IDisplayHelperFactory>().CreateHelper(new ViewContext(), null);
    dynamic shapeHelperFactory = _container.Resolve<IShapeFactory>();

    var result1 = displayHelperFactory.Something();
    var result2 = ((DisplayHelper)displayHelperFactory).ShapeExecute((Shape)shapeHelperFactory.Pager());

    displayHelperFactory(shapeHelperFactory.Pager());

    Assert.That(result1.ToString(), Is.EqualTo("<br/>"));
    Assert.That(result2.ToString(), Is.EqualTo("<div>hello</div>"));
}

Jan 16, 2012 at 11:59 AM

I *think* that gallery version has the caching behaviour, but I can't remember what file it was in - the latest code on http://scienceproject.codeplex.com has changed things around quite a lot and the caching behaviour is triggered in Drivers\Models\SocketsRootDriver. You can also trigger it from Placement in the latest version (e.g. <Place Socket-RelatedContent="Content:1;cache=true"/>)

The way it works is creating a shape called "SocketCache" which, if the rendered result is not available, will use a Capture call to capture the HTML output and store it in the cache result which will then be available for next time. All the serious work is performed within the _cacheManager.Get call, so it'll only get executed when the cache entry isn't present.

This worked against all belief ;) Unfortunately the cache key is hard-coded so it's per display type as well as request Url, because this is what I needed for the specific scenario (navigation menus) where I was having issues. I've yet to implement a more flexible cache key because I haven't needed to use it in any other context.

The method you've shown might work for those simple test shapes, but it'll fail for a lot of real-world scenarios because you need a genuine ViewContext and IViewDataContainer for many things MVC to work properly or at all, hence why they're there. The problem is that the ViewContext isn't available until the view engine creates one, and it's a pretty complex process doing so, which you don't want to repeat on the fly.

Jan 16, 2012 at 12:08 PM

A further thought - you could create a generic "CachingWrapper" shape which would perform a similar job to my SocketCache. I experimented with a similar thing looking at how to implement some Ajax functionality and it worked well.

Jan 16, 2012 at 12:08 PM
randompete wrote:

I *think* that gallery version has the caching behaviour, but I can't remember what file it was in - the latest code on http://scienceproject.codeplex.com has changed things around quite a lot and the caching behaviour is triggered in Drivers\Models\SocketsRootDriver. You can also trigger it from Placement in the latest version (e.g. <Place Socket-RelatedContent="Content:1;cache=true"/>)

The way it works is creating a shape called "SocketCache" which, if the rendered result is not available, will use a Capture call to capture the HTML output and store it in the cache result which will then be available for next time. All the serious work is performed within the _cacheManager.Get call, so it'll only get executed when the cache entry isn't present.

This worked against all belief ;) Unfortunately the cache key is hard-coded so it's per display type as well as request Url, because this is what I needed for the specific scenario (navigation menus) where I was having issues. I've yet to implement a more flexible cache key because I haven't needed to use it in anyother context.

Actually, my exact scenario is multi-tiered navigation menus. Thanks for the pointers; I'll look directly at the code on codeplex.

randompete wrote:

The method you've shown might work for those simple test shapes, but it'll fail for a lot of real-world scenarios because you need a genuine ViewContext and IViewDataContainer for many things MVC to work properly or at all, hence why they're there. The problem is that the ViewContext isn't available until the view engine creates one, and it's a pretty complex process doing so, which you don't want to repeat on the fly.

Yeah, I was afraid of that, and that it might require additional black magic to function properly.

Developer
Jan 16, 2012 at 1:03 PM

@Pete: Actually you can access the shape's html output in OnDisplayed():

    public class ShapeTests : IShapeTableProvider
    {
        public void Discover(ShapeTableBuilder builder)
        {
            builder.Describe("Shape_Name_Here")
                .OnDisplayed(context =>
                    {
                        var html = context.ShapeMetadata.ChildContent.ToString();
                    });
        }
    }

Jan 16, 2012 at 1:34 PM
Edited Jan 16, 2012 at 1:34 PM

@Piedone - aha, that makes things slightly easier, although it's still only part of the problem - how you actually decide which shapes to cache, and how you prevent drivers and other shape CPU-intensive events from running, are different matters.

Developer
Jan 16, 2012 at 2:18 PM

Yeah, that was only for the html capturing part.

Coordinator
Jan 16, 2012 at 8:39 PM

Yes, displaying and displayed were *designed* to allow for this scenario. Deciding which shapes to cache really isn't a problem: the shape table provider can use any criteria it wants to determine that.

Jan 17, 2012 at 12:14 AM

It can; but you still can't stop the driver code from running in those events, so you only get a limited amount of optimisation by not rendering the templates. With the current framework, the ContentDriver itself has to be aware of the caching mechanism, and poll the ICacheManager to see whether the relevant shape and parameters are already cached.

But ... this does lead me to a reasonably straightforward idea about providing an abstract CacheAwareContentDriver<T> and CachedDriverResult which between them could perform some of the legwork (and a ContentHandler or IShapeTableEvents to check for a dynamic property on the shape itself and capture the HTML if required). So, it could be functional (if a bit inelegant!) for easily converting existing drivers to cached shapes.

Jan 17, 2012 at 12:44 AM
Edited Jan 17, 2012 at 1:12 AM
bertrandleroy wrote:

Yes, displaying and displayed were *designed* to allow for this scenario. Deciding which shapes to cache really isn't a problem: the shape table provider can use any criteria it wants to determine that.

 

@bertrandleroy, could you explain in a few more words how to do this properly? I am seeing two issues in particular:

  • Orchard's ICacheManager only has a single Get() method that requires a closure for  getting the key's content if it hasn't been loaded. I was assuming that the scenario entailed setting context.ChildContent to the HTML in Displaying()  and if it isn't cached yet, fetching it from context.ShapeMetadata.ChildContent and adding it to the cache in Displayed(); if you try to do both in Displayed() I think the content has already been generated from the shape tree so the cache does not help there. You could probably access the cache elsewhere and add the cached object to the shape or its metadata, but I can't think that that was the designed approach. 
  • One major part of caching the rendered output is also to prevent shape generation entirely. How do you properly do this using Displaying()/Displayed()? Is there a way to prevent the driver from building the shape without it itself being aware of the caching mechanism?
Jan 17, 2012 at 1:41 AM

@dainkaplan this is exactly what I'm saying; you need code in the ContentDriver itself as well as in Displayed. It won't help much at all with non-content shapes (although just reducing # of rendered templates could be beneficial for complex shape graphs).

In the ContentDriver you need to a) check for existence of the cache and b) emit a specialised shape to display the cached result instead of the normal driver result. And in the Displayed even you can store the rendered HTML to cache if needed.

Jan 17, 2012 at 1:56 AM

@randompete Yeah, I think you and I are on the same page here. I was simply hoping that bertrandleroy had something up his sleeve he wasn't yet telling us. I'm in the process of writing up a simplification of what I found in your Mechanics module for explaining how to do what you did.

Jan 17, 2012 at 2:41 AM
Edited Jan 17, 2012 at 2:46 AM

Here's a basic idea for an abstract CachedContentPartDriver. Needs a little work but it's a start. Had to reimplement everything from ContentShapeResult in the new CachedContentShapeResult. The "cache key factory" will only be executed if placement actually matches, and then the driver factory delegate only runs if the cache isn't found. Finally in shape events you can check for the existence of Shape.CacheMetadata and store the HTML. You'll also need a CachedShape.cshtml to display the child content when the cache exists. 

 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Orchard.ContentManagement.Drivers;
using Orchard.ContentManagement;
using Orchard.ContentManagement.Handlers;
using Orchard.Caching;
using Orchard.DisplayManagement.Shapes;

namespace Downplay.Origami.Drivers {
    public abstract class CachedContentPartDriver<TContent,TCacheKey>
: ContentPartDriver<TContent> where TContent : ContentPart, new() {
        private readonly ICacheManager _cacheManager;

        public CachedContentPartDriver(ICacheManager cacheManager) {
            _cacheManager = cacheManager;
        }

        public CachedContentShapeResult<TCacheKey> Cached(string shapeType, 
Func<TCacheKey> cacheKey, Func<dynamic> factory) { return new CachedContentShapeResult<TCacheKey>(shapeType, Prefix,
(context) => factory(), cacheKey, (key) =>
_cacheManager.Get<TCacheKey, CachedShape>(key, (ctx) => { // TODO: Support monitors return new CachedShape(); })); } } public class CachedShape { public string Html { get; set; } } public class CachedContentShapeResult<TCacheKey>
: ContentShapeResult { private string _defaultLocation; private string _differentiator; private readonly string _shapeType; private readonly string _prefix; private readonly Func<BuildShapeContext, dynamic> _shapeBuilder; private string _groupId; private readonly Func<TCacheKey> _keyBuilder; private readonly Func<TCacheKey, dynamic> _cacheAccessor; public CachedContentShapeResult(string shapeType, string prefix,
Func<BuildShapeContext, dynamic> shapeBuilder,
Func<TCacheKey> keyBuilder, Func<TCacheKey, CachedShape> cacheAccessor) : base(shapeType, prefix, shapeBuilder) { _shapeType = shapeType; _prefix = prefix; _shapeBuilder = shapeBuilder; _keyBuilder = keyBuilder; _cacheAccessor = cacheAccessor; } public override void Apply(BuildDisplayContext context) { ApplyImplementation(context, context.DisplayType); } public override void Apply(BuildEditorContext context) { ApplyImplementation(context, null); } private void ApplyImplementation(BuildShapeContext context, string displayType) { if (!string.Equals(context.GroupId ?? "", _groupId ?? "", StringComparison.OrdinalIgnoreCase)) return; var placement = context.FindPlacement(_shapeType, _differentiator, _defaultLocation); if (string.IsNullOrEmpty(placement.Location) || placement.Location == "-") return; dynamic parentShape = context.Shape; dynamic newShape; var key = _keyBuilder(); var cache = _cacheAccessor(key); if (cache.Html != null) { newShape = context.New.CachedShape(ChildContent: cache.Html); } else { newShape = _shapeBuilder(context); newShape.CacheMetadata = cache; } // Only need to bother with metadata if result isn't cached // TODO: Maybe some will end up useful anyway, e.g. placement source, displaytype if (cache.Html==null) { ShapeMetadata newShapeMetadata = newShape.Metadata; newShapeMetadata.Prefix = _prefix; newShapeMetadata.DisplayType = displayType; newShapeMetadata.PlacementSource = placement.Source; // if a specific shape is provided, remove all previous alternates and wrappers if (!String.IsNullOrEmpty(placement.ShapeType)) { newShapeMetadata.Type = placement.ShapeType; newShapeMetadata.Alternates.Clear(); newShapeMetadata.Wrappers.Clear(); } foreach (var alternate in placement.Alternates) { newShapeMetadata.Alternates.Add(alternate); } foreach (var wrapper in placement.Wrappers) { newShapeMetadata.Wrappers.Add(wrapper); } } var delimiterIndex = placement.Location.IndexOf(':'); if (delimiterIndex < 0) { parentShape.Zones[placement.Location].Add(newShape); } else { var zoneName = placement.Location.Substring(0, delimiterIndex); var position = placement.Location.Substring(delimiterIndex + 1); parentShape.Zones[zoneName].Add(newShape, position); } } } }
Coordinator
Jan 17, 2012 at 5:41 AM

OK, so most of the time, shape caching is too fine-grained and what you want is what contrib.cache is doing: output caching. You may want donut caching sometimes, which I understand is on its way, but not yet available. If that's not working for you, data caching is usually your best second choice. This can happen in service classes, controllers or drivers, depending how clean you want your design to be. If that is still not what you're after, and what is really taking your cycles away is the actual drawing of a shape or tree of shapes, well, that's slightly odd but again we have you covered.

If shapes need to be cached, well, you still need those shapes to exist, and shapes are built by drivers, so yes, those drivers need to run, and shape generation is certainly not what you want to avoid. Now don't forget the lambda that shapes are built with, that are precisely there so that the shape can be created as a form of placeholder for cheap, and that lambda where the actual work happens can be delayed.

Jan 17, 2012 at 7:04 AM

@randompete I looked through your Mechanics project, and extracted the relevant bits for how you were doing caching there. The below is basically the condensed version of it (for posterity). This approach was actually easiest for me to implement given my setup so I went with something similar to it.

// Code based on code from http://scienceproject.codeplex.com

// Cache class

public class CacheResult
{
    public IHtmlString Rendered { get; set; }
    public dynamic DisplayShape { get; set; }
}

// in your ContentPartDriver
protected override DriverResult Display(SomePart part, string displayType, dynamic shapeHelper) {
	dynamic display;
	if (part.ShouldCache) {
	    var key = part.Id;
	    var cacheResult = _cacheManager.Get<string, CacheResult>(key, ctx => {
	        // Set up cache terms
	        ctx.Monitor(_signals.When("Some_Signal");

	        // Build shape
	        var display = BuildDisplayShape(part, displayType, shapeHelper);
	        return new CacheResult() {
	            DisplayShape = display
	        };
	    });
	    display = shapeHelper.Parts_Cached().Cache(cacheResult);
	} else {
	    display = BuildDisplayShape("Parts_Some", displayType, shapeHelper);
	}
	return ContentShape(shapeType, () => display);
}


// Parts/Cached.cshtml
@{
    // Capture or display cache   
    CacheResult cache = Model.Cache;
    if (cache.Rendered == null) {
        using (var capture = Capture(s => cache.Rendered = s)) {
            @Display(cache.DisplayShape)
        }
    }
    @cache.Rendered
}

Jan 17, 2012 at 7:21 AM
bertrandleroy wrote:

OK, so most of the time, shape caching is too fine-grained and what you want is what contrib.cache is doing: output caching. You may want donut caching sometimes, which I understand is on its way, but not yet available. If that's not working for you, data caching is usually your best second choice. This can happen in service classes, controllers or drivers, depending how clean you want your design to be. If that is still not what you're after, and what is really taking your cycles away is the actual drawing of a shape or tree of shapes, well, that's slightly odd but again we have you covered.

Not really sure what's odd about wanting to cache the results of a heavy operation (not only shape drawing, but the entire process from loading data, through shape building and shape drawing); it's basically ad-hoc donut caching. Looking forward to when something like explained on Haacked is released for Razor Donut Caching. But until then, kudos to randompete for a working solution.

Jan 17, 2012 at 7:57 AM
Edited Jan 17, 2012 at 7:59 AM

Nice work guys. especially the code example of randompete and dainkaplan. It's not the nicest solution but it works until we have a better solution.

Jan 17, 2012 at 9:31 AM
Edited Jan 17, 2012 at 9:33 AM

Bertrand - I'm not caching the shape; I'm caching the entire HTML output of the driver. So actually what I'm doing IS output caching, but on a Part level, which means it can also work for authenticated users (which Contrib.Cache doesn't, by design, as Sebastien has pointed out). This could be a massive performance boost in some cases e.g. Containers where by caching a single part you are actually caching a whole *list* of content items being generated in one go. Rendering a single content item is fairly expensive, and a list of them even more so. I'm sure there are many other modules where heavy work will take place in a driver :)

Coordinator
Jan 17, 2012 at 7:30 PM

Absolutely. I'm not denying that it's useful in a few cases. Just trying to add some context :)

Jan 17, 2012 at 9:44 PM

Basically it's a quick crutch until Sebastien gives us proper donut caching :) (Actually, he may very well need a similar approach when it comes to donut caching content displays...)

Coordinator
Jan 17, 2012 at 10:31 PM

For donut caching, I intend to add a create a specific part, which could be added to widgets and content items. Thus the result of the driver/zone would be cached, and a specific action would be able to render them separately. The generated page could embed the result, by resolving the cache, or render ESI tags to let reverse proxies do it by themselves (Varnish, Nginx)