Skip to content
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

Object cache #80

Merged
merged 9 commits into from Oct 18, 2013
Merged

Object cache #80

merged 9 commits into from Oct 18, 2013

Conversation

mgmartel
Copy link

Hi again! My current project was starting to lag a bit on my local server, because I'm loading and looping over a lot of files in a single view. Though using the built-in Twig compilation cache makes quite a difference already, it did not quite do the trick yet. This pull request implements two methods of caching:

1. Top-level caching

This one is quite simple. Inspired by the PodsView class of the Pods Framework, TimberLoader::render (and by proxy Timber::render) implements caching using either object cache (default), transients or site transients (for multisite).

In Pods, I never liked how the caching is based on just the file path. Instead, TimberLoader now hashes public fields in the view context. This means that as soon as the data changes, the cache is automatically invalidated (yay!).

Usage:

Timber::render(
    $filenames, 
    $data, 
    $echo, 
    $expires, /** Default: false. False disables cache altogether. When passed an array, the first value is used for non-logged in visitors, the second for users **/
    $cache_mode /** Any of the cache mode constants defined in TimberLoader **/
);

The cache modes are:

TimberLoader::CACHE_NONE /** Disable caching **/
TimberLoader::CACHE_OBJECT /** WP Object Cache **/
TimberLoader::CACHE_TRANSIENT  /** Transients **/
TimberLoader::CACHE_SITE_TRANSIENT /** Network wide transients **/
TimberLoader::CACHE_USE_DEFAULT /** Use whatever caching mechanism is set as the default for TimberLoader **/

The default is set by filtering timber_cache_mode, or by extending TimberLoader and setting $cache_mode. Defaults to object cache.

This method is very effective, but crude - the whole template is cached. So if you have any context dependent sub-views (eg. current user), this mode won't do.

2. Template based caching

This method implements the Twig Cache Extension. It adds the cache tag, for use in templates. Best shown by example:

    {% cache 'index/content' posts %}
        {% for post in posts %}
            {% include ['tease-'~post.post_type~'.twig', 'tease.twig'] %}
        {% endfor %}
    {% endcache %}

The mechanism behind it is the same as with render - the cache key is determined based on a hash of the object/array passed in (in the above example posts).

The cache method used is always the default mode, set using the bespoke filter (by default, object cache).

This method allows for very fine-grained control over what parts of templates are being cached and which are not. When applied on resource-intensive sections, the performance difference is huge.

Extra: TimberKeyGeneratorInterface

Instead of hashing a whole object, you can specify the cache key in the object itself. If the object implements TimberKeyGeneratorInterface, it can pass a unique key through the method get_cache_key. That way a class can for example simply pass last_updated as the unique key.
If arrays contain the key _cache_key, that one is used as cache key.

This may save yet another few processor cycles.

Notes

  • I was not sure about what to do with the file structure. I added a cache directory in the functions dir, and used it for the cache classes and the Twig Cache extension. This is somewhat analogous to how the router is included in Timber. Because there were a lot of files to be included, I added a simple little autoloader for the Cache Extension and Timber\Cache namespaces.

What would you think about refactoring the file structure and implement Composer to manage dependencies? (We should open an issue to discuss this.. it seems relevant to this PR though)

  • Though the cache is invalidated when the data changes, the cache item isn't actually flushed. This is fine when using a back-end like Memcached, that throws out oldest accessed cache entries when memory runs out. When using transients without expiration, however, this may lead to database bloat in the long run. I'm not sure how much of a problem this really is (or if that's the responsibility of the programmer choosing to use transients for the Timber cache...). Something to think about...
  • Developers using these mechanisms need to be aware that their templates can not use any functions that have side effects from generating a view. Eg. a naive view counter that counts every time the footer is loaded will break. This is primarily something to keep in mind for documentation.

Let me know what you think! (Sorry for the lengthy pull request ;) ) If you want to merge, I'll write up proper documentation for using the cache.

Mike added 9 commits October 16, 2013 17:42
Added transient, site-transient and object cache modes to TimberLoader::render and Timber::render methods. Expiration can differ between logged in and non-logged in users (primarily to disable caching for logged in users). Uses data + template name as the cache key so the same template can be loaded with different data and so that a data change automatically invalidates cache data.
Added Asm89's CacheExtension for Twig using the WP Object Cache, always using Generational Cache Strategy. The key generator generates a key based on all public fields available in passed object or array.
…ache key. If array is passed to cache and key '_cache_key' is available, that value is used.
All options are now constants for TimberLoader and default can be set either by extending TimberLoader and overriding $cache_mode, or by adding a filter to timber_cache_mode.
The getter and setter now handle the validation of cache_mode themselves and are more robust overall, so they can be used from outside of TimberLoader.
… access to an instance to leverage the Timber cache.
…der.

The cache adapter now accepts a TimberLoader instance instead of a cache mode, so that it simply leverages the default caching setup in TimberLoader
…ithout setting the expire explicitly, but still allow for cache in templates by default.
@jarednova
Copy link
Member

Yo Mike!

This is really exciting. At WordCamp Toronto last weekend I was talking to an Automattic dev and caching was the #1 thing he flagged. I'm going to look through this and merge. I love some of the API thinking you've applied (like the array for expires) -- my biggest questions might actually touch on changes to the existing API. For example: $echo seems like a wasteful argument on Timber::render when we could just do:

echo Timber::render('index.twig', $data, 600);

... which seems way more intuitive than:

Timber::render('index.twig', $data, true, 600);

Anyway, cool stuff. I'm going to pull down, test and merge over the next couple days

@bryanaka
Copy link

@mgmartel This is great. Twig cache is based of Rails cache digests, and it works pretty well for most cases.

I also like your suggestion of reworking the file structure. I would propose PSR-0 (or as close as we can get to it with WP). I think it just makes sense and allows jumping from library to library fairly easy because you can anticipate the file structure. Composer plays well with PSR-0 too.

And just to provide my two cents, I like the render method Jared provided. Render is really expected to echo intuitively. Returning prepared html would be a nice to have, but maybe as a lower level function and not built into the render function.

@mgmartel
Copy link
Author

@jarednova I agree about the syntax. Not sure if it's worth breaking backwards compatibility though. I doubt the $echo parameter is actually used that often (except maybe for someone's custom caching mechanism), but it will be painful if you expect the method not to echo and it suddenly starts doing so after an upgrade. Not sure what the best strategy is...

@bryanaka Yep, great fan of psr-0 loading! I added a comment in #50 about it.

@jarednova
Copy link
Member

@mgmartel as I've been thinking it over with @bryanaka's note I agree that render intuitively should echo (and like you're saying there are only edge cases where it doesn't). I think at some point introducing a secondary method (maybe ::compile?) for the times where you want the template processed but not displayed. But, battle for another day.

I got started last night on reviewing all the code in the pull req. -- hope to be done by endofday.

@bryanaka
Copy link

If Timber is following semantic versioning, then breaking changes to the API should be expected in pre-1.0.

I mean, it happens in larger projects like Ember.js (routers... lol) and Node. We shouldn't let breaking changes in pre-1.0 stop us from creating a more intuitive API.

@jarednova
Copy link
Member

@mgmartel I'm thrilled by some of the performance boosts I'm seeing. I created a deliberately heavy page with like 600 posts and watched as the time went from 5ish secs to about 2.5ish secs. The only problem I'm seeing is that b/c of the reliance on object cache, by default the boost only happens when the user has a cache plugin turned on.

The modification I'd like to make is that instead of defaulting to TimberLoader::CACHE_OBJECT we default to TimberLoader::CACHE_TRANSIENT. Even though I'd prefer that it go to Object Cache instead of DB, it turns out that Transients handles that logic; from the WordPress codex:

Use the Transients API instead of these functions if you need to guarantee that your data will be cached. If persistent caching is configured, then the transients functions will use the wp_cache functions described in this document. However if persistent caching has not been enabled, then the data will instead be cached to the options table.

Now here's where it gets really cool, when this is used in conjunction with TimberHelper::transient() the savings get really crazy. This query normally executes in 5ish seconds, but with the transients enabled:

$data = TimberHelper::transient('home_data', function(){
    $data = Timber::get_context();
    $data['menu'] = new TimberMenu();
    $posts = Timber::get_posts('numberposts=601');
    $data['posts'] = $posts;
    $data['foo'] = 'bar';
    return $data;
}, 600);
Timber::render('index.twig', $data, true, 600, 'transient');

... it executes in .05 seconds. That's not a 90% savings, that's a 99% savings. And that's achieved WITHOUT a caching plugin. Once I have W3 Total Cache turned on (just for object caching, not page caching) it goes down to about .04 seconds (doesn't sound like much, but hey that's 20%). I'm now tearing into the Twig Cache Extension integration.

@jarednova jarednova merged commit e803ffe into timber:master Oct 18, 2013
@jarednova
Copy link
Member

@mgmartel Everything is now merged. The other thing I discovered is that the {% cache %} tags weren't working with transients b/c WordPress's wp_options table has the wp_options.option_name column at 64 max chars (so keys like _transient_timber_index/content__GCS__c2ce9fb2470f58cad9861f6504 would get truncated and then unfound when get_transient() was run.

I added a bit of code to cut these off at 50 chars (also have to account for the _transient_ that WP prefixes those with.

Really excited to have this as a part of the code. This really helps organize the performance picture. Whenever you come to Boston I owe you way more than a beer.

@mgmartel
Copy link
Author

Sweet :) I completely forgot about the char limit on transient keys (tested the transients with persistent caching setup), good catch.

I agree on defaulting to transients rather than object cache. Users without persistent object cache setup would otherwise only be punished by extra OC overhead without ever reaping any benefits.

The difference in loading times is pretty amazing, happy to see those stats! Happy to contribute to this project, I'm still very excited about it.

@mgmartel mgmartel deleted the object-cache branch October 19, 2013 14:07
@jarednova
Copy link
Member

BTW, I did a Timber session at WordCamp Boston this weekend -- the caching stuff got a round of applause.

@mgmartel
Copy link
Author

Awesome! Good to hear :D

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants