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 再定義されてんならそれでもいいが、ともかくどのファイルで一回以上定義されたのかが取れればよかった
Thanks for an idea, you sparked at thought from a angle I hadn’t given thoguht to yet. Now lets see if I can do something with it.