bundlerのstandaloneモードでプログラムの起動時間を速くする

Rubyで書いたスクリプトの起動時間を速くした話。

# tl;dr

* bundle execもしくはbundle install --binstubsで生成して動かす実行ファイルはそこそこ遅い
* 一方、bundlerには bundle install --standaloneというオプションがあり、実行時にbundler無しにversion lockしたgemを使う方法を提供する仕組みで、bundle install --standalone すると bundle/bundler/setup.rb が生成される。
* bundle/bundler/setup.rb にはGemfile.lockに従ったバージョンのgemのlibのパスが$LOAD_PATHにpushされるコードが書かれているので、bundlerに含まれる 'bundle/setup' を require 'bundle/setup'する代わりに bundle/bundler/setup.rb をloadする方が起動は速くなる


# きっかけ

rubyで書いた監視スクリプトの起動が遅く、どうやら`require 'bundle/setup'`がそこそこ遅いということが分かって、では早くする方法がないか調てみた


# bundlerのstandaloneモード

bundle install --standaloneすると以下が行われる

* `project-dir'/bundle/ruby/<ruby-version>/ 以下にGemfileに書いたgemがインストールされる
* `project-dir'/bundle/bundler/setup.rb が生成される

例えば、Gemfileに`gem 'sshkit'`と書いて bundle install --standaloneすると`project-dir'/bundle/bundler/setup.rb は次のファイルが生成されるので、だいたいどういう機能か分かるとおもう。

```ruby:bundle/bundler/setup.rb 
require 'rbconfig'
# ruby 1.8.7 doesn't define RUBY_ENGINE
ruby_engine = defined?(RUBY_ENGINE) ? RUBY_ENGINE : 'ruby'
ruby_version = RbConfig::CONFIG["ruby_version"]
path = File.expand_path('..', __FILE__)
$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/colorize-0.7.7/lib"
$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/net-ssh-2.9.2/lib"
$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/net-scp-1.2.1/lib"
$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/sshkit-1.7.1/lib"
```

bundle execやrequire 'bundle/setup'する代わりに↑を実行時にloadすれば確かにGemfile.lockでロックされたバージョンが使われるので早い


## 計測

どのくらい速くなるか計測した

Gemfileを用意。gemはテストコードでは使わないのでなんでもよいのでとりあえずsshkit入れとく。

```ruby:Gemfile
 $ cat Gemfile
source "https://rubygems.org"
gem 'sshkit'
```

以下のパターンで実行時間を計測した。全てrbenv配下です。

* ruby -e '' 素のruby
* bundle exec ruby -e ''
* binstub化された実行スクリプト
* standalone化された実行スクリプト

### ruby -e ''

```
 $ time ruby -e ''

real    0m0.115s
user    0m0.063s
sys     0m0.050s
```

### bundle exec ruby -e ''

```
 $ time bundle exec ruby -e ''

real    0m0.542s
user    0m0.418s
sys     0m0.115s
```

### binstub

以下のテストスクリプトを作成。内容はbundle install --binstubsで生成される実行ファイルを参考に。

```ruby:bin/binsutb
require 'pathname'
ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
  Pathname.new(__FILE__).realpath)

require 'rubygems'
require 'bundler/setup'
```

実行結果。bundle execするよりは多少速くなったけど、まだ結構遅い。

```
$  time ruby bin/binstub

real    0m0.386s
user    0m0.258s
sys     0m0.121s
```


### standalone

bundle install --standaloneした後、以下のスクリプトを作成。

```ruby:bin/standalone
require_relative '../bundle/bundler/setup'
```

実行結果。確かに速くなった! **bundle execと比べると実に0.4秒の差**。

```
$ time ruby bin/standalone

real    0m0.110s
user    0m0.063s
sys     0m0.042s
```


# デメリット

ちなみにデメリットもある。 --standaloneオプションは.bundle/configに記録されないので、installする度に--standaloneを生やす必要がある。

またBundler.requireが使えなくなるので、例えばRailsでこれやろうとすると動かないので、なんらかうまくやる必要がある。まあRailsみたいにプロセスをdaemon化する場合はstandalone化する必要はなさそう。

# まとめ

--standaloneで実行時間を速くした。実行時間は bundle exec > binstub > standalone の順になった。

RailsなどWebサーバなどプロセスをdaemon化するケースでは起動時間は気にならないので良いですが、監視スクリプトなど実行頻度が高い場合はstandalone化したほうが良い。


自分の場合、監視サーバでの監視スクリプトの実行にstandaloneモードを使ってます。なお、deployにcapistrano-bundle_rsyncを使っており[Support bundle install with --standalone option](https://github.com/sonots/capistrano-bundle_rsync/pull/13) のpull requestを送って--standalone対応となりました。

# 参考

[Faster Test Boot Times with Bundler Standalone](http://myronmars.to/n/dev-blog/2012/03/faster-test-boot-times-with-bundler-standalone) の記事で知りました。この記事ではrspecの起動を速くしたかったようです。