Graphing Objects in Memory with Ruby

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 'ograph'
require 'rubygems'
require 'mechanize'

mech = WWW::Mechanize.new
mech.get('http://google.com/')

puts ObjectGraph.graph(mech, /^WWW/)

and the output:

Mechanize Memory Graph for Google

Right now it only supports Arrays, but should be easily extensible to support all other Enumerable types. Here's the code for ObjectGraph:

class 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

  1. Posted January 13, 2007 at 10:51 pm | Permalink

    This is the kind of thing you need to throw into a seattlerb project. May I?

  2. Posted January 13, 2007 at 11:54 pm | Permalink

    Totally!

  3. Posted January 21, 2007 at 12:38 pm | Permalink

    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.

  4. Posted January 21, 2007 at 2:57 pm | Permalink

    Thanks Zed! I’ll add that to the next release.

  5. trans
    Posted January 25, 2007 at 3:46 pm | Permalink

    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

    merge’
    /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

    execute’
    /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

    invoke’
    /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

    invoke’
    /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

  6. Posted January 25, 2007 at 3:55 pm | Permalink

    What is the source for your rake task? Do you have any more info than that?

  7. trans
    Posted January 25, 2007 at 5:14 pm | Permalink

    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

  8. Posted January 25, 2007 at 5:26 pm | Permalink

    Ah, the interface is updated a little. Try this:

    puts ObjectGraph.graph(project, :class_filter => /^Project/)

  9. trans
    Posted January 28, 2007 at 10:11 pm | Permalink

    Thanks, that provided output. But it doesn’t seem to filter. I got lots of Strings in the output.

  10. Posted January 28, 2007 at 10:18 pm | Permalink

    Strings are enumerable, so they don’t get filtered out.

Post a Comment

Your email is never shared. Required fields are marked *

*
*
Check Spelling
Activate Spell Check while Typing