You’ve profiled your application, you’re running a patched version of ruby, and still your application just doesn’t start up fast enough. What’s a poor developer to do? At The Conversation, our start up time has been hovering at a mite over 10 seconds. I know it’s symbolic, but I really want it below 10. It’s taunting me.
At RailsCamp last weekend I had a play around with an idea I had for eking out a bit more speed: optimising the load path. When you require a file, ruby scans the load path sequentially looking for the file you asked for. If you moved the most common file locations to the top of the load path, that should speed things up, right?
Analysis
The first step is to analyze the files loaded by your application to determine the most popular load paths. Loaded files are stored in $LOADED_FEATURES
, and it’s not difficult to work backwards to the load path:
class LoadPathAnalyzer
def initialize(load_path, loaded_features)
@load_path = load_path
@loaded_features = loaded_features
end
def frequencies
load_paths.inject({}) {|a, v|
a[v] ||= 0
a[v] += 1
a
}
end
private
def load_paths
@loaded_features.map {|feature|
@load_path.detect {|path|
feature[0, path.length] == path
}
}.compact
end
end
We need to store this information for later use. I dump it into a file in config using this rake task:
desc "Recalculate $LOAD_PATH frequencies."
task :recalculate_loaded_features_frequency => :environment do
require 'load_path_analyzer'
frequencies = LoadPathAnalyzer.new($LOAD_PATH, $LOADED_FEATURES).frequencies
ideal_load_path = frequencies.to_a.sort_by(&:last).map(&:first)
File.open(IDEAL_LOAD_PATH_FILE, "w") do |f|
f.puts ideal_load_path
end
end
Application
Now we need to reshuffle our load path at an opportune time before Bundler kicks in and requires everything. The top of config/application.rb
directly beforehand is a great spot.
# config/application.rb
require File.expand_path('../boot', __FILE__)
require 'rails/all'
IDEAL_LOAD_PATH_FILE = "config/ideal_load_path"
if File.exists?(IDEAL_LOAD_PATH_FILE)
order = File.open(IDEAL_LOAD_PATH_FILE).lines.map(&:chomp)
$LOAD_PATH.sort_by! {|x| order.index(x).to_i * -1 }
end
# If you have a Gemfile, require the gems listed there, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(:default, Rails.env) if defined?(Bundler)
# And so on...
Results
Does it work?
# Before
> time ruby -r./config/environment.rb -e ''
8.20s user 2.05s system 99% cpu 10.272 total
# After
> time ruby -r./config/environment.rb -e ''
7.91s user 1.62s system 99% cpu 9.560 total
The difference wasn’t as dramatic as I’d hoped, but still a tidy 700 milliseconds. Most importantly though, it got us under 10 seconds!
Note that on a new application you’ll barely notice any difference since it won’t require many files. If your application has been around the block a few times however, try it out and let us know how you go.