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は超重くて見てらんなかった。他は知らん。表示できてもでかすぎるんで縮小して見てやってください。縮小にこれまた時間かかるんだけどさ。
でかすぎてビットマップにレンダリングできなかったのでsvgで。でかすぎて役には立たないんですが、見てるとなんとなく楽しくなります。
ライブラリとか標準のクラスだけでめちゃくちゃでかいグラフを持ってて、俺の書いたクラスどこー?てな感じ。MiruとかMonotoneなんかがアプリのクラスです。
各辺には何て名前で参照されてるのかを書いてみた。これを辿るのが楽しくてなかなかいい感じ。
ぽろぽろと急にProcが居るのが気になる。どこからも参照されてないのはなんでかなーと思ってたんだけど、そういやThreadの中身辿るの忘れてたわ。Threadから参照されてるのかもしれない。
あれ?Threadの中身ってスレッド固有データしか辿れなくね…。
Procのバインディングの中身まで辿るのはやりすぎじゃねーか大変なことになるんじゃねーかと思ったけどそうでも無かったようだ。
他にも辿り忘れてる何かがあるのかもしれない。あー、Mongrelって拡張ライブラリだっけ。WEBrickで試した方が良かったわ。
Threadと言えば辿ってるうちに別なスレッド動いちゃうんじゃねこれ?そこまで厳密なデータが欲しかったわけじゃないが一応他のスレッドは止めるようにした方が良かったね。
まとめ
まじめに使うなら、一旦なんかしらの中間データに書き出してなんとかふるいにかけてからグラフにしないと使えないね。
クラスが沢山出ちゃうので定数とかクラス自体とか辿るのから外しても良かったんだけど、どこでオブジェクト抱えてるのかさっぱりわからんから適当に外すわけにもいかなくてさー。
色分けしてもっと派手にしようと思ったんだけど、何でどう色分けしたらいいのか眠くて思いつかなかったのでやめ。自分以下で参照してるオブジェクトの数を足して赤くしよう…とか思ったんだけどObjectクラスを参照する奴とかいたらまともな値出ないじゃん。参照がループしてたらめんどいし。
クラスが定義されたファイルによって自分が書いたクラスかどうか判別して、それが自分で書いたやつっぽかったら色つけるとかやろうと思ったが、そもそもオープンクラスなのに定義されたファイルってどこよ…というのはともかく*1、どちらにせよ取れなかったので諦めた。
そのうちrubyでオブジェクトのメモリサイズも取れるようになるっぽいのでそれで色分けしてもいいかもねー。
まあこれでは役に立たないし、開くのもすげー大変だし、ビットマップ系のファイルにレンダリングしようとしたらどうしてもできなかったんだけど、みててなんか楽しいのでよし。
*1 再定義されてんならそれでもいいが、ともかくどのファイルで一回以上定義されたのかが取れればよかった