Mildly Internet
November 4, 2015 | code

Profiling Rails Boot Time

Is your Rails app starting to feel sluggish when running common start up commands like server, console and rake? Let’s take a look at debugging rails applications that take a long time to boot.

Most of the pain is going to come from the Gemfile and anything in config/initializers. Large Gemfiles and long lists of initializers add to the time it takes for Rails to boot because Rails has to load all of those files and run any setup code. There are a few options to diagnose which Gems and initializers are more costly than others.

Benchmarking Boot Time

Before we start trying to improve the boot time of our application, we need some baselines to measure any improvements against. Let’s use the rake environment task as the base for our benchmark - this just loads the Rails environment and then exits. We can time it with:

$ time bundle exec rake environment
bundle exec rake environment 4.07s user 1.32s system 99% cpu 5.429 total

We use bundle exec to make sure any preloaders (spring, zeus) don’t get in the way and use a preloaded environment.

As a sample, I’m using the popular open source project Discourse. For an app as big as Discourse, a 4 second boot time is very good.

Profiling

We’ll look into a few ways to profile what’s slow and what’s fast. Let’s have some fun and monkey patch require. Open up config/boot.rb and paste in:

require "benchmark"

def require(file_name)
result = nil

time = Benchmark.realtime do
result = super
end

if time > 0.1
puts "#{time} #{file_name}"
end

result
end

This wraps the call to super - the actual require method - in a call to Benchmark#realtime which returns a float of the total time the code block took to execute. We add a conditional to print the time and file name if the file takes over 100ms to load. To get the output sorted simply pipe to sort.

$ time bundle exec rake environment | sort

0.10626332799438387 arel
0.10734090598998591 nokogiri
0.10807372402632609 active_support/time
0.1093888989998959 openid/consumer/idres.rb
0.1137797289993614 active_support/core_ext/hash/conversions
0.11505815701093525 mime/types
0.12525871800608002 pry
0.12973016197793186 byebug/commands
0.13043017499148846 pry-rails
0.1312439759785775 loofah
0.13336818301468156 openid/consumer
0.13918157300213352 active_support/core_ext/object/conversions
0.13963568801409565 rails-html-sanitizer
0.14628734899451956 openid
0.14947060000849888 action_view/helpers/form_tag_helper
0.15580355300335214 rack/openid
0.16811588499695063 byebug/core
0.17389453400392085 omniauth/strategies/open_id
0.18345668498659506 active_support/core_ext/object
0.1887360849941615 rails/configuration
0.1958723379939329 rails/railtie
0.2032998200156726 rails/engine
0.21330312499776483 sprockets/rails/helper
0.21499990700976923 active_record
0.27881946400157176 sprockets/railtie
0.29778293799608946 rails/application
0.31094263499835506 active_record/railtie
0.43013601601705886 rails
1.0511020889971405 rails/all
1.5489416829950642 discourse/config/environment.rb
bundle exec rake environment 4.15s user 1.36s system 99% cpu 5.541 total

Another great option is the Bumbler Gem. It profiles your application and shows you how long each gem or initializer took to load. Again, this is for the Discourse app.

$ bumbler
Slow requires:
101.97 omniauth-facebook
108.51 sidekiq-statistic
111.31 better_errors
123.78 image_optim
125.08 barber
127.52 onebox
141.45 rake
158.47 rspec-given
160.70 ember-rails
182.73 nokogiri
204.75 sass
285.01 byebug
286.85 seed-fu
288.10 omniauth-openid
317.03 mail
623.52 rails

$ bumbler --initializers
Slow requires:
117.19 active_record.initialize_database
118.97 ./config/initializers/04-message_bus.rb
156.36 :finisher_hook
329.09 :set_routes_reloader_hook
809.04 ./config/initializers/06-ensure_login_hint.rb
1143.55 :load_config_initializers

Remedies

Now that we know what’s slow, there are a few options for dealing with slowness.

Update Gems

The easiest way, believe it or not, is to simply update to the latest version of a slow loading gem. I have run into several instances where I was running an older version that had a dependency on an old version of a slow loading gem (like nokogiri). In the latest stable version that dependency was removed, and I was able to shave off over a second of boot time in several different applications by doing a simple bundle update gem_name.

require: false

By default, any Gem listed in the Gemfile gets loaded when Rails starts. This means boot time usually grows with Gemfile size. However, it is rarely the case that you need a particular gem to be loaded and available everywhere.

If you only need classes / functions from a gem in a handful of files, it is much more performant to add require: false to the Gem declaration. Then, in any file(s) you need access to that gem, add a require gem_name to the top of the file.

If you look at the Discourse Gemfile you will see that many of the Gem declarations tag on a require: false. That is what helps Discource to boot so quickly despite having 93 gem dependencies.

Rails inheritance in lib/

This is a potential issue I have seen in a few places where a class was inheriting from a Rails core class such as ActiveRecord::Base or ApplicationController and then being referenced in an initializer. It appeared that it was requiring Rails all over again (around a 500ms hit). Moving it into a Gem or into the app folder resolved that issue.

Faster routes…

This is one that I unfortunately have no solution for. A large routes file can have a serious impact on boot performance. In one app I worked on I’ve seen it take over 3 seconds to load the routes. In the bumbler output it is the :set_routes_reloader_hook initializer.