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
- Concept - partial caching
- EhCache upgrade
- Personalization and Cache
- Concept - Cache Improvements
- Concept - Configurable cache constraints on renderables
- Concept - Cache arbitrary objects
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
- Current implementation of DAR is able to render only areas which exist in JCR as a node (The only reason for this limitation is to prevent blank page to be rendered). The request url for DAR looks like this: http://demopublic.magnolia-cms.com/demo-project~mgnlArea=stage~.html
-
MAGNOLIA-5837
-
Getting issue details...
STATUS
- We can improve this situation and be able to render also areas which are defined in a template definition: http://demopublic.magnolia-cms.com/demo-project~mgnlArea=main~.html (currently renders the whole page instead)
- Since we support also nested areas, we would need to specify whole path to an area (to be able check existence of corresponding AreaDefinition): http://demopublic.magnolia-cms.com/demo-project~mgnlArea=main/content~.html
- Unfortunately some areas don't exist in JCR nor in template definition: http://demopublic.magnolia-cms.com/demo-project~mgnlArea=htmlHeader~.html The solution to this would be make AreaFilteringListener configurable to be able skip check for area existence.
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.
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.
- Straighforward, listeners in rendering already implemented, only some cosmetics improvement needed.
- Cache requires depedency on rendering.
- Not able to cache surrounding page.
- Needs to go to RenderingEngine.
Retrieving of cached items
- A listener checks if a cache item for the current content is cached.
- If so, writes the output and returns status "skip-rendering".
- 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:
- -> RTF: cache item for this URL not found ->
- -> RF: create RenderingTree (tree containing all areas, components and plain text fragments).
- RTF <- save RenderingTree into cache.
- Next page request:
- -> RTF: cache item found.
- Process cached item (rendering tree), go trough root elements of the tree:
- If an element is cached, retrieve it from cache.
- 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).
- save element into cache if it's cacheable
- OR save rendering subtree for this element if not
- <- PCF
Conclusion:
- Page around cached components could be cached.
- More complex logic needed.
Retrieving of cached items
- Check if a 'cache' template for the requested content is available.
- If so, compose requested content:
- All subcomponents available from cache: just return composed output.
- A subcomponent not in cache: render only this component and put it together with cached components.
- A subarea not in cache: render only his area.
- A logic for smart composing of components may be hard to implement.
C] SiteMesh filter
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
- was rewritten from SiteMesh2 and promises 3x gain in throughput and half the memory usage.
- configurable by XML, filter properties and of course by code
- extendable
- Apache Software License v2.0
- Frontend Proxy didn't make it into SiteMesh 3.0
- parallelized loading of sources
- JCache
- external request forwarder
- parallelized loading of sources
- Server side includes not out of the box (vs. SiteMesh2)
- poor documentation
SiteMesh & Magnolia
... <mgnl:include name="stage" url="/demo-project~mgnlArea=stage~" ttl="0" /> ...
Filter position
Requirements
- In front of CacheFilter since we need cached pages
- We can't use gzipped items from cache:
- Solution:
- Ungzip and gzip again.
- 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
- Solution:
- We can't use gzipped items from cache:
In front of ContentTypeFilter and following to be able dispatch requests for snippets.
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:
- Adjust ResponseContentTypeVoter to be able to vote on null content type (use default mime type as ContentTypeFilter does).
- Split ContentTypeFilter into two filters since current implementation sets except content type also whole AggregationState.
- Solution:
Conclusion
- Page around cached components could be cached.
- If separate module, cache remains independent from rendering.
- Can be used also as original SiteMesh filter.
- any post-rendering modifications
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
- ehcache 2.8
- 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 ( experimentally checked with CSS).
- 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 ( 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 (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:
- 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.
- 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 (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
- check in IE since it's not complete conditional tag
- 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 (not checked)
- The question is if we wanna to do that, because:
<!--[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
Estimate
Minimal implementation
- integrate SiteMesh filter
- 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)
- Workspace:UUID as mentioned in Concept - Configurable cache constraints on renderables
is probably not enough since a area doesn't have to have its own content node - Workspace:UUID:definitionName
ability to store also items which don't exist in JCR
simple to compose, RenderableDefinition goes into listeners as well
not able to cache personalised content - More complex cache key, see Personalization and Cache
ability to store personalized items
probably too much for starta 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?:
- AggregationState
- MgnlContext
- ?
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
- Cache module documentation
- Cache-your-jee-application
- http://symfony.com/doc/current/cookbook/cache/varnish.html
- https://www.varnish-cache.org/trac/wiki/ESIfeatures
- https://github.com/cadement/AmplifyDemo
- https://s3-eu-west-1.amazonaws.com/uploads-eu.hipchat.com/20450/94494/XtblGLkrMfnolVf/esi-Amplify-2014.pdf
- https://www.varnish-cache.org/docs/3.0/tutorial/esi.html
- Sitemesh
17 Comments
Magnolia International
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.
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.
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
Magnolia International
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:
Roman Kovařík
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.
Magnolia International
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)
Roman Kovařík
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)?
Magnolia International
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)
Roman Kovařík
So the page with <mgnl:include... /> is cached. Am I missing something?
Magnolia International
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.
Roman Kovařík
It's not implemented in the prototype but we will probably:
Magnolia International
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 ?
Roman Kovařík
I quickly tested also old modify/ new injection filter. For simple tag replacement it's not bad but I would still go with SM:
Roman Kovařík
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.
Jan Haderka
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?
Roman Kovařík
That's of course uncached page, sorry...fixed. Fully cached page with just enabling SiteMesh filter is in the second column.
Jan Haderka
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
Boris Kraft
@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.
Magnolia International
Should not be a problem ; 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")