Building The Conversation

Speed up your Rails start up time with load path optimization

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.