You have a rails webapp, you’re doing TDD, you are always implementing the simplest thing that could possibly work. Well guess what, the simplest is not always the most performant.
If you are always doing simplest thing you’ll not likely to face performance issues early in the product especially if it’s a new product and you won’t have lots of users nor data. But the time will come that your simplest thing will bite you, and that’s fine, if you know what to do.
Finding some performance holes can be quite easy if you have the right tools, some problems are straightforward and often times solving the simple problems will be more than enough. The bullet gem is a great starting point to find N+1 queries, unecessary eager loading and opportunity to use counter cache.
A classic example of the N+1 queries problem: you have a model with a relation, you’re listing all the records for this model with their children below it. For instance:
<% @articles.each do |article| %> <h2><%= article.title %> comments:</h2> <ul> <%= article.comments.each do |comment| %> <li><%= comment.content %></li> <% end %> </ul> <% end %>
Bullet will give you the recommendation for this one:
N+1 Query detected Article => [:comments] Add to your finder: :include => [:comments]
But what if it’s not something that bullet can pickup? Well I personally like rack-mini-profiler. MiniProfiler will profile your code and report it in a very nice way right on your page.
You can then drill down to where are all the queries being generated. Say you have something like this:
Total Upvotes: <%= @comments.sum(&:upvotes) %> <% @comments.find_each do |comment| %> <%= comment.content %> <% end %>
You’ll see a slow portion but not a lot of extra queries, that’s because we’re using find_each here to paginate the load of the comments, and the first line we’re calling
sum(&block). This is causing ActiveRecord to load all of the comments in memory to calculate the sum.
To fix this you just have to use the active_record’s version of sum by removing the ampersand, that is assuming
@comments is an
ActiveRecord::Relation. This will issue a database SUM and will avoid loading all the comments in memory, it is not only faster but also more memory efficient. See how the sum query is considerably faster than the
SELECT * one:
Total Upvotes: <%= @comments.sum(:upvotes) %> # ...
ActiveRecord has a lot more to offer, try to use database calculations when it makes sense.
MiniProfiler also has other nice features, to learn more about it I highly recommend checking out this screencast.
You can also go further down the hole and use the ruby-prof gem. If it’s the case where your bottleneck is not the database or if you’re not on a rack application where you can use the MiniProfiler. To use ruby-prof first you have to install it (of course), then you need to call to the ruby-prof API for it to profile your code.
You can either profile entire requests by using their middleware (add this to config.ru):
use Rack::RubyProf, path: 'tmp/profile'
Another way to run the profiler (and this one works even outside of rack applications is by manually calling the API within the code. ruby-prof’s README explains how to do that but it basically consists of starting and stopping the profiler:
result = RubyProf.profile do # ... code to profile ... end printer = RubyProf::MultiPrinter.new(result) printer.print(path: 'tmp/profile', profile: 'my-profile')
Both will create a few files under your tmp/profile directory. I found the html versions easier to parse visually. You’ll find a call stack and a graph report, they are both a bit difficult to understand but once you do they help you quite a lot on figuring out where the problem is, it’s a bit out of the scope of this blog post to explain all that output, I got some insight from this paid screencast.
These three tools will be extremely helpful to you if you learn them. Always be monitoring your app’s performance with some tool like New Relic, don’t wait until you lose all your users because you app is too slow. It is unlikely that you’ll find all the problems yourself and the tools I mentioned will only help you solve problems you know about.