In-memory (intermediate) caching

Release 1.0 - ...

You can cache the output of any piece of SXML by wrapping it in a Cache macro.

Example: cache result until site state is flushed or the application pool is recycled:

Smartsite SXML CopyCode image Copy Code
<se:cache>
   [your sxml]
</se:cache>

Example: cache result for 20 minutes:

Smartsite SXML CopyCode image Copy Code
<se:cache maxage="00:20">
   [your sxml]
</se:cache>

How it works

The Cache macro delays SXML execution of it's default parameter, Xml (that you can set using the inner section os the macro. To check whether cache exists for the SXML block to execute, the cache macro first scans the SXML to calculate a unique cache key. If the cache exists and is valid, it is returned. Otherwise, the SXML is executed and the result is stored in module cache. During execution, relations of underlying SXML elements are collected and corresponding cache dependencies are created. These cache dependencies are used to invalidate the cache when changes occur that would affect the outcome of the SXML block.

Key generation 

The cache-key generation process is not as straighforward as you might think, since it cannot simply generate a hash from the given SXML. To illustrate this, consider the following code:

Smartsite SXML CopyCode image Copy Code
<se:cache maxage="00:20">
   <se:xlinks />
</se:cache>

This xlinks macro typically resides in the context of a RenderTemplate. Each item rendered using the RenderTemplate should have different output, but how can the cache macro know about that?

During the scan phase, the SXML parser is used but actual macro execution is suppressed, allowing the cache macro to assemble and register:

  1. Properties explicitly set by the user, except when marked as 'NoCache' in their metadata.
  2. Properties not set by the user that are marked as 'Signature'  

This way, all properties that affect the outcome will be taken into account.

You can manually add data that will be used to generate a cache key by using the aim.cachekey.add() Viper method:

Example: make the cache depend on the querystring parameter 'a':

Smartsite SXML CopyCode image Copy Code
<se:cache maxage="00:20">
   <se:switch expression="request.query(a)">
     [cases]
   </se:switch>
   {aim.cachekey.add(request.query(a))} <!--// Make cache key depend on query param 'a' -->
</se:cache>

Example: make the cache depend on the user-agent (browser-specific indentifier string):

Smartsite SXML CopyCode image Copy Code
<se:cache maxage="00:20">
   <se:sitemap /> <!-- Assume the default formatting of sitemap (smartsite.config) is adapted and browser-specific features are used. -->
   {aim.cachekey.add(request.useragent())} <!--// Make cache key depend on useragent -->
</se:cache>

Cache dependencies

When SXML is executed by the cache macro (in the case no cache is found), AIM relations emitted by macros are collected and corresponding CacheDependency objects are attached to the Cache object, making sure that the cache is invalidated

Consider the following code:

Smartsite SXML CopyCode image Copy Code
<se:cache maxage="00:20">
   <se:xlinks />
   <se:parents />
</se:cache>

The following CacheDependencies are then generated:

  • ItemToFolder to the parent used in the xlinks macro.
  • FolderToItem to all parents of the current item in the current channel

You can also add relations manually:

Smartsite SXML CopyCode image Copy Code
{aim.relations.add(1409)} <!--// if 1409 changes, kill the cache -->
{aim.relations.addfolder(mymenufolder)} <!--// if anything under 'mymenufolder' changes, kill the cache -->

Binding to events

In the following example, an xlinks macro with inputdata from sqlquery to get the recent additions to the site:

Smartsite SXML CopyCode image Copy Code
<se:cache maxage="00:30">
 <se:sqlquery save="recent" maxrows="5" resulttype="datatable">
  <se:parameters>
   <se:parameter name="sql">
    select c.nr, c.title, c.adddate, u.fullname from  
    {channel.view()} c join vwUsers u 
    on c.userid=u.nr
    where c.nr in (
     SELECT nr FROM dbo.fn_RecurseChildren({cms.getitemnumber(TESTCONTENT)},0)
    )
    order by c.adddate desc
   </se:parameter>
  </se:parameters>
 </se:sqlquery>   
 <se:xlinks inputdata="recent">
  <se:parameters>
   <se:parameter name="rowformat">
    <li><a href="{this.location()}">{this.name()}</a>
     <br /> <small>{datetime.format(this.field(adddate), 'd MMMM yyyy HH:mm')} by {this.field(fullname)}</small>
    </li>
   </se:parameter>
   <se:parameter name="resultformat">
    <ul>{this.result()}</ul>
   </se:parameter>
  </se:parameters>
 </se:xlinks>
</se:cache>

When inputdata is used, the xlinks macro wil not register its AIM references, and sqlquery never adds AIM references. So if we want to make sure the cache is invalidated correctly as soon as changes occur that could affect the result, we will have to come up with something...

Well, in addition to using AIM relations for cache invalidation, you can bind cache invalidation directly to Cms events.

The following Viper methods can be used to accomplish this:

  • aim.events.bindtocontentchange()
    Binds to any contentchange.
  • aim.events.bindtocontentchange(ListOfLocators[] parents)
    Binds to contentchanges under one of the given parents.
  • aim.events.bindtocontentchangeinchannel(string channelCode)
    Binds to contentchange in the given Channel.

So, in the previous example, where a hierarchical query is used, we can add the following viper:

Smartsite SXML CopyCode image Copy Code
{aim.events.bindtocontentchange(TESTCONTENT)}

Intermediate object caching

In the examples above, the cache macro is fully self-contained, but you can also store data into cache and use the cache elsewhere.

In the following example, a datatable is created and cached, saving the key for later use...

Smartsite SXML CopyCode image Copy Code
<se:cache save="savedDatatable = this.getcachekey()" resulttype="none">
 <se:sqlquery resulttype="datatable">
  <se:parameters>
   <se:parameter name="sql">
   
    {aim.events.bindtocontentchange(TESTCONTENT, rem="Set event notification")}
   
    select top 5 c.nr, c.title, u.fullname from  
    {channel.view()} c join vwUsers u 
    on c.userid=u.nr
    where c.nr in (SELECT nr FROM dbo.fn_RecurseChildren({cms.getitemnumber(TESTCONTENT)},0))
    order by c.nr
   </se:parameter> 
  </se:parameters>
 </se:sqlquery>
</se:cache>

Note that the resulttype of the cache macro is set to "none" to suppress output after creating the cache.

Now, use the cached datatable as inputdata for an Xlinks macro:

Smartsite SXML CopyCode image Copy Code
<se:xlinks inputdata="{cache.get(buffer.get(savedDatatable))}" trim="both">
 <se:parameters>
  <se:parameter name="rowformat">
   <li>{this.field(title)} - {this.field(fullname)}</li>{char.crlf()}
  </se:parameter>
  <se:parameter name="resultformat">
   <ul>{char.crlf()}{this.result()}</ul>
  </se:parameter>
 </se:parameters>
</se:xlinks>

Pitfalls

When caching intermediate results, think carefully about what to cache, what the dependencies are, and when the cache will be invalidated.

There are many tools that help you generate the right dependencies and make sure the cache will be invalidated when underlying data has changed. If unsure, don't cache for too long, or you will end up with stale data.

Conclusion

The cache macro provides a great way to speed up your site, but don't forget to keep thinking: always remember that the amount of cache reads should be a lot higher than the amount of cache writes in any given scenario.