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?
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.
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.
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.
@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:
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.
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.
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.
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.
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.
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:
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.
34 comments
[ 2.6 ms ] story [ 82.3 ms ] threadMy 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.
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.
If template rendering was faster, it wouldn't be as necessary to worry about caching, which brings tons of headaches and complexity.
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.
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....
[1]: http://www.infoq.com/presentations/Evolution-of-Code-Design-...
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.
(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.
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
Of course if anyone knows of a better way, please don't hesitate to let me know or ping someone on rails core.
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.'
render partial: thing, collection: @things, cache: true