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// 以下にGemfileに書いたgemがインストールされる
- `project-dir'/bundle/bundler/setup.rb が生成される
例えば、Gemfileにgem 'sshkit'
と書いて bundle install --standaloneすると`project-dir'/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入れとく。
$ 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で生成される実行ファイルを参考に。
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した後、以下のスクリプトを作成。
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 のpull requestを送って--standalone対応となりました。
参考
Faster Test Boot Times with Bundler Standalone の記事で知りました。この記事ではrspecの起動を速くしたかったようです。