トップ 最新

kumaryu日記

2010-11-01

_ [Ruby] オブジェクト参照グラフを作ってみた

Rubyで、ある時点のオブジェクトがどこから参照されてるのかなーっていうグラフを作ってみました。

まず結論

見てるとなんかおもしろい。

でもやくにたたない。

動機

なんかrubyのプロセスがどんどんでっかくなっていって、メモリ512MBとかいう月490円の安VPSでは困るんだよねぇ。

GC::Profilerとか見てもなんか徐々に生きてるオブジェクトが増えてるんだけど、どこで増えてるんだこれ。ソース見ても覚えがないぞ。

じゃあ見えるようにするか。

実装

ObjectSpace.each_objectでぶん回してインスタンス変数とかたどっていけばいいかー。

結果はdotに描かせよう。

できた。

##highlight ruby
module ObjectGraph
  GraphNode = Struct.new(:object, :children) {
    def add_child(v, name)
      case v
      when Symbol, Fixnum, nil, true, false
      else
        self.children.push(GraphEdge.new(v, name))
      end
    end
  }
  GraphEdge = Struct.new(:object, :name)

  module_function
  def build_graph(root=nil, out=$stdout)
    GC.start
    objs = ObjectSpace.each_object.to_a
    objlist = {}
    objs.each do |obj|
      next if objs.__id__==obj.__id__
      ent = GraphNode.new(obj, [])
      objlist[obj.object_id] = ent 
      obj.instance_variables.each do |name|
        ent.add_child(obj.instance_variable_get(name), name)
      end
      if obj.kind_of?(Array) then
        obj.each_with_index do |v, i|
          ent.add_child(v, i)
        end
      end
      if obj.kind_of?(Hash) then
        obj.each do |k,v|
          ent.children.push(GraphEdge.new(v, k))
        end
      end
      if obj.kind_of?(Struct) then
        obj.each_pair do |k,v|
          ent.add_child(v, k)
        end
      end
      if obj.kind_of?(Range) then
        ent.add_child(obj.first, :begin)
        ent.add_child(obj.last, :end)
      end
      if obj.kind_of?(Module) then
        obj.class_variables.each do |name|
          ent.add_child(obj.class_variable_get(name), name)
        end
        obj.constants(false).each do |name|
          unless obj.autoload?(name) then
            ent.add_child(obj.const_get(name), name)
          end
        end
      end
      if obj.kind_of?(Proc) or obj.kind_of?(Binding) then
        obj = obj.binding if obj.kind_of?(Proc)
        locals = eval('local_variables', obj) + [:self]
        locals.each do |name|
          ent.add_child(eval(name.to_s, obj), name)
        end
      end
    end
    Kernel.global_variables.each do |name|
      ent = (objlist[Object.object_id] ||= GraphNode.new(Object, []))
      ent.add_child(eval(name.to_s), name)
    end
    out.puts 'digraph ObjectGraph {'
    out.puts 'graph [rankdir="LR"]'
    out.puts 'node [shape="box"]'
    owned = {}
    if root then
      output_list = []
      make_object_list(output_list, root, objlist)
      owned[root.__id__] = nil
    else
      output_list = objlist.values
    end
    output_list.each do |ent|
      ent.children.each do |c|
        owned[c.object.__id__] = nil
      end
    end
    output_list.each do |ent|
      id = ent.object.__id__
      if owned.include?(id) or !ent.children.empty? then
        klass = ent.object.class.name.gsub(/[^a-zA-Z0-9_]/, '_')
        out.puts "#{klass}_#{id} [label=\"#{object_label(ent.object)}\"];"
        ent.children.each do |c|
          cobj = c.object
          cid = c.object.__id__
          cklass = cobj.class.name.gsub(/[^a-zA-Z0-9_]/, '_')
          out.puts "#{klass}_#{id} -> #{cklass}_#{cid} [label=\"#{object_label(c.name)}\"];"
        end
      end
    end
    out.puts '}'
  end

  def make_object_list(list, obj, entlist)
    ent = entlist[obj.__id__]
    unless list.include?(ent) then
      list.push(ent)
      ent.children.each {|c| 
        make_object_list(list, c.object, entlist)
      }
    end
  end

  def object_label(obj)
    case obj
    when Fixnum, TrueClass, FalseClass, NilClass
      obj.to_s
    when Symbol
      obj.to_s.gsub(/"/, '\\"')
    when Class, Module
      obj.name
    when String
      s = obj.inspect
      (s[0,16] + (s.size>16 ? '...' : '')).gsub(/^"|"$/, '\\"')
    when Float, Bignum
      obj.to_s
    when Array, Hash
      "\#<#{obj.class.name}: #{obj.size}>"
    else
      "\#<#{obj.class.name}>"
    end
  end
end

if __FILE__==$0 then
  doc = Array.new(100) {|i|
    num = i.to_s
    proc { num }
  }
  ObjectGraph.build_graph(nil, $stdout)
end

各オブジェクトのインスタンス変数と、Moduleではクラス変数および定数と、ArrayやHashやRangeでは中身もたどって、ProcやBindingではそのバインディングでのselfとローカル変数を辿るようにした。

ObjectSpaceで列挙されない奴(Symbol、Fixnum、nil、true、false)は出てこないし、出せても出してない。ただHashの値になってる奴はキーがオブジェクトになってる奴がいる関係で出てこざるを得なかった。

拡張ライブラリっつーか、Cレベルで参照されちゃってる奴はインスタンス変数列挙で辿れない。Arrayとかもその手のやつなので思いついた分は個別対応した。

使い方はまあ見ればわかるが、ObjectGraph.build_graphってやるとその時点でのオブジェクトの参照グラフを標準出力にdotで吐き出す。第一引数になんかオブジェクトを渡すとそいつをグラフ出力の起点にするのと、第二引数になんかIOを渡すと標準出力じゃなくてそこに吐き出す。渡したオブジェクトを起点にするんじゃなくて、渡したクラスのインスタンス以下しか辿らないようにした方が良かったかもね。

結果

どんどんでかくなって困ってたmonotoneのリポジトリブラウザ(簡単なSinatraアプリ)で試しました。バックエンドはmonotoneとパイプで通信なのでDBは使ってないよ。まだ起動したばっかりの時点なのでそんなに多くはないはず…なんだけど。

注意!:超巨大svgです。FirefoxなんかのGecko系のブラウザで見るのを推奨します。Safariは超重くて見てらんなかった。他は知らん。表示できてもでかすぎるんで縮小して見てやってください。縮小にこれまた時間かかるんだけどさ。

miru.svg

でかすぎてビットマップにレンダリングできなかったのでsvgで。でかすぎて役には立たないんですが、見てるとなんとなく楽しくなります。

ライブラリとか標準のクラスだけでめちゃくちゃでかいグラフを持ってて、俺の書いたクラスどこー?てな感じ。MiruとかMonotoneなんかがアプリのクラスです。

各辺には何て名前で参照されてるのかを書いてみた。これを辿るのが楽しくてなかなかいい感じ。

ぽろぽろと急にProcが居るのが気になる。どこからも参照されてないのはなんでかなーと思ってたんだけど、そういやThreadの中身辿るの忘れてたわ。Threadから参照されてるのかもしれない。

あれ?Threadの中身ってスレッド固有データしか辿れなくね…。

Procのバインディングの中身まで辿るのはやりすぎじゃねーか大変なことになるんじゃねーかと思ったけどそうでも無かったようだ。

他にも辿り忘れてる何かがあるのかもしれない。あー、Mongrelって拡張ライブラリだっけ。WEBrickで試した方が良かったわ。

Threadと言えば辿ってるうちに別なスレッド動いちゃうんじゃねこれ?そこまで厳密なデータが欲しかったわけじゃないが一応他のスレッドは止めるようにした方が良かったね。

まとめ

まじめに使うなら、一旦なんかしらの中間データに書き出してなんとかふるいにかけてからグラフにしないと使えないね。

クラスが沢山出ちゃうので定数とかクラス自体とか辿るのから外しても良かったんだけど、どこでオブジェクト抱えてるのかさっぱりわからんから適当に外すわけにもいかなくてさー。

色分けしてもっと派手にしようと思ったんだけど、何でどう色分けしたらいいのか眠くて思いつかなかったのでやめ。自分以下で参照してるオブジェクトの数を足して赤くしよう…とか思ったんだけどObjectクラスを参照する奴とかいたらまともな値出ないじゃん。参照がループしてたらめんどいし。

クラスが定義されたファイルによって自分が書いたクラスかどうか判別して、それが自分で書いたやつっぽかったら色つけるとかやろうと思ったが、そもそもオープンクラスなのに定義されたファイルってどこよ…というのはともかく*1、どちらにせよ取れなかったので諦めた。

そのうちrubyでオブジェクトのメモリサイズも取れるようになるっぽいのでそれで色分けしてもいいかもねー。

まあこれでは役に立たないし、開くのもすげー大変だし、ビットマップ系のファイルにレンダリングしようとしたらどうしてもできなかったんだけど、みててなんか楽しいのでよし。

*1  再定義されてんならそれでもいいが、ともかくどのファイルで一回以上定義されたのかが取れればよかった