I was debugging Mechanize the other day, and thought it would be handy to have a graph of objects in memory and they're relationship with each other. So I put together a simple script that outputs a Graphviz file illustrating what the object points to. Here's the code:
require 'rubygems'
require 'mechanize'
mech = WWW::Mechanize.new
mech.get('http://google.com/')
puts ObjectGraph.graph(mech, /^WWW/)
and the output:
Right now it only supports Arrays, but should be easily extensible to support all other Enumerable types. Here's the code for ObjectGraph:
def self.graph(target, class_name = /./)
stack = [target]
object_links = []
seen_objects = []
seen_hash = {}
while stack.length > 0
object = stack.pop
next if seen_hash.key? object.object_id
seen_hash[object.object_id] = 1
if object.is_a?(Enumerable) && ! object.is_a?(String)
object.each { |iv|
if iv.class.to_s =~ class_name || object.is_a?(Enumerable)
object_links.push([object.object_id, iv.object_id])
stack.push(iv)
end
}
else
object.instance_variables.each do |iv_sym|
iv = object.instance_variable_get iv_sym
if iv.class.to_s =~ class_name || iv.is_a?(Enumerable)
object_links.push([object.object_id, iv.object_id])
stack.push(iv)
end
end
end
seen_objects.push([object.object_id, object.class])
end
s = <<END
digraph g {
graph [ rankdir = "LR" ];
node [ fontsize = "8"
shape = "ellipse"
];
edge [ ];
END
seen_objects.each { |id, klass|
s += <<END
"#{id}" [
label = "<f0> #{id}|#{klass}"
shape = "record"
]
END
}
object_links.each_with_index { |(from, to), i|
s += "\"#{from}\":f0 -> \"#{to}\":f0 [ id = #{i} ]\n"
}
s += "}\n"
s
end
end
Update: added Enumerable support, so Hashes are now graphed.

10 Comments
This is the kind of thing you need to throw into a seattlerb project. May I?
Totally!
You need to assume that the objects you touch can blow up when you start calling method on them. For example, I ran this on mongrel and it blows up when it tries to call .each on the closed socket with an IOError. Simply putting a begin/rescue/end around the part that starts calling methods on object fixes it.
Otherwise, pretty nice.
Thanks Zed! I’ll add that to the next release.
trans@upixie:~/ruby/ratchets/src/project$ rake ograph –trace
(in /file/trans/my/code/ruby/ratchets/src/project)
** Invoke ograph (first_time)
** Execute ograph
rake aborted!
can’t convert Regexp into Hash
/usr/lib/ruby/gems/1.8/gems/ograph-0.0.1/lib/ograph.rb:8:in
/usr/lib/ruby/gems/1.8/gems/ograph-0.0.1/lib/ograph.rb:8:in
graph’
/file/trans/my/code/ruby/ratchets/src/project/Rakefile:81
/usr/lib/ruby/gems/1.8/gems/rake-0.7.1/lib/rake.rb:387:in
/usr/lib/ruby/gems/1.8/gems/rake-0.7.1/lib/rake.rb:387:in
execute’
/usr/lib/ruby/gems/1.8/gems/rake-0.7.1/lib/rake.rb:357:in
/usr/lib/ruby/1.8/thread.rb:135:in
synchronize’
/usr/lib/ruby/gems/1.8/gems/rake-0.7.1/lib/rake.rb:350:in
/usr/lib/ruby/gems/1.8/gems/rake-0.7.1/lib/rake.rb:1906:in
run’
/usr/lib/ruby/gems/1.8/gems/rake-0.7.1/lib/rake.rb:1906:in `run’
/usr/lib/ruby/gems/1.8/gems/rake-0.7.1/bin/rake:7
/usr/bin/rake:18
What is the source for your rake task? Do you have any more info than that?
Pretty much ditto of the example except an instantiation of my Project class.
desc “project object graph”
task :ograph do
require ‘ograph’
require ‘project/project’
project = Project.load
puts ObjectGraph.graph(project, /^Project/)
end
Ah, the interface is updated a little. Try this:
puts ObjectGraph.graph(project, :class_filter => /^Project/)
Thanks, that provided output. But it doesn’t seem to filter. I got lots of Strings in the output.
Strings are enumerable, so they don’t get filtered out.