7 ms·
Faster Rails partial rendering and caching. 78% improvement of test application
- tomfakes 14y agoI 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.
- habosa 14y agoI 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?
- nate 14y agoI 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.
- joevandyk 14y agoIt'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.
- steveklabnik 14y agoIf you're rendering a bunch of partials in a loop, you should try rendering it as a collection instead; it's significantly faster.
- netghost 14y agoI know I should just go read the code, but any insight into what the difference is?
- steveklabnik 14y agoRather than all the setup and teardown work that's done for every single partial, that work can be done once for the entire collection.
- netghost 14y agoThanks Steve, that makes perfect sense.
- pjungwir 14y agoI 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.
- taf2 14y agothere is disk i/o involved in rendering each partial every request in development and at least once in production...
- jeltz 14y agoI 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.
- atomical 14y agoHow is the cache being invalidated in this example when a model changes?
- cliftonk 14y agoRails, 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.
- nate 14y agoThanks for answering that. Yep, there's a bit here: https://github.com/n8/multi_fetch_fragments/blob/master/lib/multi_fetch_fragments.rb 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.html#method-i-cache_key http://api.rubyonrails.org/classes/ActiveRecord/Integration....
- cliftonk 14y agoAnother thing to note is that typical memcached configurations have a short max key length (256 or 512 or something like that).
- sokrates 14y agoThis 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-at-Facebook http://www.infoq.com/presentations/Evolution-of-Code-Design-...
- nate 14y agoOh, 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.
- jherdman 14y agoLooks interesting. Has a pull request been sent to Rails to incorporate these changes directly?
- nate 14y agoNo, but if I'm not mistaken they typically like to see new features shake themselves out as gems. Like turbolinks or cache_digests. Right?
- jherdman 14y agoDefinitely cache_digests, Turbolinks seems to have jumped the queue though. Either way, I'm excited to see some real world numbers in my app.
- lukes386 14y agoCorrect 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.
- atomical 14y agoWould template rendering benefit from a C extension like JSON encoding does?
- steveklabnik 14y agoIt'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.
- nthj 14y agoFor what it's worth, I just implemented this on the staging environment of https://www.biglittlepond.com 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.
- nate 14y agoYay! That's awesome. Thanks for that.
- nthj 14y agoSure! 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 https://github.com/n8/multi_fetch_fragments/pull/4
- hayksaakian 14y agoWhy not contribute this to the rails standard package?
- nate 14y agoI'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.
- steveklabnik 14y agoAs 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.'
- nate 14y agoThanks 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
- steveklabnik 14y agoHmm, yeah, then it might be gem-worthy. Can't hurt to talk about it on the list!
- nachteilig 14y agoThis has finally made me use collections appropriately, and man does it make things fast.
- khangtoh 14y agoIt 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.
- eLobato 14y agoAs a guy working on a Rails app that takes 5000ms avg to render a partial, this is likely going into production now.