メモリプロファイラ

メモリプロファイラを作りたいなと思い、gc.cを眺めていたら、ObjectSpace#count_objectsなるメソッドを見つけました。

irb(main):001:0> ObjectSpace.count_objects
=> {:TOTAL=>28000, :FREE=>9767, :T_OBJECT=>105, :T_CLASS=>745, :T_ICLASS=>28, :T
_MODULE=>26, :T_FLOAT=>5, :T_STRING=>4866, :T_REGEXP=>87, :T_ARRAY=>1044, :T_HAS
H=>112, :T_BIGNUM=>3, :T_FILE=>7, :T_DATA=>532, :T_MATCH=>92, :T_VALUES=>136, :T
_NODE=>10445}
irb(main):002:0>

これを使ってメモリプロファイラもどきを作ってみました。
使い方です

  • ruby 1.9.0でかなり新しいバージョンじゃないと動かないと思います
  • rgplot(http://rubyforge.org/projects/rgplot)が必要なのでインストールしておきます
  • 最後に示すソースコードをmemprof.rbという名前でセーブします
  • ruby memprof プロファイルしたいプログラム 引数 として起動します。
  • カレントディレクトリにfoo.pngというグラフが出来ています。
  • WATCH_TYPEを変えることでどの型のデータを表示するか変えることが出来ます。

例えば、次のようなプログラムのプロファイルを取ってみます。

a = []
(1..1000000).each do |n|
  a.push n.to_s
end

(1..1000000).each do |n|
  a[n] = n 
  if n % 100000 == 0 then
    ObjectSpace.garbage_collect
  end
end

途中でGCを強制的に起しています。そうしないとオブジェクトが減らないのでグラフが面白くないです。

こんな感じのグラフになります。ここで注意することは、型ごとの総バイト数じゃなくてその型のオブジェクトの数になっていることです。だから、配列はものすごく大きくなりますが、数は増えてないのでグラフには表れないです。

memprof.rbはこんな感じで動きます。

  • メモリプロファイル用のスレッドを用意し、一定時間間隔でObjectSpace.count_objectsを実行します
  • 実行した結果は配列に実行した時間とともに記録しておきます
  • Ruby終了時にGnuplotのグラフに出力します(END{}を使います)
#!/bin/env ruby
#
# メモリプロファイラ
#
require 'gnuplot'

module MemProf
  PROFILE_RESOLUTION = 0.01

  WATCH_TYPE = [
    :TOTAL, :T_ARRAY, :T_STRING, :T_NODE
  ]
  TEXT_OUTPUT = false

  GNUPLOT_OUTPUT = true
  GNPULOT_FORMAT = "png"
  GNUPLOT_OUTFILE = "foo.png"
  
  mem_snapshot = []
  Thread.new(mem_snapshot) do |ms|
    prevtime = 0
    while true do
      currenttime = Process.times.utime
      if currenttime - prevtime > PROFILE_RESOLUTION then
        ms.push [currenttime, ObjectSpace.count_objects]
      end
      prevtime = currenttime
      Thread.pass
    end
  end
  
  END {
    if TEXT_OUTPUT == true then
      WATCH_TYPE.each do |ev|
        print "# #{ev} \n"
        mem_snapshot.each do |t, v|
          print "#{t} #{v[ev]}\n"
        end
        print "\n\n"
      end
    end

    if GNUPLOT_OUTPUT == true then
      Gnuplot.open do |gp|
        Gnuplot::Plot.new(gp) do |plot|
          plot.terminal GNPULOT_FORMAT
          plot.output GNUPLOT_OUTFILE
          plot.title "Memory usage"
          x = mem_snapshot.map {|n| n[0]}
          WATCH_TYPE.each do |ev|
            y = mem_snapshot.map {|n| n[1][ev]}
            plot.data << Gnuplot::DataSet.new([x, y]) do |ds|
              ds.with = "line"
              ds.title = ev
            end
          end
        end
      end
    end
  }
end

$0 = ARGV[0]
fn = ARGV[0]
ARGV.shift

load fn, true