bundle install を早くする唯四の方法

皆さん、bundler 使用していますか?(はーい!という声)そうですね、とても便利ですね。でも遅い。何より速さが足りない。そう思うことも時折あるのではないでしょうか。bundle install に数分間かかるのはよくある話、ときによっては10分以上も bundle install だけでかかってしまう。しかも遅いときに限って何も表示されない。壊れてる?…と思って放置してごはんを食べて戻ってくるといつの間にか終わってたりする。別に bundler 自体が悪いわけではないですが、不安になります。
「俺の bundler がこんなに遅いわけがない。もっともっと速くなればいいのに」、そう思ったあなたのためにこの記事です。bundle install 時間を短くするただ一つ、いや四つの方法をご紹介します。

rubygems のバージョンを適切にアップデートする

あまり知られてない気がしますが、gem のバージョンによっては bundle install がものすごく低速になることがあります。私が経験したところでは、 2.0.3 では 1.8.25 の 3 倍程度遅くなっていました。

$ gem --version
2.0.3
$ sudo gem update --system

バージョンを確認して 2.0.3 以下の 2.0 系だった場合、アップデートしておきましょう。2.0.7 は速度的には問題なさそうです。

https://rubygems.org ではなく http://rubygems.org を利用する

標準で利用される source 'https://rubygems.org' よりも、 source 'http://rubygems.org' としたほうが微妙に bundle install が早くなります。下の画像はさくらの VPS にて bundle install を試したときの時間を計測したものですが、 http のときは https と比較して1.5倍近く高速化されていますね。

http proxy がはさまれている環境の場合は、さらに早くなる可能性もあります。
ただ、http にすれば当然ながら公開鍵認証による信頼の恩恵は受けられませんし、本家 bundler にて推奨しているのは https://rubygems.org です。このあたりは自己責任でプリーズ。

並列 bundle install を利用する*1

1.4.0.pre.1 より、並列 bundle install が可能になっています。これは爆速です。ぜかましです。本家 bundler の issue によれば、特に RTT が長いときに効果を発揮するとのこと。ちなみに source 行の http/https の違いは並列化でかなり吸収されますので、並列 install を利用する場合はわざわざ source 行を http に書き換える必然性は薄いです。
雑な計測*2ですが、私の環境ではこんな結果が出ました。

$ gem install bundler --version='1.4.0.pre.2'
$ time bundle install --path=.bundle/gems --binstubs=.bundle/bin
real    4m32.956s
$ rm -rf .bundle/gems .bundle/bin
$ time bundle install -j 10 --path=.bundle/gems --binstubs=.bundle/bin
real    0m39.983s

この機能の原作者は @eagletmt 先生。利用する際はありがとうを三唱しましょう。
ただ、正式版ではなく pre の機能*3 ということはお忘れなく。メモリやCPUも使いますし。これも自己責任ですね。

インストール時間を表示させる

bundler は多くの gem を扱うわけなので、そのうちのひとつの gem の install が長くなると、全体としての bundle install 時間も一気に遅くなってしまいます。
具体的には、therubyracer 0.11.0 問題とか、Nokogiri 1.6.0 問題とかですね。
こういった問題に遭遇していたら、install の遅かったやつを探す必要があるわけですが、現状の bundler だとどこが遅いのか、よくわかりません。
ところで MOGOK では bundle install の時間が表示されるのが地味に便利です*4

Bundler installing..
2013-09-08T14:26:02+09:00 console[app2002.22]:   $ bundle install --path=.bundle/gems --binstubs=.bundle/bin --without=test development
2013-09-08T14:26:02+09:00 console[app2002.22]: Fetching gem metadata from http://rubygems.org/...........
2013-09-08T14:26:09+09:00 console[app2002.22]: Fetching gem metadata from http://rubygems.org/..
2013-09-08T14:26:10+09:00 console[app2002.22]: Installing rake (10.1.0)
2013-09-08T14:26:10+09:00 console[app2002.22]: Installing i18n (0.6.5)
...
2013-09-08T14:26:16+09:00 console[app2002.22]: Installing activeresource (3.2.14)
2013-09-08T14:26:16+09:00 console[app2002.22]: Using bundler (1.1.3)
2013-09-08T14:26:16+09:00 console[app2002.22]: Installing json (1.8.0) with native extensions
2013-09-08T14:26:18+09:00 console[app2002.22]: Installing libv8 (3.16.14.3)
2013-09-08T14:26:18+09:00 console[app2002.22]: Installing rack-ssl (1.3.3)
...

これをローカルでも実施できるよう、 bundle install に時刻表示をつけてみます。なんとなくこんな感じで Gemfile にモンキーパッチを書いておけばそれっぽく表示できます。

class << Bundler.ui
  def tell_me (msg, color = nil, newline = nil)
    msg = word_wrap(msg) if newline.is_a?(Hash) && newline[:wrap]
    msg = "[#{Time.now}] " + msg if msg.length > 3
    if newline.nil?
      @shell.say(msg, color)
    else
      @shell.say(msg, color, newline)
    end
  end
end

source 'http://rubygems.org'
gem 'libv8', '~>3.11.8.17'
gem 'therubyracer'

表示がこんな具合↓になるので、ボトルネック探しがはかどりますね。

$ bundle install --path=.bundle/gems --binstubs=.bundle/bin
[2013-09-08 14:38:26 +0900] Fetching gem metadata from http://rubygems.org/..
[2013-09-08 14:38:27 +0900] Installing libv8 (3.11.8.17)
[2013-09-08 14:38:28 +0900] Installing ref (1.0.5)
[2013-09-08 14:38:46 +0900] Installing therubyracer (0.11.4)
[2013-09-08 14:38:46 +0900] Using bundler (1.4.0.pre.2)
[2013-09-08 14:38:46 +0900] Your bundle is complete!
[2013-09-08 14:38:46 +0900] It was installed into ./.bundle/gems

ただ、これは bundler の内部構造に依存したモンキーパッチなので、バージョンによっては使用できなかったり、最悪の場合は機能を壊してしまう可能性があります。これも自己責任。

以上。

これ全部やれば、人によっては bundle install の時間が 1/10 くらいに縮むかもしれません。
システムワイドにインストールするとか、複数のプロジェクトで同じ BUNDLE_PATH を利用する方法もありますが、それらはあまりお勧めできないかなーと思います。bundle install は --path 指定をし、プロジェクトごとに別々の gem を使うようにしましょー。

Have a happy bundle life!

*1:1.4.0.pre.1 〜

*2:Gemfile.lock も .bundle/config も残ってる

*3:pre.2 の段階では並列のオプションが保存されないとか、pre1 では :github の互換性が崩れていたとか、まだ十分に叩かれていないかも知れないとか、画面表示が変わるとか、いろいろ

*4:ステマ