SystemTap and ruby 2.0

As you might know already that the second preview of ruby 2.0 interpreter has been released recently. And there is dtrace support among other cool features. I’m not fan of Solaris like others and use just Debian GNU/Linux. But linux still has a tool which intended to solve similar tasks: SystemTap. And it is possible to use it with applications where dtrace support enabled as said on their wiki:

These calls are source compatible with dtrace on other platforms, so the same source code compiled on a system that doesn’t have systemtap available, but does have a dtrace implementation will be able to discover the same probe locations.

Let check if it is true for ruby.

Setup

To use probepoints in ruby interpreter you should have a kernel with UPROBES enabled:

~ $ grep  UPROBES /boot/config-*
/boot/config-3.6-trunk-amd64:CONFIG_ARCH_SUPPORTS_UPROBES=y
/boot/config-3.6-trunk-amd64:CONFIG_UPROBES=y

At this moment debian doesn’t ship kernels with this option, but building custom kernel isn’t a complex task these days.

And of course SystemTap itself. I had an runtime compilation issue with version from Debian repository (version 1.7-1+b1). The problem reported as Bug 14220 and fixed in e14ac0e. If you have similar issues, just install recent stable version.

So given you have all prerequisites installed, just download and build the ruby. It will automatically detect and configure SystemTap probes. To verify installation, let just ask it to output all accessible probes:

~ $ stap -L 'process("/usr/local/bin/ruby").mark("*")'
process("/usr/local/bin/ruby").mark("array__create") $arg1:long $arg2:long $arg3:long
process("/usr/local/bin/ruby").mark("cmethod__entry") $arg1:long $arg2:long $arg3:long $arg4:long
process("/usr/local/bin/ruby").mark("cmethod__return") $arg1:long $arg2:long $arg3:long $arg4:long
process("/usr/local/bin/ruby").mark("find__require__entry") $arg1:long $arg2:long $arg3:long
process("/usr/local/bin/ruby").mark("find__require__return") $arg1:long $arg2:long $arg3:long
process("/usr/local/bin/ruby").mark("gc__mark__begin")
process("/usr/local/bin/ruby").mark("gc__mark__end")
process("/usr/local/bin/ruby").mark("gc__sweep__begin")
process("/usr/local/bin/ruby").mark("gc__sweep__end")
process("/usr/local/bin/ruby").mark("hash__create") $arg1:long $arg2:long $arg3:long
process("/usr/local/bin/ruby").mark("load__entry") $arg1:long $arg2:long $arg3:long
process("/usr/local/bin/ruby").mark("load__return") $arg1:long
process("/usr/local/bin/ruby").mark("method__entry") $arg1:long $arg2:long $arg3:long $arg4:long
process("/usr/local/bin/ruby").mark("method__return") $arg1:long $arg2:long $arg3:long $arg4:long
process("/usr/local/bin/ruby").mark("object__create") $arg1:long $arg2:long $arg3:long
process("/usr/local/bin/ruby").mark("parse__begin") $arg1:long $arg2:long
process("/usr/local/bin/ruby").mark("parse__end") $arg1:long $arg2:long
process("/usr/local/bin/ruby").mark("raise") $arg1:long $arg2:long $arg3:long
process("/usr/local/bin/ruby").mark("require__entry") $arg1:long $arg2:long $arg3:long
process("/usr/local/bin/ruby").mark("require__return") $arg1:long
process("/usr/local/bin/ruby").mark("string__create") $arg1:long $arg2:long $arg3:long

Okay, looks like it does define some useful probepoints. Original probes.d could be found in ruby repository. Lets see if we can extract something useful.

Counting ‘require’ in rails console

For example, determine how many files required when rails starts console. To count all of then we will trace all points marked require__entry. This is the simple script which does the job:

global nmodules = 0;

probe process("/usr/local/bin/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;
}

As you can see the script just outputs the required file name and the place where require command was called. And then outputs total number of the calls. The partial output (full):

$ sudo stap rails-console-require.stp -c 'script/rails console'
ruby(420) require__entry ruby:0 required file `enc/encdb.so'
ruby(420) require__entry <internal:enc/prelude>:3 required file `enc/encdb.so'
ruby(420) require__entry <internal:enc/prelude>:3 required file `enc/trans/transdb.so'
ruby(420) require__entry <internal:gem_prelude>:1 required file `rubygems.rb'
... snip ...
Total files: 966

“top” for ruby functions

Another interesting example taken from SystemTap wiki and slightly modified. It displays list of recent function calls for all ruby processes in the system. It also refresh the list each seconds.

global fn_calls;

probe process("/usr/local/bin/ruby").mark("method__entry")
{
    signature = sprintf("%s#%s", kernel_string($arg1), kernel_string($arg2))
    source = sprintf("%s:%d", kernel_string($arg3), $arg4)
    fn_calls[pid(), signature, source] <<< 1;
}

probe process("/usr/local/bin/ruby").mark("cmethod__entry")
{
    signature = sprintf("%s.%s", kernel_string($arg1), kernel_string($arg2))
    source = sprintf("%s:%d", kernel_string($arg3), $arg4)
    fn_calls[pid(), signature, source] <<< 1;
}

probe timer.ms(1000) {
    ansi_clear_screen()
    printf("%-6s %-6s %-30s %-40s\n",
            "PID", "CALLS", "FUNCTION", "FILENAME")
    foreach ([pid, signature, source] in fn_calls- limit 20) {
        printf("%-6d %-6d %-40s %-80s\n",
                pid, @count(fn_calls[pid, signature, source]),
                signature, source);
    }
    delete fn_calls;
}

The script collects stats and every seconds draw them in the table. In this form it might be not very useful, but it is easy to modify the script for your needs. Here is the sample output. I’m running with option -DMAXMAPENTRIES=10000 to extend the size of fn_calls map.

$ sudo stap -DMAXMAPENTRIES=10000 rubytop.stp
PID    CALLS  FUNCTION                           FILENAME
3900   29598  IO.write                           /usr/local/lib/ruby/2.0.0/irb/ext/save-history.rb:92
4468   14799  String.chomp                       /usr/local/lib/ruby/2.0.0/irb/ext/save-history.rb:79
4468   2223   Module.===                         /usr/local/lib/ruby/2.0.0/rubygems/specification.rb:1725
4468   2106   BasicObject.==                     /usr/local/lib/ruby/2.0.0/rubygems/specification.rb:1725
4468   2106   Kernel.===                         /usr/local/lib/ruby/2.0.0/rubygems/specification.rb:1725
4468   837    File.file?                         /usr/local/lib/ruby/2.0.0/rubygems/specification.rb:1371
4468   780    Gem::Specification#default_value   /usr/local/lib/ruby/2.0.0/rubygems/specification.rb:1437
4468   780    Symbol.to_s                        /usr/local/lib/ruby/2.0.0/rubygems/specification.rb:1729
4468   780    Kernel.instance_variable_set       /usr/local/lib/ruby/2.0.0/rubygems/specification.rb:1729
4468   702    Kernel.initialize_dup              /usr/local/lib/ruby/2.0.0/rubygems/specification.rb:1726
4468   702    Kernel.dup                         /usr/local/lib/ruby/2.0.0/rubygems/specification.rb:1726
4468   468    Array.initialize_copy              /usr/local/lib/ruby/2.0.0/rubygems/specification.rb:1726
4468   390    Kernel.instance_variable_set       /usr/local/lib/ruby/2.0.0/rubygems/specification.rb:1719
4468   390    Symbol.to_s                        /usr/local/lib/ruby/2.0.0/rubygems/specification.rb:1719
4468   309    Gem#suffixes                       /usr/local/lib/ruby/2.0.0/rubygems.rb:837
4468   289    Hash.[]=                           /usr/local/lib/ruby/2.0.0/rubygems.rb:962
4468   282    RbConfig#expand                    /usr/local/lib/ruby/2.0.0/x86_64-linux/rbconfig.rb:221
4468   282    String.gsub                        /usr/local/lib/ruby/2.0.0/x86_64-linux/rbconfig.rb:222
4468   279    Enumerable.any?                    /usr/local/lib/ruby/2.0.0/rubygems/specification.rb:1371
4468   279    Array.each                         /usr/local/lib/ruby/2.0.0/rubygems/specification.rb:1371

Conclusion

This post shows only the tip of the iceberg. I really recommend you to take a look at SystemTap, because it is very powerful tool which might tell you a lot about your system and application you are running. Also note that using dtrace/SystemTap doesn’t introduce a lot of overhead in the binary, therefore you can inspect your production deployments too.

More links: