Easily Valgrind & GDB your Ruby C Extensions Monday, June 08, 2009

Update: John Barnette (@jbarnette) has packaged these rake tasks up as a Hoe plugin: hoe-debugging.

When developing Nokogiri, the most valuable tool I use to track down memory-related errors is Valgrind. It rocks! Aaron and I run the entire Nokogiri test suite under Valgrind before releasing any version.

I could wax poetic about Valgrind all day, but for now I'll keep it brief and just say: if you write C code and you're not familiar with Valgrind, get familiar with it. It will save you countless hours of tracking down heisenbugs and memory leaks some day.

In any case, I've been meaning to package up my utility scripts and tools for quite a while. But they're so small, and it's so hard to make them work for every project ... it's looking pretty likely that'll never happen, so blogging about them is probably the best thing for everyone.

Basics

Let's get to it. Here's how to run a ruby process under valgrind:

# hello-world.rb
require 'rubygems'
puts 'hello world'

# run from cmdline
valgrind ruby hello-world.rb

Oooh! But that's not actually what you want. The Matz Ruby Interpreter does a lot of funky things in the name of speed, like using uninitialized variables and reading past the ends of malloced blocks that aren't on an 8-byte boundary. As a result, something as simple as require 'rubygems' will give you 3800 lines of error messages (see this gist for full output).

Let's try this:

valgrind --partial-loads-ok=yes --undef-value-errors=no ruby hello-world.rb

==15535== Memcheck, a memory error detector.
==15535== Copyright (C) 2002-2007, and GNU GPL'd, by Julian Seward et al.
==15535== Using LibVEX rev 1804, a library for dynamic binary translation.
==15535== Copyright (C) 2004-2007, and GNU GPL'd, by OpenWorks LLP.
==15535== Using valgrind-3.3.0-Debian, a dynamic binary instrumentation framework.
==15535== Copyright (C) 2000-2007, and GNU GPL'd, by Julian Seward et al.
==15535== For more details, rerun with: -v
==15535== 
hello world
==15535== 
==15535== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
==15535== malloc/free: in use at exit: 10,403,440 bytes in 138,986 blocks.
==15535== malloc/free: 420,496 allocs, 281,510 frees, 155,680,688 bytes allocated.
==15535== For counts of detected errors, rerun with: -v
==15535== searching for pointers to 138,986 not-freed blocks.
==15535== checked 10,654,020 bytes.
==15535== 
==15535== LEAK SUMMARY:
==15535==    definitely lost: 21,280 bytes in 1,330 blocks.
==15535==      possibly lost: 27,368 bytes in 1,840 blocks.
==15535==    still reachable: 10,354,792 bytes in 135,816 blocks.
==15535==         suppressed: 0 bytes in 0 blocks.
==15535== Rerun with --leak-check=full to see details of leaked memory.

Ahhh, much better. We don't see any spurious errors.

Without going too far off-topic, I'd should just mention that those "leaks" aren't really leaks, they're characteristic of how the Ruby interpreter manages its internal memory. (You can see this by running this example with --leak-check=full.)

Rakified!

Here's an easy way to run Valgrind on your gem's existing test suite. This rake task assumes you've got Hoe 1.12.1 or higher.

namespace :test do
  # partial-loads-ok and undef-value-errors necessary to ignore
  # spurious (and eminently ignorable) warnings from the ruby
  # interpreter
  VALGRIND_BASIC_OPTS = "--num-callers=50 --error-limit=no \
                         --partial-loads-ok=yes --undef-value-errors=no"

  desc "run test suite under valgrind with basic ruby options"
  task :valgrind => :compile do
    cmdline = "valgrind #{VALGRIND_BASIC_OPTS} ruby #{HOE.make_test_cmd}"
    puts cmdline
    system cmdline
  end
end

Those basic options will give you a decent-sized stack walkback on errors, will make sure you see every error, and will skip all the BS output mentioned above. You can read Valgrind's documentation for more information, and to tune the output.

If you're not testing a gem, or don't have Hoe installed, try this for Test::Unit suites:

def test_suite_cmdline
  require 'find'
  files = []
  Find.find("test") do |f|
    files << f if File.basename(f) =~ /.*test.*\.rb$/
  end
  cmdline = "#{RUBY} -w -I.:lib:ext:test -rtest/unit \
               -e '%w[#{files.join(' ')}].each {|f| require f}'"
end

namespace :test do
  # partial-loads-ok and undef-value-errors necessary to ignore
  # spurious (and eminently ignorable) warnings from the ruby
  # interpreter
  VALGRIND_BASIC_OPTS = "--num-callers=50 --error-limit=no \
                         --partial-loads-ok=yes --undef-value-errors=no"

  desc "run test suite under valgrind with basic ruby options"
  task :valgrind => :compile do
    cmdline = "valgrind #{VALGRIND_BASIC_OPTS} #{test_suite_cmdline}"
    puts cmdline
    system cmdline
  end
end

Getting this to work for rspec suites is left as an exercise for the reader. :-\

A Note for OS X Users

Valgrind isn't just for Linux. You can make Valgrind work on your fancy-pants OS, too! Check out http://www.sealiesoftware.com/valgrind/ for details.

GDB FTW!

Another thing I find myself doing pretty often is running the test suite under the gdb debugger:

gdb --args ruby -S rake test

or in your Rakefile:

namespace :test do
  desc "run test suite under gdb"
  task :gdb => :compile do
    system "gdb --args ruby #{HOE.make_test_cmd}"
  end
end