2011年12月24日土曜日

【翻訳】なぜ Bundler 1.1 は速くなるのか

Pat Shaughnessyさんの "Why Bundler 1.1 will be much faster" を翻訳しました。
元記事はこちら: http://patshaughnessy.net/2011/10/14/why-bundler-1-1-will-be-much-faster
(翻訳の公開と画像の使用は本人より許諾済みです)

翻訳の間違い等があればブログコメントやTwitter(@oshow)などで遠慮無くご指摘ください。

2011年12月発売の WEB+DB PRESS Vol.66 には Bundler の解説記事が載っているそうです。
「Bundler1.1 ではなく Bundler 自体を知りたい」という人は、そちらを手にとってみてはいかがでしょうか。

なぜ Bundler 1.1 は速くなるのか


2011年10月15日
by Pat Shaughnessy

 ここ一年ほどの間で Rails 3 アプリケーションを作っていた人ならば、bundle installbundle update に長い長い時間がかかるのが珍しくないことに気づいていた事と思う。ターミナルが30秒かそれ以上の間ハングしたように見え、その後 "Fetching source index for http://rubygems.org/." なんて文字が表示される。さてここで、あなたに良いニュースがある。明敏なる Bundler & RubyGems.org チームはこれに対する解決策を考えだし、来たる新バージョンの Bundler ではそれが物凄く高速化されたのだ! 今日は、如何にして Bundler 1.1 がこんなにも速くなったのかを見ていきたいと思う――つまり RubyGems & Bundler チーム達の、「巧速化」ぶりを見ていこう。

2011年11月追記:11月15日の Boston.rb の集まりで、この話題に関するプレゼンをしたら盛況だった。Bundler 1.1 についてもっと知りたい人は、その時の動画を見たりスライドのダウンロードが可能だ。

なぜ Budler 1.0 が遅かったのか


 Bundler 1.1 を見ていく前に、なぜ Bundler 1.0 はあんなにも遅かったのだろうか? "Fetching source index..." と表示される時は何が起こっていて、そしてなぜそれに長時間かかったのか? Nick Quaranto はこれについて、2011年1月に良いまとめ記事を書いてくれた。詳細を知りたいなら、その記事を見てみるといいだろう。Nick の説明によると、Bundler を走らせる度に起きているのは、RubyGems.org から全 gem のリストをダウンロードしてそれを処理に通すという作業らしい。RubyGems.org には数万個の gem があるわけだから、とても、とても時間がかかる。Nick は同時に、私がこれから続く節以降で説明しようとしている解決法も示唆してくれた。

なぜこんなことをしているのかと言えば、Bundler の仕事はどの gem をあなたのアプリケーションに含めるべきかを決定する事――「バンドル」する事――であり、その決定は、ある gem がどの gem に依存しているかに基づいているからだ。だから、全ての gem の依存関係情報をダウンロードする必要があるのだ。この全ての情報をダウンロードして処理するのには、30秒かそれ以上かかる(ネットワーク環境と CPU にも左右されるが)。

Bundler 1.1 は本当に速くなったのか?


 新バージョンの Bundler が速くなったのかを調べるのに一番いい方法は、ただインストールして試すだけだ。(訳注:執筆・翻訳時点では Bundler 1.1 が正式リリース前であるため、インストールするために --pre を付けている)

$ gem install bundler --pre
Successfully installed bundler-1.1.rc
1 gem installed
Installing ri documentation for bundler-1.1.rc...
Installing RDoc documentation for bundler-1.1.rc...
$ cd /path/to/my/favorite/rails/app
$ bundle update
Fetching gem metadata from http://rubygems.org/.........
Using rake (0.9.2)
Using multi_json (1.0.3)
Using activesupport (3.1.1)
Using builder (3.0.0)

etc...

Using sass-rails (3.1.4)
Using sqlite3 (1.3.4)
Using uglifier (1.0.3)
Your bundle is updated! Use `bundle show [gemname]` to see where a bundled gem is installed.

もし上記を試したなら、2つの劇的な違いに気づくはずだ。

  1. ずううっと速くなっている。私の場合たった4秒だった。Bundler 1.0 では30秒以上かかった――目覚しい進歩だ!
  2. "Fetching source index..." の表示と長い沈黙の代わりに、"Fetching gem metadata from http://rubygems.org/........." の表示と共にドットが徐々に増えていき、なんだか spec を走らせているように思えた。

しかし、これは実際どう動いているのだろう? そして "Fetching gem metadate…" は何を意味しているのか? ドットが徐々に増えるのは何によってなのか? より詳しく見てみようじゃないか。

RubyGems.org の API


 何が起こっているかのヒントを得るために、bundle update--verbose オプションを付けてもう一度試してみよう。

$ bundle update --verbose
Fetching gem metadata from http://rubygems.org/
Query List: ["rails", "sqlite3", "json", "sass-rails", "coffee-rails", "uglifier", "jquery-rails"]
Query Gemcutter Dependency Endpoint API: rails sqlite3 json sass-rails coffee-rails uglifier jquery-rails
Fetching from: http://rubygems.org/api/v1/dependencies?gems=rails,sqlite3,json,sass-rails,coffee-rails,uglifier,jquery-rails
HTTP Success
Query List: ["bundler", "railties", "actionmailer", "activeresource", "activerecord", "actionpack", "activesupport", "rake", "actionwebservice", "ffi", "sprockets", "tilt", "sass", "coffee-script", "multi_json", "execjs", "therubyracer", "thor"]
Query Gemcutter Dependency Endpoint API: bundler railties actionmailer activeresource activerecord actionpack activesupport rake actionwebservice ffi sprockets tilt sass coffee-script multi_json execjs therubyracer thor
Fetching from: http://rubygems.org/api/v1/dependencies?gems=bundler,railties,actionmailer,activeresource,activerecord,actionpack,activesupport,rake,actionwebservice,ffi,sprockets,tilt,sass,coffee-script,multi_json,execjs,therubyracer,thor
HTTP Success

etc...

おっと、これで Bundler 1.1 がどう動いているかわかりそうだ。短いリストで指定された gem の依存関係情報を得るために、RubyGems.org が提供する HTTP の API を呼んでいる。source index 全体をダウンロードはしないわけだ。どれかの URL をブラウザに貼り付けてみれば、指定された gem の依存関係を表す、JSON に似た何やら複雑なフォーマットのデータが返されるのがわかるだろう。これは実は JSON ではなく、Ruby の Marshal ライブラリが生成した文字列だ。

RubyGems.org の API が動作する感覚をつかむために、HTTParty と Marshal を使って、指定した gem の依存関係を表示する簡単なスクリプトを書いてみた。

require 'rubygems'
require 'httparty'

 class RubyGemsApi
  include HTTParty
  base_uri 'rubygems.org'

  def self.info_for(gems)
    res = get('/api/v1/dependencies', :query => { :gems => gems })
    Marshal.load(res)
  end

  def self.display_info_for(gems)
    info_for(gems).each do |info|
      puts "#{info[:name]} version #{info[:number]} dependencies: #{info[:dependencies].inspect}"
    end
  end
end

RubyGemsApi.display_info_for(ARGV[0])

例として私の大好きな gem である "uglifier" に対してこれを走らせてみる。

$ ruby parse_rubygems_api.rb uglifier
uglifier version 1.0.3 dependencies: [["multi_json", ">= 1.0.2"], ["execjs", ">= 0.3.0"]]
uglifier version 1.0.2 dependencies: [["multi_json", ">= 1.0.2"], ["execjs", ">= 0.3.0"]]
uglifier version 1.0.1 dependencies: [["multi_json", ">= 1.0.2"], ["execjs", ">= 0.3.0"]]
uglifier version 1.0.0 dependencies: [["multi_json", ">= 1.0.2"], ["execjs", ">= 0.3.0"]]
uglifier version 0.5.4 dependencies: [["multi_json", ">= 1.0.2"], ["execjs", ">= 0.3.0"]]
uglifier version 0.5.3 dependencies: [["multi_json", ">= 1.0.2"], ["execjs", ">= 0.3.0"]]
uglifier version 0.5.2 dependencies: [["multi_json", ">= 0"], ["execjs", ">= 0.3.0"]]
uglifier version 0.5.1 dependencies: [["json", ">= 0"], ["execjs", ">= 0"]]
uglifier version 0.5.0 dependencies: [["json", ">= 0"], ["execjs", "~> 0.1.0"]]
uglifier version 0.4.0 dependencies: [["therubyracer", "~> 0.8.0"]]
uglifier version 0.3.0 dependencies: [["therubyracer", ">= 0.8.0"]]
uglifier version 0.2.0 dependencies: []
uglifier version 0.1.1 dependencies: []
uglifier version 0.1.0 dependencies: []

レスポンスには最新版だけじゃなく、gem の各バージョン毎の依存関係が含まれているのに気づいて欲しい。これが必要なのは、Bundler の依存解決アルゴリズムが gem の古いバージョンを使う可能性があるからだ(バンドルしている他の gem の内容により、動作が違ってくる)。

gem 名をコンマで区切ったリストを指定することもでき、例えば上の方で --verbose を指定した時のリストを使えばこうだ。

$ ruby parse_rubygems_api.rb rails,sqlite3,json,sass-rails,coffee-rails,uglifier,jquery-rails

この HTTP API の呼び出しは非常に速い――1秒未満だ――うえに、Bundler が欲する情報を全て提供し、それ以上のことは何もしない。

依存 gem をダウンロードする
Bundler 1.1 のアルゴリズムを視覚化する


 さっきと同じで、簡単な Gemfile を使ってみよう(この Gemfile は3週間前の How does Bundler bundle で使ったものだ)。

source 'http://rubygems.org'
gem 'uglifier’

そして、この Gemfile の内容を表現するシンプルな図を描こう。つまり、一つの gem だけだ。



次に、この Gemfile のあるディレクトリで bundle update --verbose を実行し、出力を見てみる。ただし今度は、出力テキストの合間に図を挟み込み、Bundler の依存 gem 取得アルゴリズムが実際に何をしているかを示そう。

$ bundle update --verbose
Fetching gem metadata from http://rubygems.org/



Query List: ["uglifier"]
Query Gemcutter Dependency Endpoint API: uglifier
Fetching from: http://rubygems.org/api/v1/dependencies?gems=uglifier
HTTP Success

ここで最初に起こっているのは、Gemfile の中に唯一入っている gem である "uglifier" の依存関係を決定するための HTTP リクエストだ。この HTTP リクエストの結果は、上にある parse_rubygems_api.rb スクリプトの出力でわかる。uglifier のそれぞれ違うバージョンの中に出てくる、4つの gem だ。



最新の uglifier は json、execjs、multi_json に依存していて、ある古いバージョンでは "therubyracer" gem に依存している。

その次に Bundler がする事は、2回目の HTTP リクエストを RubyGems.org へ送り、これら4つの gem の依存関係を要求することだ。



Query List: ["multi_json", "execjs", "json", "therubyracer"]
Query Gemcutter Dependency Endpoint API: multi_json execjs json therubyracer
Fetching from: http://rubygems.org/api/v1/dependencies?gems=multi_json,execjs,json,therubyracer
HTTP Success

興味があるなら、parse_rubygems_api.rb を実行してこのリクエストの結果を見ることも出来る。以下は RubyGems.org が返す結果の図だ。



今度は "execjs" gem がいくつかのバージョンの "multi_json" へ依存している事と、"therubyracer" が "libv8" に依存していることがわかる。そして Bundler は RubyGems.org への3回目の HTTP リクエスト送信へと続き、libv8 の依存関係を得る。このリクエストに multi_json は含まれない。なぜなら、既にその情報は持っているからだ。



Query List: ["libv8"]
Query Gemcutter Dependency Endpoint API: libv8
Fetching from: http://rubygems.org/api/v1/dependencies?gems=libv8
HTTP Success

今度は RubyGems.org が空のセットを返す。libv8 は一つも依存する gem を持たないということだ。



Query List: []
Unmet Dependencies:

これで Bundler は必要な全依存情報を手に入れたので依存解決アルゴリズムの実行へと進み、そして最後に、たった今新しくなったバンドルに含まれている gem を列挙する。

Using multi_json (1.0.3) from /Users/pat/.rvm/gems/ruby-1.8.7-p352/specifications/multi_json-1.0.3.gemspec
Using execjs (1.2.9) from /Users/pat/.rvm/gems/ruby-1.8.7-p352/specifications/execjs-1.2.9.gemspec
Using uglifier (1.0.3) from /Users/pat/.rvm/gems/ruby-1.8.7-p352/specifications/uglifier-1.0.3.gemspec
Using bundler (1.1.rc) from /Users/pat/.rvm/gems/ruby-1.8.7-p352/specifications/bundler-1.1.rc.gemspec


関連記事:【翻訳】速くなったのはいいとして、Bundler 1.1 の他の新機能は?

0 件のコメント:

コメントを投稿

フォロワー