- Notifications
You must be signed in to change notification settings - Fork 272
Cache compiled templates for partials#205
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Conversation
When mustache encounters `{{> partial_name}}` it loads the `partial_name` partial and renders it using the current context. This is a fairly expensive code path because it involves loading the partial template file, compiling the template, and rendering it. In order to make improvements to the performance of this we want to have a benchmark to assess code changes against.The mustache manual shows partials being used like: ``` base.mustache: <h2>Names</h2>{{#names}}{{> user}}{{/names}} user.mustache: <strong>{{name}}</strong> ``` When using partials in this way, the `user.mustache` partial is loaded and compiled once for each iteration, even though the content is static. Compiling a template is computationally expensive, especially for large and complex partials. This change caches a template object keyed by the partial string, after indentation is applied. Using the same template object each time the partial is used means that compilation only has to happen once. For simple partials (such as the example from the manual, shown above) there's a ~16x speed up. The gain does not increase much as the list length grows. For complex partials the speed up can be better than ~50, getting better with longer lists.hugopeixoto commented Apr 29, 2015
Compiling a partial seems to depend on two things: its name and the indentation level. Maybe using that pair as the cache key would make some sense (to avoid the memory overhead of keeping the whole string around) |
evilstreak commented May 5, 2015
@hugopeixoto The problem with caching based on the name of a partial is that the content of the partial is not guaranteed to be the same from one use to the next. Here's a contrived example: classMyMustache < Mustachedefpartial(name)@counts ||= {}@counts[name] ||= 0count_string="-- this partial has been included #{@counts[name]} times already\n"@counts[partial] += 1super + count_stringendendThat would output something like: I have no way of knowing how many people are using the gem like that, but it would suck to break it for them. There was definitely concern about people using dynamically generated templates in #173, though I'm not sure if it was with a use case like this in mind. The only way to be sure that the template we want is equivalent to one we've used before is to use the full string. |
hugopeixoto commented May 5, 2015
@evilstreak you're right, I forgot that we allowed overriding The only thing that comes to mind that could be a problem (and I'm playing devil's advocate here) is if someone overrode |
evilstreak commented May 7, 2015
I considered overriding |
evilstreak commented May 15, 2015
Is this PR something you're broadly open to? It would be good to get a "yes, this feature is fine in principle but still needs a review to consider the approach taken and the code style" or a "no, this isn't a feature that should be in this library". |
hugopeixoto commented May 15, 2015
Personally I don't see anything wrong with the feature being included, but I'm not a maintainer. I'll try to ping @locks to review this. What do you think about adding an LRU? Does it make sense? Sam Saffron has a gem called lru_redux (https://github.com/SamSaffron/lru_redux) which might be easy to add. |
evilstreak commented May 15, 2015
Given that the cache is only going to be hanging around for the lifetime of the The only case where this naive caching could cause problems is where someone has a large number of different, large templates, and a mid-render garbage collection is necessary to stop the process running out of memory. I don't think guarding against that case is worth the cost of including a dependency, especially since this library doesn't depend on any other gems at the moment. |
Cache compiled templates for partials
locks commented May 16, 2015
🎆 |
judofyr commented Jun 24, 2015
We could also make |
The change we want to include is the performance improvement when using partials in loops: mustache/mustache#205 The breaking change from 0.99 to 1.0 is that mustache now requires Ruby >= 2.0.
Background
We made some changes to the search results page for GOV.UK and when we deployed the new code we saw a massive spike in CPU usage on our servers. We rolled back the change and investigated, and tracked the issue down to the Mustache gem.
The problem turned out to be the way we had used partials, and the way Mustache loads and renders them. We found a work-around using a different partial structure, but since the original structure is easier to work with and likely to be repeated by another developer in the future, wanted to address the root cause in Mustache.
What's the problem?
The mustache manual shows partials being used like:
When using partials in this way, the
user.mustachepartial is loaded and compiled once for each iteration, even though the content is static.Compiling a template is computationally expensive, especially for large and complex partials.
What's changed?
This change caches a template object for each unique partial string after indentation is applied. Using the same template object each time the partial is used means that compilation only has to happen once.
How much faster is it?
We've added a benchmark script for rendering partials. Doing a before and after comparison shows a ~16–20x speed up, depending on where it's run and how many list items need to be iterated through.
For complex partials the speed up can be better than ~50x. Here's a benchmark for the actual partial we were using for search results on GOV.UK: https://gist.github.com/evilstreak/9d3cc767fd0734bbb1f0
Here's a sample run of the benchmark included in this pull request:
Before
After
Possible objections
Template.newmethod and used a class variable to store the cached versions of templates. Since we're using instance variables Ruby should be able to garbage collect them when they're finished with. That should mean we aren't at risk of introducing memory leaks.Hashimplementation is pretty efficient, so it's unlikely we could do better with a naive approach like calculating an MD5 hash of the partial to key the template with. Even if we could find a more efficient approach like that, we'd have to worry about the risk of collisions.