what we blog

systemtap and Ruby 2.1 (on Ubuntu)

I like dtrace. I also like Linux. And thats where the problems begin: there is no proper dtrace for Linux. However, systemtap exists. systemtap is compatible to dtrace probes, however, it comes with it's own scripting language.

For those who don't know what any of these are: both are tools to efficiently trace programs at runtime, with a very low overhead. To allow that, they use so-called "probes" compiled into the program. Your kernel is usually equipped with such probes and Ruby 2.0 or higher also comes with quite a few of them.

However, the whole process is a bit involved and I found no good explanation for the whole setup and the pitfalls. Read on for my attempt. I will use Ubuntu as an example system.

Prerequisites

To use the probes embedded in the Ruby interpreter, you need first need to make sure that you have a kernel that support userspace probes. This is the case since Linux 3.5, patches for older kernels are available and are part of some distributions (e.g. RHEL). According to the systemtap wiki and my own experiences, this rules out any Ubuntu with a version number below 12.10, unless you want to compile your own kernel. So, let's grab a Ubuntu, e.g. using the raring64 Vagrant box from vagrantbox.es:

$ mkdir systemtapvm; cd systemtapvm
$ vagrant init raring64 https://copy.com/OljurIPEsjaX
$ vagrant up
$ vagrant ssh

After connecting to the VM, you need to install multiple packages before installing Ruby:

apt-get install systemtap systemtap-sdt-dev

systemtap is the tool itself, systemtap-sdt-dev contains additional tools to write userspace probes. Especially, it contains the dtrace binary and headers, which we will need later when building Ruby.

apt-get install linux-headers-$(uname -r)

You kernel headers. systemtap needs them to compile it's probes. This is strictly necessary.

Optionally, if you want to trace things included in the kernel, you will need your kernels debug image. The process involves adding an additional repository and can be found here. Be aware that the debug image weights around 600MB and the download servers are rather slow. It took me more than an hour to download.

As a final, also optional step, add your user to the stapdev or stapusr group (both explained here):

$ adduser vagrant stapusr
$ adduser vagrant stapdev

Finally, there is this nice script that checks whether you got everything right.

Aside: the whole thing on Gentoo

$ sudo emerge =systemtap-2.4 --autounmask-write

Follow the instructions from there.

Compiling Ruby

If everything is set up, the only thing to do is to compile Ruby 2.0 or higher on your own. Either get the source, alternatively use rvm or ruby-install. DTrace support will be built automatically if available, but I recommend passing --enable-dtrace to make sure that the build process fails if anything is wrong.

$ ./configure --enable-dtrace
$ make
$ make install

So, let's see what we can do with this! First of all, let's list all available probes:

$ stap -L 'process("ruby").mark("*")'
process("ruby").mark("array__create") $arg1:long $arg2:long $arg3:long
...

Refer to the full list of probes for explanations.

Finally, tracing Ruby!

Instead of implementing DTraces scripting language, systemtap comes with it's own scripting language. The scripts allow us to make sense of the events passed by the probes.

For example, we can count all requires happening (example taken from http://avsej.net/2012/systemtap-and-ruby-20/):

global nmodules = 0;

probe process("ruby").mark("require__entry")
{
    module = kernel_string($arg1)
    file = kernel_string($arg2)
    line = $arg3
    printf("%s(%d) %s %s:%d required file `%s'\n", execname(), pid(), $$name, file, line, module)
    nmodules++;
}

probe end {
    printf("Total files: %d\n", nmodules);
    delete nmodules;
}

Let's run this on a simple Ruby program:

$ stap requires.stp -c 'ruby -e "exit"'
ruby(5191) require__entry ruby:0 required file `enc/encdb.so'
...
Total files: 27

And we will get all requires, neatly ordered (in this case, 27, 3 for the encoding system, the rest for rubygems).

Another interesting example is Aman Guptas method cache script (my fork, due to a bug in the original). It allows us to see when the method cache is cleared and which objects are involved. This probe is only available in Ruby 2.1 or higher, as well as the fine-grained method cache. This finally gives us insight into the what happens when pull certain metaprogramming tricks.

This time, let's try it differently. systemtap can also be attached to processes externally:

$ stap ruby_mcache.stp

Open another console and type:

$ irb

The first console should show lines like this:

irb(5329) method__cache__clear /home/vagrant/.rubies/ruby-2.1.0/lib/ruby/2.1.0/irb /extend-command.rb:198 cleared `Object'

For added fun: require 'ostruct'.

This allows us to attach and detach from currently running processes (or ones probably running in the future). Try closing and restarting stap while keeping the irb session.

Conclusion

I haven't tested systemtap in production, but I like what I see so far. I ran the whole thing on the Padrino codebase, to good results. I will definitely try to use it in development more and maybe move to production after a while.

Still, I found the setup process a bit involved, especially as there are many systems out there that don't come with the necessary kernel features. Check before you buy!

Credits

Code examples and lots of pointers from avsej. One code example from tmm1.