Optimizing Your Rails App

NOTE: This is only a short post on one thing that I did that drastically reduced memory leaks and helped the performance of my app. if you want to go more in depth, I would recommend Alexander Dymo’s Ruby Performance Optimization.

About a month ago, I became the lead (and only dev) at a small startup. I emphasize small because that’s important to what comes later. Soon after I started, I was seeing a load of Heroku R14 errors... to the tune of 200+ per day. At first I just paid for one more dyno... then another dyno. When I got to seven, I realized that I needed to do something else and fast, because we couldn’t keep upping the dynos every time we got a little bit more traffic. I was also still seeing a TON of R14 errors. Again, I’ll emphasize small, because we really don’t have the large user base that would be actually causing us problems like this. We were just having massive memory leaks. So, time to dig into the code!

The main issue seemed to be a function that saved all recipes that a user could eat (i.e.: they weren’t allergic, didn’t contain any of their dislikes, etc). We were originally determining this by doing something like this:

recipes = Recipe.all
recipes.reject! do | recipe |
recipes.reject! do | recipe |
# and so on...

So what's the better, safer way to do this? Just build a query! ActiveRecord only runs a query when you perform an action on it, so I could build the whole query and then have my only action be saving it to User.recipes. Example:

user_recipes = Recipe.joins(:ingredients)
user_recipes = user_recipes.where('ingredients.id not in (?)', allergies.map(&:id))
user_recipes = user_recipes.where('ingredients.id not in (?)', dislikes.map(&:id))
# and so on...

This is a gross simplification of the actual code. I know that seems like a silly trivial change, but, no joke, it sped our tests alone up by 4 minutes. Why? Because previously, when we were doing all the rejects, Ruby was saving a new variable in each iteration. Also, while I forgot to test my hypothesis, I'm pretty sure that since recipes was an attribute of User and we were assigning to a 'variable' recipes within the User model, that it was actually saving during each call... also super memory intensive.

So what can you do? If you have some code in your app that you think is slow, you can test it using this benchmark script. If you have that added to your lib directory, just call it like this:

Measure.run do
  Recipe.last #whatever code you want to test

The output will look like this:

To translate:
{ruby-version: {"gc": are_garbage_collectors_enabled?, "time": time_it_took_to_run, "gc_count": total_num_of_garbage_collectors, "memory": amount_of_memory_used }}