34 comments

[ 2.6 ms ] story [ 82.3 ms ] thread
I really like this technique. I've used it in a number of places, and it really does give great performance.

My usage has never been as clean looking as this though.

It's always been a bit surprising that Rails doesn't use the multi_get caching calls very much. These can be key to getting great results.

I normally don't like HN negativity, but can someone play skeptic here and tell me why this probably won't give me 78% partial rendering performance gains in a real app? Or if it will, why this isn't standard?
I can totally play skeptic :)

So like I mentioned in the description it really all depends. If your cache store is already really close to your application like on the same server, you aren't likely to see much gain since fetching from Memcached doesn't even go across the network. But using something like Heroku where your Memcached server might even be on another network that's not Amazon's you'll see some nice benefit not having to connect to that Memcached server sequentially.

Also I rendered out 50 items from Memcached in the test app. Your use case might be a lot less, or even a lot more.

It's always annoyed me how slow Ruby/Rails is at rendering views and partials. I don't know why, but some of our small partials that don't do any network calls take a long time to render. Profiling them doesn't really seem to help. Possibly it's GC related.

If template rendering was faster, it wouldn't be as necessary to worry about caching, which brings tons of headaches and complexity.

If you're rendering a bunch of partials in a loop, you should try rendering it as a collection instead; it's significantly faster.
I know I should just go read the code, but any insight into what the difference is?
Rather than all the setup and teardown work that's done for every single partial, that work can be done once for the entire collection.
Thanks Steve, that makes perfect sense.
I remember looking into a similar problem once and seeing something like:

  rendered "foo" 2ms
  rendered "foo" 2ms
  rendered "foo" 1ms
  rendered "foo" 4ms
  rendered "foo" 175ms
  rendered "foo" 2ms
Assuming a really simple partial (and no swapping or run queue), that looks suspiciously like a GC problem.
there is disk i/o involved in rendering each partial every request in development and at least once in production...
I have not looked into it in detail but when I worked on a project using Ramaze and Erubis I had no problems so I am pretty sure the slowdown is somewhere in rails. No idea where though. It is probably partially GC related, but I have no idea why Rails need to create many objects when rendering a template.
How is the cache being invalidated in this example when a model changes?
Rails, by default, uses '<table-name>-<id>-<updated_at>' for the cache key of an activerecord object. When an ActiveRecord object's `updated_at` column is touched, then all of it's cache keys will invalidate. I don't believe there's a default implementation for a collection.

There's significant network overhead to looping over a collection of objects and using cache blocks, so this gem appears to be a big win by using memcached's `multi_get` command.

Thanks for answering that. Yep, there's a bit here:

https://github.com/n8/multi_fetch_fragments/blob/master/lib/...

@collection.each do |item| key = @options[:cache].is_a?(Proc) ? @options[:cache].call(item) : item expanded_key = ActiveSupport::Cache.expand_cache_key(key) keys_to_collection_map[expanded_key] = item end

Where I use ActiveSupport::Cache.expand_cache_key to create a key based on the item or the Proc passed in. And expand_cache_key does the work of coming up with the proper key. And activerecord objects have a default cache_key implemented that uses an updated_at timestamp if it's available:

http://api.rubyonrails.org/classes/ActiveRecord/Integration....

Another thing to note is that typical memcached configurations have a short max key length (256 or 512 or something like that).
This reminds me of a talk about Evolution of Code at Facebook [1]. One of the first architectural restructurings (and still the major driving factor behind later changes) was the switch from sequential memcache_get to parallel memcache_multiget.

[1]: http://www.infoq.com/presentations/Evolution-of-Code-Design-...

Oh, thanks for sharing that. This presentation is great and is food for a couple more ideas I think I might be able to pull off for Rails using multi fetching.
Looks interesting. Has a pull request been sent to Rails to incorporate these changes directly?
No, but if I'm not mistaken they typically like to see new features shake themselves out as gems. Like turbolinks or cache_digests. Right?
Definitely cache_digests, Turbolinks seems to have jumped the queue though. Either way, I'm excited to see some real world numbers in my app.
Correct me if I'm wrong, but isn't 10 dynos to serve ~10 requests / second pretty excessive?

The gem looks great and kudos to the author for getting such a great improvement in responsiveness. I'm just a bit confused why he would choose to set up the benchmark the way he did.

Would template rendering benefit from a C extension like JSON encoding does?
It's not as simple as "write it in C." There's an overhead involved in switching from Rubyland to C land, and that can be significant.
For what it's worth, I just implemented this on the staging environment of https://www.biglittlepond.com. The one-line `render` call for the most recently collected items dropped from ~700 ms to ~50 ms. 25 items per page. This will be going into the production release later this week.

(This doesn't affect every page load, because I was caching the entire rendered view, as well, but for cache-rebuilds when an item is changed or added, this is -stellar-). Thanks, Nate.

Yay! That's awesome. Thanks for that.
Sure! I should note, too, I was already caching each individual item, then pulling in each cache one-by-one. The above improvements were only due to your gem.

Also, I submitted a pull request [1] to add support for cache options, like "expires_in: 3.days".

That's the only thing I could think to add. This is one of those magic libraries that you're just like, man, did that really happen? Everybody should use this.

[1] https://github.com/n8/multi_fetch_fragments/pull/4

Why not contribute this to the rails standard package?
I'd love for it to be part of standard rails, but don't they usually like to see features like this play themselves out as gems before trying to incorporate them (e.g. turbolinks, cache-digests, etc.).

Of course if anyone knows of a better way, please don't hesitate to let me know or ping someone on rails core.

As the newest Rails committer, I'd suggest that you post to the rails-core list to discuss it, that's how things would work.

I haven't looked into your implementation, but if it just makes things faster, and doesn't change semantics, then rolling it right into Rails is feasible. It's new features-semantics that are generally 'new gems.'

Thanks Steve, I'll do that. All it does is add an extra option (or two) to the views render method. By default nothing changes. Stuff only gets invoked if someone wants to do:

render partial: thing, collection: @things, cache: true

Hmm, yeah, then it might be gem-worthy. Can't hurt to talk about it on the list!
This has finally made me use collections appropriately, and man does it make things fast.
It still amazes me that devs are pulling hair trying to save a couple hundreds of ms here and there but ignores and disses the view rendering time Rails suffers from.
As a guy working on a Rails app that takes 5000ms avg to render a partial, this is likely going into production now.