Ruby でラインメモリプロファイラ

プロファイラ好きなモニタの前の皆さんこんにちは。@sonots です。この記事では、Ruby コードのどの行がどのぐらいメモリを消費しているか調べる方法を紹介します。

## オブジェクトの数を数える

Ruby には [ObjectSpace](http://docs.ruby-lang.org/ja/1.9.3/class/ObjectSpace.html) というオブジェクトの情報を集めたり操作したりする module があります。

このモジュールの `each_object` メソッドを使用すると、RubyVM 上の全てのオブジェクトを取り出すことができます。

このメソッドを使って、以下のようなコードを書くと、実行した地点で、RubyVM 中にどのクラスのオブジェクトが何個存在しているのかカウントできたりするわけです。興味深いですね!

```rb
ObjectSpace.each_object.inject(Hash.new 0) {|h,o| 
  h[o.class]+=1; h
}
#=> {Class=>241, String=>9522, Module=>22, Encoding=>100, Object=>2, Bignum=>2, File=>19, Hash=>64, Regexp=>68, RubyVM::InstructionSequence=>659, Array=>779, Mutex=>2, Complex=>1, ThreadGroup=>1, IOError=>1, Binding=>9, RubyVM::Env=>13, Thread=>1, .... }
```

Cレベルのオブジェクトはこれではカウント出来てなかったりするので、より正確には `count_objects` も併用します。

```rb
ObjectSpace.count_objects
#=> {:TOTAL=>26498, :FREE=>605, :T_OBJECT=>44, :T_CLASS=>511, :T_MODULE=>22, :T_FLOAT=>4, :T_STRING=>9542, ...}
```

メモ：ヒープを全部辿って VALUE の TYPE 毎の内訳を出力しているようです。:TOTAL が全体で、:FREE が ruby 内で確保されているけれど、使われてないもの。

ちなみに、[sigdump](https://github.com/frsyuki/sigdump) という gem を require しておくと、`kill -CONT {pid}` のように CONT シグナルを送るとこれらの情報をファイルに吐き出してくれるので便利です。

## オブジェクトのメモリサイズを知る

オブジェクトの**数**ではなく、オブジェクトの**メモリ使用量**を知りたい場合は `memsize_of` を利用できます。

こちらは [Ruby 1.9.2](http://docs.ruby-lang.org/ja/1.9.3/library/objspace.html) から利用可能で、`require 'objspace'` して使用します。

```rb
require 'objspace'
ObjectSpace.memsize_of({a: 4})
#=> 192
```

(短い) String や (小さい) Integer には 0 が返ります。

```rb
ObjectSpace.memsize_of(123)
#=> 0
ObjectSpace.memsize_of("123")
#=> 0
```

これは、Cレベルで最適化の為に、小さい Integer、短い String は VALUE に直接保存されるようになっていて、オブジェクトが作られていないためのようです。この辺りについては [Rubyのしくみ - Ruby Under a Microscope](http://www.amazon.co.jp/exec/obidos/ASIN/4274050653/sonots-22/ref=nosim/) に書いてあるはずなので、そちらを読むと良いと思います。

`GC::INTERNAL_CONSTANTS[:RVALUE_SIZE]` を足すと正しい値になります。

```rb
rvalue_size = GC::INTERNAL_CONSTANTS[:RVALUE_SIZE]
#=> 40 (x86_64)
ObjectSpace.memsize_of(123) + rvalue_size
#=> 40
ObjectSpace.memsize_of("123") + rvalue_size
#=> 40
ObjectSpace.memsize_of({a: 4}) + rvalue_size
#=> 232
```

なお、RVALUE_SIZE も最初から足してくれたらいいのに、という件については issue になっているようです。2.2.0 には入るのかな？ => [Bug #8984](https://bugs.ruby-lang.org/issues/8984)
後記: Ruby 2.2.0 では加算されるようになりました

## オブジェクトが定義された箇所を知る

[Ruby 2.1 以降の ObjectSpace](http://ruby-doc.org/stdlib-2.1.0/libdoc/objspace/rdoc/ObjectSpace.html) にはさらに機能が追加されていて、オブジェクトが定義された箇所を知ることができるようになっています。

この機能を使うには `trace_object_allocations` でブロックを囲むか、`trace_object_allocations_start` および `trace_object_allocations_stop` で処理を囲む必要があります。

```rb
# example.rb
require 'objspace'

ObjectSpace.trace_object_allocations_start
o = Object.new # 調べたいやつ
ObjectSpace.trace_object_allocations_stop

puts ObjectSpace.allocation_sourcefile(o) #=> "example.rb"
puts ObjectSpace.allocation_sourceline(o) #=> 5
ObjectSpace.trace_object_allocations_clear
```

ファイル名と行数がわかりましたね！

## Ruby コードのどの行がどのぐらいメモリを消費しているのか調べる

では、実際にこれらを組み合わせて Ruby コードのどの行がどのぐらいメモリを消費しているのか調べてみましょう。今回は example.rb 中のオブジェクトだけ追って見ることにします。

```rb
# example.rb
require 'objspace'
ObjectSpace.trace_object_allocations_start
foo = ["foo"] * 255 # 調べたいやつ
ObjectSpace.trace_object_allocations_stop

trace_file = File.basename(__FILE__) # example.rb
rvalue_size = GC::INTERNAL_CONSTANTS[:RVALUE_SIZE]
ObjectSpace.each_object do |o|
  file = ObjectSpace.allocation_sourcefile(o)
  next unless file == trace_file # 今回は example.rb だけ追う
  line = ObjectSpace.allocation_sourceline(o)
  memsize  = ObjectSpace.memsize_of(o) + rvalue_size
  klass = o.class
  puts "#{memsize} #{file}:#{line}:#{klass}"
end
ObjectSpace.trace_object_allocations_clear
```

```bash
$ ruby example.rb | sort -nr
2080 example.rb:3:Array
40 example.rb:3:Array
40 example.rb:3:String
```

おー、example.rb の 3行目でなにか大きい Array があることがわかりました！！

## memprof2 gem :dancers:

毎回上のコードを書くのが面倒？よろしい、ならば gem だ！ということで拵えたものがこちらになります。=> [memprof2](https://github.com/sonots/memprof2)

名前は Ruby 1.8 時代に暗躍したらしい [memprof](https://github.com/ice799/memprof) をリスペクトして memprof2 にしてみました。使い方は README に書いていますが、こんなかんじです。

```rb
# example.rb
require 'memprof2'
Memprof2.start
12.times{ "abc" }
Memprof2.report
Memprof2.stop
````

```bash
$ ruby example.rb
480 example.rb:3:String
```

シンプルですね！

## まとめ

Ruby 2.1 から入った ObjectSpace に入った機能を駆使することで、Ruby コードのどの行でどのぐらいのメモリが使われているのかを調べることができるようになりました。

簡単に利用できるように gem にしたものが [memprof2](https://github.com/sonots/memprof2) にありますので、ご利用いただけます。

是非ご活用ください :sushi:

注意: かなりリソースを喰うのでproductionではご利用いただけません