Components may not be evil, but they sure can be slow
Posted 18 Nov 2005
Let me start with a confession: I've been ignoring Rails components for a while, as I just couldn't figure out what they where good for. Meaning of course, that I didn't need them for my application ;-)
However, after DHH posted his opinions on components on the Rails weblog, I thought I'd better a have a closer look, what they could do for me and also inspect their implementation details. Additionally, I discovered that Typo uses components to display various parts of the front page of my blog. And it seemed pretty slow at doing that. So I smelled a performance rat.
Basically, components enable you to capture the presentation logic of a controller and embed it into another controller. This is implemented by duplicating the current request object, starting all over with request processing, capturing the rendering results and embedding them in the page that called in the first place.
This comprises creating a new controller object, in order enable request processing, and a new view object for template rendering. This already sounded like it wouldn't be particularly efficient, but the results I got, after creating a benchmark configuration for my blog and running railsbench to get some performance data, were shocking. Here's my benchmark data:
page request total stddev% r/s ms/r 1:/ 14.86220 0.1405 6.73 148.62 2:/articles/2005/11/08/accelera... 13.83662 0.1071 7.23 138.37 3:/admin 1.47821 0.9429 67.65 14.78 4:/admin/content 2.95742 0.2235 33.81 29.57 5:/admin/pages 2.33728 0.0951 42.78 23.37 6:/admin/categories 2.09189 0.2452 47.80 20.92 7:/admin/sidebar 13.99597 0.9474 7.14 139.96 8:/admin/themes 5.44847 0.3377 18.35 54.48 9:/admin/users 1.48696 1.0194 67.25 14.87
Page / measures the main blog page, the second entry measures a single article, and the remaining entries present data on the blog's admin interface.
I just couldn't believe my eyes. Only 6.7 requests per second to generate my blogs entry page? Unbelievable (on an Athlon 64 3000+). Especially since my own app runs over 200 requests per second on the same box.
After studying call patterns using Ruby Performance Validator, which is a really good tool for this task btw., I found out that Typo was loading its configuration from the database several times per request. It also tried to figure out the current page's feed url just as often. That seemed awfully wasteful of resources.
The repetitions of the aforementioned actions were caused by 2 before filters installed on the application controller:
class ApplicationController < ActionController::Base
before_filter :reload_settings
before_filter :auto:discovery_defaults
def reload_settings
config.reload
end
def auto_discovery_defaults
auto_discovery_feed(:type => 'feed')
end
def auto_discovery_feed(options)
options = {:only_path => false, :action => 'feed',
:controller => 'xml'}.merge options
@auto_discovery_url_rss = url_for({:format => 'rss20'}.merge options)
@auto_discovery_url_atom = url_for({:format => 'atom10'}.merge options)
end
end
And sidebar content (archive, flickr, tada, etc.) gets delivered through components, which are all derived from ApplicationController, thus inheriting the filters. Bingo!
There are, of course, several ways to fix this problem, I chose the simplest one, taking advantage of the fact that the request object passed to components gets duplicated for component processing. Upon entering the controller responsible for the request, config will be loaded and a processing flag is stored as an instance variable in the request object. Similarly, the values retrieved during feed get stored there as well. When a component gets invoked it retrieves these values from the request object.
def reload_settings
unless @request.instance_variable_get(:@config_reloaded)
@request.instance_variable_set(:@config_reloaded, true)
config.reload
end
end
def auto_discovery_defaults
@auto_discovery_url_rss =
@request.instance_variable_get(:@auto_discovery_url_rss)
@auto_discovery_url_atom =
@request.instance_variable_get(:@auto_discovery_url_atom)
unless @auto_discovery_url_rss && @auto_discovery_url_atom
auto_discovery_feed(:type => 'feed')
@request.instance_variable_set(:@auto_discovery_url_rss,
@auto_discovery_url_rss)
@request.instance_variable_set(:@auto_discovery_url_atom,
@auto_discovery_url_atom)
end
end
Running railsbench again and comparing the results shows that the most important pages are now rendered 4 to 5 times as fast:
page c1 real c2 real c1 r/s c2 r/s c1 ms/r c2 ms/r c1/c2 1: 14.86220 3.75458 6.7 26.6 148.62 37.55 3.96 2: 13.83662 2.70294 7.2 37.0 138.37 27.03 5.12 3: 1.47821 1.29960 67.6 76.9 14.78 13.00 1.14 4: 2.95742 2.71447 33.8 36.8 29.57 27.14 1.09 5: 2.33728 2.13827 42.8 46.8 23.37 21.38 1.09 6: 2.09189 1.73313 47.8 57.7 20.92 17.33 1.21 7: 13.99597 2.73185 7.1 36.6 139.96 27.32 5.12 8: 5.44847 1.42805 18.4 70.0 54.48 14.28 3.82 9: 1.48696 1.15061 67.3 86.9 14.87 11.51 1.29
Note: c1 and c2 label data of the original/modified application. r/s are requests per second and ms/r are milliseconds per request.
Now that's some niiiiiice improvement!
I think that it's pretty easy to shoot oneself in the foot using components. I tend to avoid them. But sometimes they can be nice, in that case: watch out for da slow filters! Either don't derive the components from ApplicationController, make sure that the filters are fast, possibly using the technique presented in this article, or turn them off completely, e.g. if you don't really need to run them and or can get at the necessary data using some other technique.
The possibility to disable filters in derived controller classes was recently added to Rails. So we could have said
skip_before_filter :reload_config
on the component classes, instead of storing a flag inside the request object. It should be even faster than storing flags on the request object, because the filter will be removed from the filter chain during class load time. Maybe I'll measure that at a later time.