Johan Sørensen

A quick stroll through DTrace

DTrace has been getting a lot more press recently, since Apple has ported it to Leopard, it’s also been getting a lot of mentions in the Ruby community since Apple has included the DTrace providers for it. Yet, surprisingly few seem to actually use DTrace much (yours truly included really). So here’s a short intro to DTrace and D (not to be confused with the other D).

The essence of DTrace is probes, these are event handlers that fire whenever their particular event happens, you can then register interest in these probe events with a particular action, like printing it, aggregating usage counts and whatever other way you decide to use this information.

Since there’s over 450 000 probes in Leopard, there’s a lot of information you can gather and the trick is to start at a high level and drill down — “hmm, why are there 800 syscalls? hmm, what function caused this? what is it writing? what did it do right before it made the call to write()?” and so, one question leads to next with DTrace.

We can get a list of all the probes currently available on our system, by running dtrace -l, or drilling down with the -P flag


$ sudo dtrace -l | wc -l
  454839
$ sudo dtrace -l -P syscall | head -5
   ID   PROVIDER            MODULE                          FUNCTION NAME
17590    syscall                                             syscall entry
17591    syscall                                             syscall return
17592    syscall                                                exit entry
17593    syscall                                                exit return

So let’s start with asking what syscalls are currently being made by all the applications currently running (unless otherwise told to, DTrace will listen forever so finish it with ctrl+c):


$ sudo dtrace -n 'syscall:::entry{trace(execname)}'
dtrace: description 'syscall:::entry' matched 427 probes
CPU     ID                    FUNCTION:NAME
  1  17698                      ioctl:entry   dtrace                           
  1  17698                      ioctl:entry   dtrace                                             
  1  17682                  sigaction:entry   dtrace                           
  1  17682                  sigaction:entry   dtrace                           
  1  17682                  sigaction:entry   dtrace                           
  1  18258           __semwait_signal:entry   Little Snitch U
  1  17686                sigprocmask:entry   WindowServer                     
  1  17696                sigaltstack:entry   WindowServer

The probes are specified in a provider:module:function:name format, with an empty entry being a wildcard, so asking for all syscall function entries would mean asking for syscall:::entry, we could get all write syscall entries by asking for syscall::write:entry and its (function) returns by asking for syscall::write:return for the write() function.

So the above output isn’t all that useful since it’s too much information for us puny humans to parse effectively. Luckily DTrace provides means of aggregating things with the @[key(s)] notation, where key(s) is an arbitary comma-seperated list of D expressions and the value is an aggregating function like count() that simply counts the number of times something happens. So to aggregate the number of syscalls on the application name we can use execname:


$ sudo dtrace -n 'syscall:::entry{ @[execname] = count() }'
dtrace: description 'syscall:::entry' matched 427 probes
^C

  DirectoryServic                  2
  Finder                           2
...
  WindowServer                    46
  launchd                         48
  natd                            81
  SystemUIServer                 113
  Adium                          131
  ruby                           356
  pmTool                         584

We can even expand this to see what probe function is being called using the probefunc expression:


$ sudo dtrace -n 'syscall:::entry{ @[execname, probefunc] = count() }'
dtrace: description 'syscall:::entry' matched 427 probes
^C

 Finder                   kevent                    1
 Safari                   gettimeofday              1
 Terminal                 mmap                      1
...
 ruby                     select                   10
 dtrace                   ioctl                    14
 WindowServer             sigaltstack              15
 WindowServer             sigprocmask              15
 ruby                     __semwait_signal        141
 pmTool                   __sysctl                291

Ruby seems to be waiting in a semaphore/thread, lets take a look at its current stacktrace. We can do this by specifying a predicate for our probing, think of it as a conditional. So, by only registering interest in a proble if the execname == "ruby" predicate is met, we print the stack:


$ sudo dtrace -n 'syscall:::entry/execname == "ruby"/{ ustack() }'
dtrace: description 'syscall:::entry' matched 427 probes
CPU     ID                    FUNCTION:NAME
  0  18258           __semwait_signal:entry 
              libSystem.B.dylib`__semwait_signal+0xa
              libruby.1.dylib`rb_thread_group+0x29f
              libSystem.B.dylib`_pthread_start+0x141
              libSystem.B.dylib`thread_start+0x22

  0  18258           __semwait_signal:entry 
              libSystem.B.dylib`__semwait_signal+0xa
              libruby.1.dylib`rb_thread_group+0x29f
              libSystem.B.dylib`_pthread_start+0x141
              libSystem.B.dylib`thread_start+0x22

Yep, looks like an rb_thread allright. And that makes perfect sense since I had a mongrel running there in the background.

Let’s take a look at what Ruby providers are available (you need a running ruby process to see this):


$ sudo dtrace -l -P "ruby*"          
   ID   PROVIDER            MODULE             FUNCTION NAME
19708  ruby48398   libruby.1.dylib             rb_call0 function-entry
19709  ruby48398   libruby.1.dylib             rb_call0 function-return
19710  ruby48398   libruby.1.dylib      garbage_collect gc-begin
19711  ruby48398   libruby.1.dylib      garbage_collect gc-end
19712  ruby48398   libruby.1.dylib              rb_eval line
19713  ruby48398   libruby.1.dylib         rb_obj_alloc object-create-done
19714  ruby48398   libruby.1.dylib         rb_obj_alloc object-create-start
19715  ruby48398   libruby.1.dylib      garbage_collect object-free
19716  ruby48398   libruby.1.dylib           rb_longjmp raise
19717  ruby48398   libruby.1.dylib              rb_eval rescue
19718  ruby48398   libruby.1.dylib    ruby_dtrace_probe ruby-probe

We wildcard the ruby provider name since they’re per app specific (the 48398 part is the PID). Which is cool if you’re running more than one ruby process, so you could poke around figureing out why one is eating cpu and the other isn’t (Here’s an explanation of the Ruby probes). Let’s see what method calls are being used the most in a typical Rails request:


$ sudo dtrace -n 'ruby*:::function-entry{ @[copyinstr(arg0), copyinstr(arg1)] = count()  }'
dtrace: description 'ruby*:::function-entry' matched 1 probe
^C
...                                                                       
	Array            pop                                     24
  File::Stat       size                                    24
  Inflector        inflections                             24
  Inflector        inflections_without_route_reloading     24
...                                                    
	Hash             []                                     557
  Hash             []=                                    623
  String           to_s                                   723
  Hash             key?                                  4379

Here we make the aggregation list keys out of the class and the method name, which is specified as argN. args[] is an array of arguments for the probe, argN is a shortcut for that array, in this case the arguments are what the probe made them up to be (class and method name, arg 2 and 3 are sourcefile and line number), but it could also be the arguments for a function call. copyinstr() simply means “make a string out of this pointer reference”.

Back to poking around, Hash lookups and String#to_s isn’t all that interesting for us right now, but I’m kinda curious about what it is stat()‘ing 24 times for a request? Let’s try and find out:


sudo dtrace -n 'ruby*:::function-entry/copyinstr(arg0) == "File::Stat" && copyinstr(arg1) == "size"/{ ustack()  }'
dtrace: description 'ruby*:::function-entry' matched 1 probe
CPU     ID                    FUNCTION:NAME
  0  19708          rb_call0:function-entry 
              libruby.1.dylib`rb_eval_string_wrap+0x43f9
              libruby.1.dylib`rb_eval_string_wrap+0x5173
              libruby.1.dylib`rb_eval_string_wrap+0x23ee
              libruby.1.dylib`rb_eval_string_wrap+0x34ba
              libruby.1.dylib`rb_eval_string_wrap+0x1f53
              libruby.1.dylib`rb_eval_string_wrap+0x2dbb
              libruby.1.dylib`rb_eval_string_wrap+0x4d65
              libruby.1.dylib`rb_eval_string_wrap+0x5173
              libruby.1.dylib`rb_eval_string_wrap+0x23ee
              libruby.1.dylib`rb_eval_string_wrap+0x149d
              libruby.1.dylib`rb_eval_string_wrap+0x4d65
              libruby.1.dylib`rb_eval_string_wrap+0x5173
              libruby.1.dylib`rb_eval_string_wrap+0x23ee
              libruby.1.dylib`rb_eval_string_wrap+0x4d65
              libruby.1.dylib`rb_eval_string_wrap+0x5173
              libruby.1.dylib`rb_eval_string_wrap+0x23ee
              libruby.1.dylib`rb_thread_trap_eval+0x959
              libruby.1.dylib`rb_yield+0x21
              libruby.1.dylib`rb_ary_each+0x1e
              libruby.1.dylib`rb_eval_string_wrap+0x455f

  0  19708          rb_call0:function-entry 
              libruby.1.dylib`rb_eval_string_wrap+0x43f9
              libruby.1.dylib`rb_eval_string_wrap+0x5173
              libruby.1.dylib`rb_eval_string_wrap+0x23ee
              libruby.1.dylib`rb_eval_string_wrap+0x1f53
              libruby.1.dylib`rb_eval_string_wrap+0x4d65
              libruby.1.dylib`rb_eval_string_wrap+0x5173
              libruby.1.dylib`rb_eval_string_wrap+0x23ee
              libruby.1.dylib`rb_eval_string_wrap+0x149d
              libruby.1.dylib`rb_eval_string_wrap+0x4d65
              libruby.1.dylib`rb_eval_string_wrap+0x5173
              libruby.1.dylib`rb_eval_string_wrap+0x23ee
              libruby.1.dylib`rb_eval_string_wrap+0x4d65
              libruby.1.dylib`rb_eval_string_wrap+0x5173
              libruby.1.dylib`rb_eval_string_wrap+0x23ee
              libruby.1.dylib`rb_thread_trap_eval+0x959
              libruby.1.dylib`rb_yield+0x21
              libruby.1.dylib`rb_ary_each+0x1e
              libruby.1.dylib`rb_eval_string_wrap+0x455f
              libruby.1.dylib`rb_eval_string_wrap+0x5173
              libruby.1.dylib`rb_eval_string_wrap+0x23ee
                                                         24

By adding the predicate of our target class and method we get only what we’re interested in, and print the stack using ustack. Unfortunately this being Ruby it’s not all that useful to us, since it’s pretty much all rb_eval-inner-ruby-runtime-here-be-dragons-stuff (I would love a ustack helper for ruby, like there is for python), that doesn’t make much sense to us. I wonder which file it’s doing this in though?


$ sudo dtrace -n 'ruby*:::function-entry/copyinstr(arg0) == "File::Stat" && copyinstr(arg1) =="size"/{ printf("%s in %s", copyinstr(arg0),copyinstr(arg2))}'
  dtrace: description 'ruby*:::function-entry' matched 1 probe
File::Stat in [...]gems/mongrel-1.0.1/lib/mongrel/handlers.rb
File::Stat in [...]gems/mongrel-1.0.1/lib/mongrel/handlers.rb
File::Stat in [...]gems/mongrel-1.0.1/lib/mongrel/handlers.rb
# (output slightly truncated)

OK, so that tells us where this is happened, but not what it’s stat()‘ing, luckily File::Stat sounds like something that might be doing a syscall, and we have probes for that, here’s a script that matches up the ruby function-entry with looking at syscalls at the same time:


#!/usr/sbin/dtrace -s

#pragma D option quiet

ruby*:::function-entry
/copyinstr(arg0) == "File::Stat" && copyinstr(arg1) == "size"/
{
  self->interested = 1;
  self->rubymethod = copyinstr(arg1);
  self->rubyclass = copyinstr(arg0)
}

syscall::stat*:entry
/self->interested/
{
  printf("%s from %s#%s\n", copyinstr(arg0), self->rubyclass, self->rubymethod); 
}

By defining the variable interested whenever we’re in the function-entry we’re interested in, we can use that variable as a predicate for our syscall::stat*:entry (stat* is wildcarded because there’s things like stat64() as well), making it executable and running it we see:


$ chmod +x who_be_stattin.d	
$ sudo ./who_be_stattin.d 
RAILS_ROOT/public/favicon.ico from File::Stat#size
RAILS_ROOT/public/favicon.ico from File::Stat#size
RAILS_ROOT/public/favicon.ico from File::Stat#size
RAILS_ROOT/public/favicon.ico from File::Stat#size
RAILS_ROOT/public/favicon.ico from File::Stat#size
RAILS_ROOT/public/favicon.ico from File::Stat#size
RAILS_ROOT/public/javascripts/application.js from File::Stat#size
RAILS_ROOT/public/javascripts/application.js from File::Stat#size
RAILS_ROOT/public/javascripts/application.js from File::Stat#size
RAILS_ROOT/public/javascripts/application.js from File::Stat#size

Aha! It must be the Rails mongrel handler that checks the size of the asset files, before it sends them down the wire (it’s from a local ./script/console). Not so interesting after all, but at least we learnt a bit along the way.

Remember the ruby providers from up there? the ruby-probe one? That one is basically a plugin that lets you fire your very own probes in your app, using the (Apple shipped) DTracer class:


>> def with_my_probe &blk
>>   DTracer.fire("my-probe-entry")
>>   yield
>>   DTracer.fire("my-probe-return")
>>   end
=> nil
>> with_my_probe{ puts "moo" }


$ cat probe_my_probe.d
#!/usr/sbin/dtrace -s

ruby*:::ruby-probe
/copyinstr(arg0) == "my-probe-entry"/
{
  self->interested = 1;
}

syscall:::
/self->interested/
{
  /* default action is to just print it */
}

ruby*:::function-entry
/self->interested/
{
  printf("%s in %s", copyinstr(arg1), copyinstr(arg0));
}

ruby*:::ruby-probe
/copyinstr(arg0) == "my-probe-return"/
{
  self->interested = 0;
}

$ sudo ./probe_my_probe.d
dtrace: script './ruby_probe_test.d' matched 860 probes
CPU     ID                    FUNCTION:NAME
  1 454800          rb_call0:function-entry puts in Object
  1 454800          rb_call0:function-entry write in IO
  1  17598                      write:entry 
  1  17599                     write:return 
  1 454800          rb_call0:function-entry write in IO
  1  17598                      write:entry 
  1  17599                     write:return 
  1 454800          rb_call0:function-entry fire in Module

# (While running 'with_my_probe{ puts "foo" }' in irb)

But more on that later. In the meantime do go off exploring your OS and applications with DTrace, you’d be surprised how quickly you can loose an hour or two just by asking “why is that doing this here?”…


Comments:

  1. Harish Mallipeddi Says:


    Is there any place where I can find out more about the Python probes? Apart from that one article on a Sun engineer’s blog, I couldn’t really find more info on the Python ustack support. Apple shipped DTrace alright, but never packaged any proper documentation with it :(