This concept is implemented for Magnolia 5.4.

From evaluated solutions, the one that was finally implemented is that based on SiteMesh filter.

The implementation itself is part of Advanced Cache Module and is using SiteMesh module to perform it's functions.

The main use is to allow caching of parts of the page that are static even when parts of the page are dynamic.

 

JIRA ticket

MAGNOLIA-5992 - Getting issue details... STATUS

Related concepts

Prerequisites

Direct Area Rendering improvements 

Current implementation of DAR is not sufficient.  MAGNOLIA-5895 - Getting issue details... STATUS

Performance 

Whole page needs to be  rendered if we want to render a single area. This reduces the advantages of snippet caching. We need rendering listeners to be able to improve this.  MAGNOLIA-5839 - Getting issue details... STATUS

  • The rendering listeners are already implemented, see  MAGNOLIA-4990 - Getting issue details... STATUS .

 

public interface RenderingListener {
    public void before(Node content, RenderableDefinition definition, Map<String, Object> contextObjects, OutputProvider out);
    public void after(Node content, RenderableDefinition definition, Map<String, Object> contextObjects, OutputProvider out);
}
  • What is missing is configurable OuputProvider  which would serve configurable wrapper for ouput. (currently only custom outputprovider for Direct Area Rendering, hereinafter referred as DAR, is provided) - easy to achieve.
  • Every listener is called before/after every content node to render.
  • We needs to be able send signals to rendering to be able e.g. skip rendering of current content or whole rest of a page after the requested area is rendered.
public interface RenderingListener {
    public String init(OutputProvider out);
    public String before(Node content, RenderableDefinition definition, Map<String, Object> contextObjects, OutputProvider out);
    public String after(Node content, RenderableDefinition definition, Map<String, Object> contextObjects, OutputProvider out);
}
  • And return codes like:
public class RenderingListenerCode {
    SKIP_RENDERING,
	RENDERING_FINISHED,
	...
}
Quick performance test

Time to render http://demopublic.magnolia-cms.com/demo-project~mgnlArea=stage~.html 

Loading

 


Limitations

DAR for JSP not supported 

See Direct Area Rendering MAGNOLIA-5126 - Getting issue details... STATUS . The JSP uses its own output (JspWriter) (see info.magnolia.templating.jsp.cms.AbstractTag.doTag()). It's not possible to control output unless we implement wrapper for it.

We should decide if we want to invest time also to JSP or area caching for JSP won't be supported.

Investigated Options

A] Caching rendering Listener

Creation of cache items

  • A caching listener would:
    • before method: Start caching a component or do nothing according to its cache configuration available from its RenderableDefinition (to be provided by MAGNOLIA-3902 - Getting issue details... STATUS )

    • Every cacheable component would have its own OutputStream.

    • Everything writed in response output is saved also in:

      •  Every seperate component output

        • A] duplicates - every component is saved also in its parent component and again for its grandparent....

        • B] no special logic for putting cached items together needed  (just render whole content from the cache if it's in there, don't collect any cached children) 

    • OutputStream is saved to cache in after method and removed from list of components which are in process of caching.

  • (plus) Straighforward, listeners in rendering already implemented, only some cosmetics improvement needed.
  • (minus) Cache requires depedency on rendering.
  • (minus) Not able to cache surrounding page.
  • (minus) Needs to go to RenderingEngine.

Retrieving of cached items

    1. A listener checks if a cache item for the current content is cached.
    2. If so, writes the output and returns status "skip-rendering".
      • (minus) Small changes in rendering required:
        • rendering listeners API change: listeners are currently not able to signal anything, there are just void methods
        • RederingEngine.render()  has to react and skip redering

B] RenderingTree

See also Concept - partial caching ("Current thoughts").

PageCacheFilter(PCF) -> RenderingTreeFilter(RTF) -> RenderingFilter(RF)

Creation of cache items

  • First page request: 
    1. -> RTF: cache item for this URL not found ->
    2. -> RF: create RenderingTree (tree containing all areas, components and plain text fragments).
    3. RTF <- save RenderingTree into cache.
  • Next page request:
    1. -> RTF: cache item found. 
    2. Process cached item (rendering tree), go trough root elements of the tree:
      1. If an element is cached, retrieve it from cache.
      2. Render it if its not in cache: call RenderingEngine (we would need new RenderingEngine.renderArea(Node, areaName) method if we don't want to have any rendering login in RTF filter and also to not have to modify selectors for every area).
        1. save element into cache if it's cacheable
        2. OR save rendering subtree for this element if not
    3. <- PCF

 Conclusion:

  • (plus) Page around cached components could be cached.
  • (minus) More complex logic needed.

Retrieving of cached items

  1. Check if a 'cache' template for the requested content is available.
  2. If so, compose requested content:
    1. All subcomponents available from cache: just return composed output.
    2. A subcomponent not in cache: render only this component and put it together with cached components.
    3. A subarea not in cache: render only his area.
  • (minus) A logic for smart composing of components may be hard to implement.

C] SiteMesh filter

SiteMesh  is filter which intercepts the request and is able to modify content. The main difference with previous solution would be the order of filters:

We can still use current page caching mechanism, SiteMesh filter sends request for non cached components and inserts them into the resulting page.

The original purpose of SiteMesh is to merge a page with so called 'decorators' which are a kind of template. SiteMesh extracts content from page and composes it according to decorator. We actually doesn't need a decorator for snippet caching but we can use useful implementations of content insertions, request dispatchers and other stuff.

Latest version is SiteMesh 3.0.0

  • (plus)  was rewritten from SiteMesh2 and promises 3x gain in throughput and half the memory usage.
  • (plus) configurable by XML, filter properties and of course by code
  • (plus) extendable
  • (plus) Apache Software License v2.0
  • (minus) Frontend Proxy didn't make it into SiteMesh 3.0
    • parallelized loading of sources
    • JCache
    • external request forwarder
  • (minus) Server side includes not out of the box (vs. SiteMesh2)
  • (minus) poor documentation

SiteMesh & Magnolia

 

Page script
...
<mgnl:include name="stage" url="/demo-project~mgnlArea=stage~" ttl="0" />
...

Filter position

Requirements

  1. In front of CacheFilter since we need cached pages
    • We can't use gzipped items from cache:
      • Solution:
        1. Ungzip and gzip again.
        2. Prevent Gzipping: Signal to GZip/Cache Filter to prevent encoding (we could modify the header of our request wrapper and remove gzip from "Accept-Encoding" - SiteMesh has similar functionallily to filter"If-Modified-Since" header already). Implemented in the prototype (tick)

  2. In front of ContentTypeFilter and following to be able dispatch requests for snippets.

  3. Behind GzipFilter to be able gzip processed pages coming from SiteMesh. This brings the little problem since GzipFilter expect non null content type to be able vote properly.

    • Solution:
      1. Adjust ResponseContentTypeVoter to be able to vote on null content type (use default mime type as ContentTypeFilter does).
      2. Split ContentTypeFilter into two filters since current implementation sets except content type also whole AggregationState.

Conclusion

  • (plus) Page around cached components could be cached.
  • (plus) If separate module, cache remains independent from rendering.
  • (plus) Can be used also as original SiteMesh filter.
    • any post-rendering modifications 
  • (minus) GZipping needs to be handled.

To be done

  • snippet caching module 
    • introduce SiteMesh as separate module (concept to module)  MSITEMESH-8 - Getting issue details... STATUS
    • inner caching (implement internal cache for snippet discovery) 
  • ce cache 
    • backend engine upgrade/migration
      1. ehcache 2.8
      2. Hazelcast / JCache
  • introduce configuration for snippet caching  MSITEMESH-7 - Getting issue details... STATUS
  • redering listeners for automatic snippet inclusion  MAGNOLIA-6000 - Getting issue details... STATUS
  • integration tests  MGNLEE-376 - Getting issue details... STATUS

Simple performance tests

Loading

TTFB = Time to first byte, Total = time to get whole page, Tested page: demo-project home page, number of requests per value: 100. Note that only our current PageCache was used without any optimisations like caching of pre-parsed pages or any caching mechanism directly inside of SiteMesh filter. 

  • Column 1: values for our current page cache with SiteMesh filter disabled. 
  • Column 2: cached page with SiteMesh filter enabled, but no snippets included. 
  • Column 3: processing of page with one snippet (stage), both page and snippet are cached.
  • Column 4: processing of page with one snippet (stage), page cached, the stage has to be rendered.
  • Column 5: redering of whole page with SiteMesh disabled (taken as base = 100%).
  • Column 6: redering of whole page with SiteMesh enabled.

Questions

  • Part of cache of separate module?
    • If we remain the original purpose of SiteMesh (so it could be used not only for snippet caching), should we move SiteMesh into separate module? Secondly if we implement a rendering listener to automatic snippet tags insertion, we would need to add dependency to rendering. By moving it to a separate module cache remains independent on rendering module.

Questions from architecture meetings

  • Is it possible to use SiteMesh for non HTML / XML pages?
    • SiteMesh filter processes only text/html by default, but can be configured for other mime-types as well and is able to include snippets if an include tag is present ((tick) experimentally checked with CSS).

  • Why the above example uses two tags for snippet insertion? (obsolete)
    • It's only example to demonstrate two rules (tags) process (<mgnl:include> for snippet retrieval, <sitemesh:write> for snippet inclusion). It possible to merge these two into  one tag below which also renders area within the page and shows it in the resulting HTML if SiteMesh filter is disabled ((tick)checked by implementation):

      <mgnl:include url="/demo-project~mgnlArea=stage~">
              [@cms.area name="stage"/]
      </mgnl:include>
  • Is it possible to include these tags into HTML comments (<!-- <mgnl:include...) to prevent being rendered when SiteMesh filter is disabled?
    • The question is if we wanna to do that, because:
      1. We would need to use different comment tags for HTML/CSS/... . Maybe it would be better to just check if SiteMesh filter is disabled and don't include SiteMesh tags if so. 
      2. We can't request for direct rendering of an area on the same page if it would be commented.
    • But in case we'll  really want to:
      • org.sitemesh.tagprocessor.TagTokenizer doesn't search comments for tags, but it would be probably possible to use a custom lexer.flex to do that ((error)not checked)
      • OR use conditional comment start tag as workaround and fool SiteMesh (SiteMesh searches for tags inside of these comments)
        • check in non IE browsers (tick) 
        • check in IE since it's not complete conditional tag (question)
<!--[mgnl:include url="/demo-project~mgnlArea=stage~"/><!---->

D] Our own ESI filter

Could be build on modify stream module. Although all weaknesses  can be implemented by us, it's hard to estimate required time for that.  

Comparison with SiteMesh approach

  • (plus) No need of 3th party project.
  • (minus) Needs to be implemented:
    • snippet request dispatcher
  • (minus) Never used in production, no guarantee of stability.
  • Modify stream vs. SiteMesh
    • (minus) processing of multiple tags (currently only one init/close tag can be specified)
    • (minus) wide XML/java configuration out of the box (processed mime-types)

Decision
We decided to go with option C] SiteMesh Filter. 

Estimate

Minimal implementation

  • integrate SiteMesh filter (tick)
  • move it to separate module, polish the code (2 weeks)
  • create samples from SiteMesh and test (1 day)
  • introduce snippet caching configuration (2 days)
  • intergration tests (2 days)
  • This minimal implementation is completely cache-independent, uses only current (Page)CacheFilter

Optimal implementation

  • Cache results of parsing for future use (2 days)
  • Parellel requests for snippets (2 days)
  • Testing/polishing (up to 1 week)

Upgrade to JCache

  • split cache to submodules (1 week)
  • integrate Hazelcast implementation (2 weeks)
Cache keys
  • Workspace:UUID as mentioned in Concept - Configurable cache constraints on renderables
    (minus) is probably not enough since a area doesn't have to have its own content node 

  • Workspace:UUID:definitionName
    (plus) ability to store also items which don't exist in JCR
    (plus) simple to compose, RenderableDefinition goes into listeners as well
    (minus) not able to cache personalised content

  • More complex cache key, see Personalization and Cache 
    (plus) ability to store personalized items
    (minus) probably too much for start
    (question)a class for cache key composition as part of CacheConfiguration?
    • ContentIdentifier configurable per Renderable (with reasonable default).
    • cross site / per site / unique ?

Snippets in page cache keys

We should be able to store parent page cache key into snippet key or better the other way around: store all snippet keys into page cache key (One tag could be used for multiple 'parent' pages). We should store tag key when adding tag (e.g. in rendering listener which would insert esi/ssi tags instead of a component). Where to store snippet keys?:

    1. AggregationState
    2. MgnlContext
    3. ?

We would add snippets key into page cache key on the way back when storing page into the cache so we could delete all related pages when removing a snippet from the cache.

Configurable caching per renderable

  • enabled
  • TTL
  • ?

Open questions

  • "A more advanced implementation would probably do something similar to Edge Side Includes or Varnish, i.e still cache the surrounding page.Concept+-+partial+caching 
  • "We shouldn't introduce cache-specific logic in the rendering module" Configurable cache constraints
  • info.magnolia.module.cache.AbstractListeningFlushPolicy.flushByUUID() has to flush all subnodes?

Additional Reading

Git prototypes




  • No labels

17 Comments

  1. (question) a class for cache key composition as part of CacheConfiguration?

    In page caching, the CachePolicy does that. So far, I am not sure if we want to reuse CachePolicy (and its related configuration) for snippet caching, or an entirely different mechanism.

    I'm also not sure if we want one "global" type of cache key for snippets (say, one per filter and/or per site), or really per component.

    More complex cache key, see Personalization and Cache

    This concept is more about caching entire pages while taking p13n into account. I'm not sure it's still relevant if we have snippet caching, and/or if snippet caching is relevant for p13n (since p13n currently personalizes entire pages)

    We did have, at some point, the idea to add traits to cache keys. But not all traits make sense in a cache key (the number of cached entries would grow exponentially with each possible trait combinations - some traits have many possible values)

    It might make sense to configure/register what we want in the cache key on a per-component basis (workspace:uuid would be default, but in compo A, I also want geo information, in compo B i want gender and usergroups). But it does sound like rendering would depend (optionally) on cache. (to keep it simple, we'd add a CacheKeyGenerator or something on Renderable)

    While this may seem "too much" for a first step, we definitely need to take these cases into account to try and make the right decision.

    B] ESL-like markers

    I'm not sure we'd need a Tee'd output. If we have 2 filters like below, the "snippets" can be taken care of individually by the ESI filter 

    ESIFilter {
      void doFilter() {
        chain.doFilter() // we'll fill the blanks in after stuff gets rendered (or served by CacheFilter)
        fill-in-the-blanks()
      }
    }
     
    CacheFilter {
      void doFilter() {
        // get from cache
        // if not in cache:
        chain.doFilter()
        // store in cache
      }
    }
     
  2. Thanks for the research about TagTokenizer. You're right, it probably doesn't make a whole lot of sense.

    Here's another bunch of questions though:

    • After a page has been parsed, does Sitemesh cache anything ? Or does it process the page and throw away everything ? 
    • If the later, could we cache the result of the parsing (what is it?) and would that help ?
      • If so, we'll want to somehow reuse the same cache key as the regular cache and/or only cache that instead of the rendered html ?
    • Does Sitemesh parse the response output stream "on the fly" or does it do it only once the request finishes ?
  3. After a page has been parsed, does Sitemesh cache anything ? Or does it process the page and throw away everything ?

    SM doesn't use any caching mechanism out of the box.

    If the later, could we cache the result of the parsing (what is it?) and would that help ?

    TagProcessor goes trough CharSequence and processes rules on the way. So the result is again a new CharSequence, no DOM model of something like that. So the caching of this wouldn't help much since it would be cached by (Page)CacheFilter already.

    If so, we'll want to somehow reuse the same cache key as the regular cache and/or only cache that instead of the rendered html ?

    As I said, I don't see simple way how to do it, TagProcessor expects just plain text to go trough, but we could save tags positions so we wouldn't need to go trough whole CharSequence every request.

    Does Sitemesh parse the response output stream "on the fly" or does it do it only once the request finishes ?

    Only after the request finishes, same principle as magnolia response wrappers.

    1. That last reply worries me a bit: that essentially we means we keep the whole response in buffer until we process it. For every request, not just when caching the page.

      A response wrapper could parse while streaming (by buffering only enough bytes to "see" the markup we're looking for)

      1. Doesn't this affects only the first request which goes to rendering since next requests are served by (Page)Cache which streams all at once anyway (unless it's served from a file)?

        1. What's the order of the filters now ? The whole reason for this feature in the first place is that the whole page can't be cache. The "snippets" are too dynamic or are user-dependent, so we don't want to cache them (or we do, but with different cache keys than the page)

          1. So the page with <mgnl:include... /> is cached. Am I missing something?

            1. yep that's cache, but unless I am missing something, that cached-page-with-mgnl-include tags is buffered up in memory for every request while we swap the tags with the actual snippets.

              1. It's not implemented in the prototype but we will probably:

                • cache parsing-information after first request, so we would have something like CharSequence1->snippet1Url->CharSequence2…
                • use this on next requests: we could start streaming CharSequence1 while we are waiting for snippet1...we could skip SiteMesh processing at all 
                1. Yep, that's why I'm not super convinced we still need SiteMesh for this at all; have you looked at the instream-modification module and what Milan Divilek is doing with it ?

                  1. I quickly tested also old modify/ new injection filter. For simple tag replacement it's not bad but I would still go with SM:

                    • I was able to preserve the original functionality of SiteMesh so we would probably introduce it as separate module.
                    • Should be stable as it's used in production (don't know if modify stream was/is used somewhere...I had to fix some N2B issues to get the sample filter even work)
                    • SiteMesh has also couple of useful classes as request dispatchers for decorators which the prototype use for snippet requests so we don't need to implement it on our own and e.g. XML/java configuration of the box
  4. Grégory Joseph

    4:42 PM it looks like the person who writes the template (ftl) is typing in the sitemesh tags, in your example. and there's thus some sort of redundancy between that and the [@cms.area] inside it. What i *tought* we were going for is that .. there's zero changes to the ftl, and whatever renders the areas or components decides to render it in place or to render a "sitemesh" tag instead

    It can be easily done by rendering listener (implemented in the prototype). Area between sitemesh tags doesn't have to be there...but could be there as fallback if fresh snippet is not available.

  5. page:cached, sitemesh:false, ttfb: 0.00233  vs. page:cached, sitemesh:true, ttfb: 0.06377 ... do i read it right that serving of the fully cached page gets 27 times slower with just enabling SiteMesh filter?

  6. That's of course uncached page, sorry...fixed. Fully cached page with just enabling SiteMesh filter is in the second column.

  7. ha that's much better, so just 1.5ms difference for that, but 10-fold difference for when you can still cache all except one snippet vs. not caching at all. cool. I like that (smile)

  8. @gjoseph A hint on packaging here from the unofficial Packaging Cell: advanced performance features like this will likely end up in a new high performance solution ("Ultimate Edition"), so we need to make sure it is independent of what we have today.

    1. Should not be a problem (smile); in fact, I've been wanting to split the cache module like we've split up a couple of others recently (dam, rest, ...) so that might be a good reason to actually do it. (another reason will be that we might have to offer support for something else than ehcache as a "backend")