This site is dedicated to further knowledge about creating Ruby on Rails applications professionaly. We discuss Ruby on Rails features from a performance angle, discuss Ruby on Rails performance analysis methods, provide information on Ruby on Rails scaling and benchmark Ruby on Rails performance for each release. We discuss best practices for selecting Ruby on Rails session containers, fragment and page caching and optimizing database queries.

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 render_component 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.

Posted in performance | Tags components

Comments

blog comments powered by Disqus