DTrace and Ruby
It’s been an exciting few weeks for DTrace. The party got started with Wez Furlong’s new PHP DTrace provider at OSCON. Then Devon O’Dell announced that he was starting to work in earnest on a DTrace port to FreeBSD. And now, Rich Lowe has made available a prototype Ruby DTrace provider. To install this, grab Ruby 1.8.2, apply Rich’s patch, and run ./configure
with the --enable-dtrace
option. When you run the resulting ruby
, you’ll see two probes: function-entry
and function-return
. The arguments to these probes are as follows:
-
arg0
is the name of the class (a pointer to a string within Ruby) -
arg1
is the name of the method (also a pointer to a string within Ruby) -
arg2
is the name of the file containing the call site (again, a pointer to a string within Ruby) -
arg3
is the line number of the call site.
So if, for example, you’d like to know the classes and methods that are called in a particular Ruby script, you could do it with this simple D script:
#pragma D option quiet
ruby$target:::function-entry
{
@[copyinstr(arg0), copyinstr(arg1)] = count();
}
END
{
printf("%15s %30s %s\n", "CLASS", "METHOD", "COUNT");
printa("%15s %30s %@d\n", @);
}
To run this against the cal.rb
that ships in the sample
directory of Ruby, call the above script whatmethods.d
and run it this way:
# dtrace -s ./whatmethods.d -c "../ruby ./cal.rb"
August 2005
S M Tu W Th F S
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31
CLASS METHOD COUNT
Array <= 2
Hash each 2
Hash keys 2
Module attr 2
Module method_undefined 2
Module public 2
Rational coerce 2
Array + 3
Class civil_to_jd 3
Hash [] 3
Object collect 3
Array collect 4
Class inherited 4
Range each 4
String size 4
Module private_class_method 5
Object eval 5
Object require 5
String gsub 5
Class jd_to_wday 7
Class once 7
Date __8713__ 7
Date wday 7
Fixnum % 8
Array join 10
Hash []= 10
String + 10
Array each 11
NilClass to_s 11
Module alias_method 22
Module private 22
Symbol to_s 26
Module module_eval 28
Date mday 31
Object send 31
Date mon 42
Date __11105__ 43
Class jd_to_civil 45
Date succ 47
Class os? 48
Date + 49
Fixnum <= 49
String rjust 49
Class ajd_to_jd 50
Class clfloor 50
Date __10417__ 50
Integer to_i 50
Object Integer 50
Rational divmod 50
Rational to_i 50
Date 51
Date ajd 51
Date jd 51
Rational 51
Class new0 52
Date initialize 52
Integer to_r 52
Object singleton_method_added 67
Date civil 75
Symbol to_i 91
Float * 96
Float coerce 96
Fixnum / 97
Object frozen? 100
Rational - 104
Fixnum to_s 123
Array [] 141
Object class 150
Module method_added 154
Float / 186
Module === 200
Rational / 204
Rational + 248
Float floor 282
Fixnum << 306
Class reduce 356
Integer gcd 356
Object Rational 356
Fixnum + 414
Class new! 610
Rational initialize 610
Class new 612
Fixnum abs 712
Fixnum div 762
Fixnum * 1046
Fixnum 1970
Fixnum - 2398
Object kind_of? 2439
Fixnum >> 4698
Fixnum [] 7689
Fixnum == 11436
This may leave us with many questions. For example, there are a couple of calls to construct new objects – where are they coming from? To answer that question:
#pragma D option quiet
ruby$target:::function-entry
/copyinstr(arg1) == "initialize"/
{
@[copyinstr(arg0), copyinstr(arg2), arg3] = count();
}
END
{
printf("%-10s %-40s %-10s %s\n", "CLASS",
"INITIALIZED IN FILE", "AT LINE", "COUNT");
printa("%-10s %-40s %-10d %@d\n", @);
}
Calling the above whereinit.d
, we can run it in a similar manner:
# dtrace -s ./whereinit.d -c "../ruby ./cal.rb"
August 2005
S M Tu W Th F S
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31
CLASS INITIALIZED IN FILE AT LINE COUNT
Cal ./cal.rb 144 1
Date /usr/local/lib/ruby/1.8/date.rb 593 1
Date /usr/local/lib/ruby/1.8/date.rb 703 1
Date /usr/local/lib/ruby/1.8/date.rb 916 1
Time /usr/local/lib/ruby/1.8/date.rb 702 1
Date /usr/local/lib/ruby/1.8/date.rb 901 49
Rational /usr/local/lib/ruby/1.8/rational.rb 374 610
Looking at the Date
class, it’s interesting to look at line 901 of file /usr/local/lib/ruby/1.8/date.rb
:
897 # If +n+ is not a Numeric, a TypeError will be thrown. In
898 # particular, two Dates cannot be added to each other.
899 def + (n)
900 case n
901 when Numeric; return self.class.new0(@ajd + n, @of, @sg)
902 end
903 raise TypeError, 'expected numeric'
904 end
This makes sense: we’re initializing new Date
instances in the +
method for Date
. And where are those coming from? It’s not hard to build a script that will tell us the file and line for the call site of an arbitrary class and method:
#pragma D option quiet
ruby$target:::function-entry
/copyinstr(arg0) == $$1 && copyinstr(arg1) == $$2/
{
@[copyinstr(arg2), arg3] = count();
}
END
{
printf("%-40s %-10s %s\n", "FILE", "LINE", "COUNT");
printa("%-40s %-10d %@d\n", @);
}
For this particular example (Date#+()
), call the above wherecall.d
and run it this way:
# dtrace -s ./wherecall.d "Date" "+" -c "../ruby ./cal.rb"
August 2005
S M Tu W Th F S
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31
FILE LINE COUNT
./cal.rb 102 2
./cal.rb 60 6
./cal.rb 63 41
And looking at the indicated lines in cal.rb:
55 def pict(y, m)
56 d = (1..31).detect{|d| Date.valid_date?(y, m, d, @start)}
57 fi = Date.new(y, m, d, @start)
58 fi -= (fi.jd - @k + 1) % 7
59
60 ve = (fi..fi + 6).collect{|cu|
61 %w(S M Tu W Th F S)[cu.wday]
62 }
63 ve += (fi..fi + 41).collect{|cu|
64 if cu.mon == m then cu.send(@da) end.to_s
65 }
66
So this is doing exactly what we would expect, given the code. Now, if we were interested in making this perform a little better, we might be interested to know the work that is being induced by Date#+()
. Here’s a script that reports the classes and methods called by a given class/method – and its callees:
#pragma D option quiet
ruby$target:::function-entry
/copyinstr(arg0) == $$1 && copyinstr(arg1) == $$2/
{
self->date = 1;
}
ruby$target:::function-entry
/self->date/
{
@[strjoin(strjoin(copyinstr(arg0), "#"),
copyinstr(arg1))] = count();
}
ruby$target:::function-return
/copyinstr(arg0) == $$1 && copyinstr(arg1) == $$2/
{
self->date = 0;
ndates++;
}
END
{
normalize(@, ndates);
printf("Each call to %s#%s() induced:\n\n", $$1, $$2);
printa("%@8d call(s) to %s()\n", @);
}
Calling the above whatcalls.d
, we can answer the question about Date#+()
this way:
# dtrace -s ./whatcalls.d "Date" "+" -c "../ruby ./cal.rb"
August 2005
S M Tu W Th F S
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31
Each call to Date#+() induced:
1 call(s) to Class#new0()
1 call(s) to Class#reduce()
1 call(s) to Date#+()
1 call(s) to Date#initialize()
1 call(s) to Fixnum#+()
1 call(s) to Fixnum#<<()
1 call(s) to Integer#gcd()
1 call(s) to Module#===()
1 call(s) to Object#Rational()
1 call(s) to Object#class()
2 call(s) to Class#new()
2 call(s) to Class#new!()
2 call(s) to Fixnum#abs()
2 call(s) to Fixnum#div()
2 call(s) to Rational#+()
2 call(s) to Rational#initialize()
3 call(s) to Fixnum#*()
3 call(s) to Fixnum#()
23 call(s) to Fixnum#>>()
37 call(s) to Fixnum#[]()
52 call(s) to Fixnum#==()
That’s a lot of work for something that should be pretty simple! Indeed, it’s counterintuitive that, say, Integer#gcd()
would be called from Date#+()
– and it certainly seems suboptimal. I’ll leave further exploration into this as an exercise to the reader, but suffice it to say that this has to do with the use of a rational number in the Date
class – the elimination of which would elminate most of the above calls and presumably greatly improve the performance of Date#+()
.
Now, Ruby aficionados may note that some of the above functionality has been available in Ruby by setting the set_trace_func
function (upon which the Ruby profiler is implemented). While that’s true (to a point – the set_trace_func
seems to be a pretty limited mechanism), the Ruby DTrace provider is nonetheless a great lurch forward for Ruby developers: it allows developers to use DTrace-specific constructs like aggregations and thread-local variables to hone in on a problem; it allows Ruby-related work performed lower in the stack (e.g., in the I/O subsystem, CPU dispatcher or network stack) to be connected to the Ruby code inducing it; it allows a running Ruby script to be instrumented (and reinstrumented) without stopping or restarting it; and it allows multiple, disjoint Ruby scripts to be coherently observed and understood as a single entity. More succinctly, it’s just damned cool. So thanks to Rich for developing the prototype provider – and if you’re a Ruby developer, enjoy!