Template Caching

9 views
Skip to first unread message

Mike Malone

unread,
Aug 5, 2009, 5:24:48 PM8/5/09
to django-d...@googlegroups.com

Hey everyone,


I've been working on a patch for Django that would allow you to optionally cache templates after they've been lexed and parsed (compiled) by the template engine. I've got things far enough along that I have a working implementation, so I thought I'd share here and see if anyone had any thoughts / comments. I've attached my diff to ticket #6262, so visit http://code.djangoproject.com/ticket/6262 to check it out.


I'd like to add template caching as a feature to Django 1.2, and am willing to do whatever is necessary to make this happen.


If you'd rather read prose, here's a bit of an explanation / justification for the changes:


Why?


At the moment, Django is reading templates from disk, lexing them, parsing them, and then rendering them with the current context _every time_ a template is rendered. If your blog.html template loops through an array of 15 blog posts and {% includes %} post.html for each of them, post.html is read from disk (or disk cache), lexed, parsed, and rendered 15 times. All but the render step is avoidable with a bit of caching.


I've done some rather crude benchmarking and, by my measurements, a template takes about 1ms to lex and parse. That doesn't seem like much, but it adds up. I'm working on a project that renders ~400 templates for the index page (you need to do lots of extends and includes if you're making a reusable app that you want to let people skin) and with this patch enabled we're seeing template rendering time decrease by about 350ms. Big win.


How?


The only thing that's absolutely necessary to make cached templates possible in Django is to add a branch in ``django.template.loader.get_template()`` that checks whether the returned template "source" is actually a compiled template, in which case it is returned directly and the compilation step is skipped. Once this is done, template caching can be implemented with a custom template loader.


By simply checking that the template has a ``render`` method in ``get_template``, we get the added benefit of allowing users to write loaders that return custom Template instances, or Template instances that use an alternative template language like Jinja or Mako.


Template-Tag State:


In order to make cached templates usable in practice (and backwards compatible) some changes need to be made in the template tags as well. In particular, the block, extends, and cycle template tags store state on instances. Since template tags are instantiated when the template is parsed, this state sticks around between template renders. This also means templates are not thread-safe (a separate, but related problem) -- if two threads share the same instantiated template and both try to render it problems can arise.


To make template tags safe for cached templates (and thread-safe) all state should be stored in the template context. But if we just stick state in the context dictionary we're polluting the template namespace with irrelevant stuff that probably shouldn't be exposed there. Therefore, I propose adding an attribute to Context instances, ``parser_context`` that can be used to store parser state. 


Unlike the Context object, I believe parser context should be statically scoped per Template render. That is, if the dictionary at the top of the "stack" doesn't contain the requested key, a KeyError should be raised immediately rather than walking the stack looking for the key in dictionaries further up the stack. I think this makes sense since parser state is generally associated with a single Template, and Templates are rendered recursively (because of extends and include tags). My implementation pushes() the parser_context stack at the beginning of each template render, and pops it after rendering is complete.


Unfortunately, since there are numerous template tags that exist "in the wild," some work will probably have to be done to port existing Django projects over to use cached templates. But since template caching will be implemented using a custom loader, and off by default, users can choose to enable it or not, so this shouldn't be a problem.


Refactoring Loaders:


At the moment, template loaders are implemented as module-level functions. This makes them difficult to extend. I suggest refactoring the built-in template loaders to be classes. By implementing __call__ in the ``BaseLoader`` and instantiating a module-level instance of each loader we can maintain backwards compatibility.


Cached Template Loader Options:


Once all this groundwork is done, we need to decide how to implement a caching template loader. There are several options:


1. Don't include one at all. Let users write their own and implement it however they want.

2. Implement a generic caching loader that can be instantiated with a list of loaders that it should try to use to load templates, caching the results. This requires a bit of a change to ``django.template.loader.find_template_source()`` since the current implementation assumes you're passing in a string containing the module path.

3. Implement a caching version of each existing template loader.


I've implemented option 2, and I think it's probably the best alternative.



Looking forward to hearing your comments, suggestions, ridicule, etc,
- Mike


P.S. Thanks to Alex Gaynor and Marty Alchin who have helped review some of the stuff I've done so far. Respect and whatnot.

Russell Keith-Magee

unread,
Aug 6, 2009, 3:55:40 AM8/6/09
to django-d...@googlegroups.com
On Thu, Aug 6, 2009 at 5:24 AM, Mike Malone<mjma...@gmail.com> wrote:
> Hey everyone,
>
> I've been working on a patch for Django that would allow you to optionally
> cache templates after they've been lexed and parsed (compiled) by the
> template engine. I've got things far enough along that I have a working
> implementation, so I thought I'd share here and see if anyone had any
> thoughts / comments. I've attached my diff to ticket #6262, so
> visit http://code.djangoproject.com/ticket/6262 to check it out.
>
> I'd like to add template caching as a feature to Django 1.2, and am willing
> to do whatever is necessary to make this happen.

I haven't had a chance to dig into your patch in detail, but I'm
certainly interested in seeing template improvements like the ones you
describe. At the very least, it's worth putting on the list for
consideration for v1.2.

As is noted in the ticket, one of the reasons that this wasn't done
originally was that the performance boost wasn't seen as being that
considerable. You have provided some compelling stats for the complex
case (400+ template fragments) - but this is a slightly pathological
case. What happens in the simple case? What's the cost/benefit for for
rendering a simple template with just a small handful of fragments?
What about a single page with a very complex node parse tree? Stats
like that would be very helpful in making the case and getting over
any institutionalized lethargy regarding this change.

> By simply checking that the template has a ``render`` method in
> ``get_template``, we get the added benefit of allowing users to write
> loaders that return custom Template instances, or Template instances that
> use an alternative template language like Jinja or Mako.

I'd be particularly interested in hearing Jinja and Mako users weigh
in on this particular comment. If we can improve the accommodation for
external template engines with a relatively simple change to our
rendering, I'm all for it.

> Template-Tag State:
>
> In order to make cached templates usable in practice (and backwards
> compatible) some changes need to be made in the template tags as well.

This is the only part of your proposal that makes me nervous is the
template-tag bit. This isn't because your approach is obviously wrong
or anything like that - it's just a critical part of Django that we
don't want to accidentally cock up in a subtle but
backwards-incompatible way. This is one area that needs to be
inspected _very_ closely.

> Cached Template Loader Options:
>
> Once all this groundwork is done, we need to decide how to implement a
> caching template loader. There are several options:
>
> 1. Don't include one at all. Let users write their own and implement it
> however they want.
>
> 2. Implement a generic caching loader that can be instantiated with a list
> of loaders that it should try to use to load templates, caching the results.
> This requires a bit of a change to
> ``django.template.loader.find_template_source()`` since the current
> implementation assumes you're passing in a string containing the module
> path.
>
> 3. Implement a caching version of each existing template loader.
>
> I've implemented option 2, and I think it's probably the best alternative.

I'm inclined to agree. The decision to cache a template should be
independent of the template loader that is used. Option 1 requires
everyone to reinvent the wheel. Option 3 means that anyone with a
custom template loader would need to implement a second cache-enabled
version. Option 2 means you can wrap the underlying template loader in
a "and cache it while you're at it" layer that is separated from the
loader itself.

Yours,
Russ Magee %-)

Jacob Kaplan-Moss

unread,
Aug 6, 2009, 9:15:35 AM8/6/09
to django-d...@googlegroups.com
Hi Mike --

Ah, it looks like my strategy of "wait for someone else to reply and
hope that he sums up my feelings so that I don't have to bother" has
worked perfectly. Thanks, Russ!

[IOW: I agree completely with Russ.]

I'll be digging into this patch in some detail, but based on what I've
seen so far I'm quite happy with it.

On Thu, Aug 6, 2009 at 2:55 AM, Russell
Keith-Magee<freakb...@gmail.com> wrote:
> As is noted in the ticket, one of the reasons that this wasn't done
> originally was that the performance boost wasn't seen as being that
> considerable.

I should point out that that was *quite* some time ago --
pre-open-source, certainly -- and that the template engine has gotten
a *lot* more complicated since then (autoescaping, template loaders,
app templates... heck, templates didn't even have *comments* back
then...)

I suspect there'll be a goodly speedup even for the common case, since
what caching basically avoids here is the IO requirements of going to
the disk. Processors have gotten lots more powerful over the last five
years, but disk IO is just as slow.

Finally, like Russ, I'm worried about the effect this will have on
existing template tags. Auditing code I have lying around, I see at
least a half-dozen tags that store state on self. I think it's even
figured into some docs and books. So figuring out *some* way of at the
very least easing that transition would help the pill go down quite a
bit.

I think in the end it's worth it regardless, but we need to think a
bit more carefully about how to accommodate legacy template tags
easily.

Jacob

Marty Alchin

unread,
Aug 6, 2009, 10:41:46 AM8/6/09
to django-d...@googlegroups.com
I won't speak to most of this, since Russ and Jacob have said it all,
but I do want to respond to one point.

On Thu, Aug 6, 2009 at 3:55 AM, Russell
Keith-Magee<freakb...@gmail.com> wrote:
>> By simply checking that the template has a ``render`` method in
>> ``get_template``, we get the added benefit of allowing users to write
>> loaders that return custom Template instances, or Template instances that
>> use an alternative template language like Jinja or Mako.
>
> I'd be particularly interested in hearing Jinja and Mako users weigh
> in on this particular comment. If we can improve the accommodation for
> external template engines with a relatively simple change to our
> rendering, I'm all for it.

Mike and I talked about this a bit in IRC before he submitted the
patch, because I had looked into the topic fairly thoroughly while
working my way through Pro Django. I actually wrote up a template
loader that wraps up a Jinja template in a Django candy shell, but of
course it didn't work because the engine expected templates to be
strings. Prior to engaging in a long, beastly workaround (which I
admit was funky, but it worked and I'm fairly proud of it,
regardless), I did make a small change that allowed template loaders
to return Template objects, which worked fine for both Django
templates and (wrapped) Jinja templates.

Of course, it was getting down to the wire on Django 1.0 at the time,
so I didn't submit the patch for review or anything, I just went ahead
with the workaround. So, I won't pretend to be from the Jinja or Mako
camp, but I can speak from experience that Jinja templates can be
achieved with just the small change Mike included in the larger issue
here. In fact, the interfaces are so similar, it would *almost* work
even without the extra wrapper around Jinja templates. The only real
trouble is that their contexts work differently, so it's not a perfect
match, but with the right template loader, the conversion is fairly
straightforward.

Anyway, I hope that perspective helps, but I also look forward to
hearing from people with Jinja/Mako/etc experience, to see if this
addresses any needs I may not have considered during my testing. If it
ends up working as well as it did for me last year, I think this one
feature alone will be well worth including, even if the caching stuff
has to be delayed because of concerns over template tag state.

-Gul

Alex Gaynor

unread,
Aug 6, 2009, 12:48:46 PM8/6/09
to django-d...@googlegroups.com
It seems to me that existing templatetags and users will be just fine
as long as they don't enable the CachedTemplateLoader. If they make
no changes to their application everything will continue to work, but
if they only make a portion of the changes (enabling caching without
fixing their tags) it won't be thread safe.

> I think in the end it's worth it regardless, but we need to think a
> bit more carefully about how to accommodate legacy template tags
> easily.
>
> Jacob
>
> >
>

Alex

--
"I disapprove of what you say, but I will defend to the death your
right to say it." -- Voltaire
"The people's good is the highest law." -- Cicero
"Code can always be simpler than you think, but never as simple as you
want" -- Me

Mike Malone

unread,
Aug 6, 2009, 1:49:09 PM8/6/09
to django-d...@googlegroups.com
> On Thu, Aug 6, 2009 at 2:55 AM, Russell
> Keith-Magee<freakb...@gmail.com> wrote:
>> As is noted in the ticket, one of the reasons that this wasn't done
>> originally was that the performance boost wasn't seen as being that
>> considerable.
>
> I suspect there'll be a goodly speedup even for the common case, since
> what caching basically avoids here is the IO requirements of going to
> the disk. Processors have gotten lots more powerful over the last five
> years, but disk IO is just as slow.

I haven't done very extensive testing on projects other than my own,
but at the very least these changes shouldn't slow anything down. What
I'd really love to see, actually, is some empirical data from people
testing these changes on their own apps. I was really surprised at the
performance gains we saw, so I'd be interested in hearing if simpler
apps see a noticeable improvement as well.

> Finally, like Russ, I'm worried about the effect this will have on
> existing template tags. Auditing code I have lying around, I see at
> least a half-dozen tags that store state on self. I think it's even
> figured into some docs and books. So figuring out *some* way of at the
> very least easing that transition would help the pill go down quite a
> bit.

Yes, I agree that accommodating legacy tags is very important. Leaving
caching off by default should make things easier on people -- they can
choose to upgrade their existing code if and when they decide template
caching is worthwhile. The addition of the "parser context" should
also help. This feature should be well documented along with the risks
associated with storing state on self (note that instances can store
_some_ state if it doesn't change, like variable names and other
arguments).

The only dangerous things I found in the built-in template tags were
block, extends, and cycle. Fixing block & extends was tough (there was
some prior work here which helped a lot, so thanks to the folks who
worked on that). But cycle was a pretty trivial change (two or three
lines), and I suspect it's a more typical tag -- with the "parser
context" available I think most tags can be made cache/thread-safe
fairly easily.

I thought about adding a ``data`` dictionary to template tags with
each template render. This dictionary could be used in ``render`` to
store state (so you could just stick stuff in self.data[key] and
self.data would be set to something like context.parser_context[self])
but I decided that was a bit too magical and wasn't really a big
improvement over explicitly using ``context.parser_context``.

Russ, I totally agree that we need to be careful not to screw up the
template tag stuff. The existing template-tag API is unchanged -- in
fact there's no need to change anything at all unless you enable
caching. The bit that probably needs the most attention & review are
the block/extends changes. I spent a lot of time deciphering that code
and I'm fairly confident that the patch duplicates the existing
functionality, but it was rather complicated code and it's important
we don't mess something up that's used by pretty much every Django app
in the wild ;).

Mike

Reply all
Reply to author
Forward
0 new messages