December 2012

この記事は Ruby の Advent Calendar に参加しようとして用意しましたが、間に合わなかったものです。
Ruby 2.0でdtrace対応が入りました。この機能はLinuxのsystemtapからもアクセス出来ます。でも、どこにもドキュメントがないのでいっちょ使い方を解説しようという、そういう趣旨の記事です。

まず、基本の使い方ですが、以下の様に

process(プロセス名).provider("ruby").mark(Rubyで定義されてるprobe名) で引っ掛けるイベント名を
記述して、tapscriptを記述します

ruby.stp
probe process("./ruby").provider("ruby").mark("find__require__entry")
{
printf("%s %s %d\n", user_string($arg1), user_string($arg2), $arg3)
}


test.rb
require "thread"


んで、 sudo stap TAPスクリプト -c 任意のコマンド として動かします。
(コマンドは全体を""で括らないとおかしくなるので注意)

$ sudo stap ../ruby.stp -c "./ruby --disable-gems ../test.rb"
enc/encdb.so ./ruby 0
enc/encdb.so 3
enc/trans/transdb.so 3
rubygems.rb 1
rbconfig /home/kosaki/local/ruby-trunk/lib/ruby/2.0.0/rubygems.rb 98
rubygems/compatibility /home/kosaki/local/ruby-trunk/lib/ruby/2.0.0/rubygems.rb 105
rubygems/defaults /home/kosaki/local/ruby-trunk/lib/ruby/2.0.0/rubygems.rb 107
rubygems/deprecate /home/kosaki/local/ruby-trunk/lib/ruby/2.0.0/rubygems.rb 108
rubygems/errors /home/kosaki/local/ruby-trunk/lib/ruby/2.0.0/rubygems.rb 109
rubygems/specification /home/kosaki/local/ruby-trunk/lib/ruby/2.0.0/rubygems.rb 1076
rubygems/version /home/kosaki/local/ruby-trunk/lib/ruby/2.0.0/rubygems/specification.rb 39
rubygems/requirement /home/kosaki/local/ruby-trunk/lib/ruby/2.0.0/rubygems/specification.rb 40
rubygems/version /home/kosaki/local/ruby-trunk/lib/ruby/2.0.0/rubygems/requirement.rb 11
rubygems/deprecate /home/kosaki/local/ruby-trunk/lib/ruby/2.0.0/rubygems/requirement.rb 12
rubygems/platform /home/kosaki/local/ruby-trunk/lib/ruby/2.0.0/rubygems/specification.rb 41
rubygems/deprecate /home/kosaki/local/ruby-trunk/lib/ruby/2.0.0/rubygems/platform.rb 1
rubygems/deprecate /home/kosaki/local/ruby-trunk/lib/ruby/2.0.0/rubygems/specification.rb 42
rubygems/exceptions /home/kosaki/local/ruby-trunk/lib/ruby/2.0.0/rubygems.rb 1079
rubygems/defaults/operating_system /home/kosaki/local/ruby-trunk/lib/ruby/2.0.0/rubygems.rb 1088
rubygems/defaults/ruby /home/kosaki/local/ruby-trunk/lib/ruby/2.0.0/rubygems.rb 1097
rubygems/core_ext/kernel_gem /home/kosaki/local/ruby-trunk/lib/ruby/2.0.0/rubygems.rb 1107
rubygems/core_ext/kernel_require /home/kosaki/local/ruby-trunk/lib/ruby/2.0.0/rubygems.rb 1108
thread /home/kosaki/local/ruby-trunk/lib/ruby/2.0.0/rubygems/core_ext/kernel_require.rb 45



おおー。見事に preludeコードから暗黙で呼ばれているencdb, transdbも含めてrequire関係がとれましたね。

では、使用出来るprobeはどこを見ればいいかというと、見るべきところは2つあって、1つはrubyソースコードのprobe.d、なにせコンパイル時に実際に使ってる定義だから嘘がない。もう1つは RubyのBTSにある wiki情報で簡単な解説つき。http://bugs.ruby-lang.org/projects/ruby/wiki/DTraceProbes

さて、ここでこの記事を終わらせてしまってもいいのですが、もうちょっと頑張りましょう。このままではちょっと使いにくいですよね。

まず、すぐ気づくダメな点は probe.d に引数の情報がないので、見ても参考にならなさすぎなのです。こんな感じ

https://github.com/ruby/ruby/blob/afb02bbe92e55f877d50ed8705c95a41d541458d/probes.d

#include "vm_opts.h"

provider ruby {
probe method__entry(const char *, const char *, const char *, int);
probe method__return(const char *, const char *, const char *, int);

probe cmethod__entry(const char *, const char *, const char *, int);
probe cmethod__return(const char *, const char *, const char *, int);

probe require__entry(const char *, const char *, int);
probe require__return(const char *);

probe find__require__entry(const char *, const char *, int);
probe find__require__return(const char *, const char *, int);

probe load__entry(const char *, const char *, int);
probe load__return(const char *);

probe raise(const char *, const char *, int);

probe object__create(const char *, const char *, int);
probe array__create(long, const char *, int);
probe hash__create(long, const char *, int);
probe string__create(long, const char *, int);

probe parse__begin(const char *, int);
probe parse__end(const char *, int);

#if VM_COLLECT_USAGE_DETAILS
probe insn(const char *);
probe insn__operand(const char *, const char *);
#endif

probe gc__mark__begin();
probe gc__mark__end();
probe gc__sweep__begin();
probe gc__sweep__end();
};

#pragma D attributes Stable/Evolving/Common provider ruby provider
#pragma D attributes Stable/Evolving/Common provider ruby module
#pragma D attributes Stable/Evolving/Common provider ruby function
#pragma D attributes Evolving/Evolving/Common provider ruby name
#pragma D attributes Evolving/Evolving/Common provider ruby args


実はDTraceの仕様としては引数名は書けるけども無視されるという仕様なので、書いてしまいましょう。
こんな感じ。

#include "vm_opts.h"

provider ruby {
probe method__entry(const char *classname, const char *methodname, const char *sourcefile, int sourceline);
probe method__return(const char *classname, const char *methodname, const char *sourcefile, int sourceline);

probe cmethod__entry(const char *classname, const char *methodname, const char *sourcefile, int sourceline);
probe cmethod__return(const char *classname, const char *methodname, const char *sourcefile, int sourceline);

probe require__entry(const char *filename, const char *sourcefile, int sourceline);
probe require__return(const char *filename);

probe find__require__entry(const char *filename, const char *sourcefile, int sourceline);
probe find__require__return(const char *filename, const char *sourcefile, int sourceline);

probe load__entry(const char *filename, const char *sourcefile, int sourceline);
probe load__return(const char *filename);

probe raise(const char *classname, const char *sourcefile, int sourceline);

probe object__create(const char *classname, const char *sourcefile, int sourceline);
probe array__create(long capacity, const char *sourcefile, int sourceline);
probe hash__create(long size, const char *sourcefile, int sourceline);
probe string__create(long length, const char *sourcefile, int sourceline);

probe parse__begin(const char *sourcefile, int sourceline);
probe parse__end(const char *sourcefile, int sourceline);

#if VM_COLLECT_USAGE_DETAILS
probe insn(const char *insns_name);
probe insn__operand(const char *val, const char *insns_name);
#endif

probe gc__mark__begin();
probe gc__mark__end();
probe gc__sweep__begin();
probe gc__sweep__end();
};

#pragma D attributes Stable/Evolving/Common provider ruby provider
#pragma D attributes Stable/Evolving/Common provider ruby module
#pragma D attributes Stable/Evolving/Common provider ruby function
#pragma D attributes Evolving/Evolving/Common provider ruby name
#pragma D attributes Evolving/Evolving/Common provider ruby args


github にもアップしといた。

https://github.com/kosaki/ruby/blob/b93717702e3db5d9a6c900afbb61422ef4970f89/probes.d

んで、ここまであれば、systemtapのライブラリ(tapsetといいます)が自動生成できます。こんな感じ

#!/usr/bin/ruby
# -*- coding: us-ascii -*-

def set_argument (arg, nth)
# remove C style type info
arg.gsub!(/.+ (.+)/, '\1')

# use user_string if arg is a pointer
if (arg[0,1] == "*")
"#{arg[1, 9999]} = user_string($arg#{nth})"
else
"#{arg} = $arg#{nth}"
end
end

ruby_path = "./ruby"

text = ARGF.read

# remove preprocessor directives
text.gsub!(/^#.*$/, '')

# remove provider name
text.gsub!(/^provider ruby \{/, "")
text.gsub!(/^\};/, "")

# probename()
text.gsub!(/probe (.+)\( *\);/) {
probe_name = $1
probe = <<-End
probe #{probe_name} = process("ruby").provider("ruby").mark("#{probe_name}")
{
}
End
}

# probename(arg1)
text.gsub!(/ *probe (.+)\(([^,)]+)\);/) {
probe_name = $1
arg1 = $2

probe = <<-End
probe #{probe_name} = process("ruby").provider("ruby").mark("#{probe_name}")
{
#{set_argument(arg1, 1)}
}
End
}

# probename(arg1, arg2)
text.gsub!(/ *probe (.+)\(([^,)]+),([^,)]+)\);/) {
probe_name = $1
arg1 = $2
arg2 = $3

probe = <<-End
probe #{probe_name} = process("#{ruby_path}").provider("ruby").mark("#{probe_name}")
{
#{set_argument(arg1, 1)}
#{set_argument(arg2, 2)}
}
End
}

# probename(arg1, arg2, arg3)
text.gsub!(/ *probe (.+)\(([^,)]+),([^,)]+),([^,)]+)\);/) {
probe_name = $1
arg1 = $2
arg2 = $3
arg3 = $4

probe = <<-End
probe #{probe_name} = process("#{ruby_path}").provider("ruby").mark("#{probe_name}")
{
#{set_argument(arg1, 1)}
#{set_argument(arg2, 2)}
#{set_argument(arg3, 3)}
}
End
}

# probename(arg1, arg2, arg3, arg4)
text.gsub!(/ *probe (.+)\(([^,)]+),([^,)]+),([^,)]+),([^,)]+)\);/) {
probe_name = $1
arg1 = $2
arg2 = $3
arg3 = $4
arg4 = $5

probe = <<-End
probe #{probe_name} = process("#{ruby_path}").provider("ruby").mark("#{probe_name}")
{
#{set_argument(arg1, 1)}
#{set_argument(arg2, 2)}
#{set_argument(arg3, 3)}
#{set_argument(arg4, 4)}
}
End
}

print text


おなじくgithubにUpしといた

https://github.com/kosaki/ruby/commit/0d2653e849d5352d8287e97bce053c07f6e84384

これで以下のようなライブラリが生成される

$ tool/gen_ruby_tapset.rb probes.d > ./ruby-tapset.stp



ruby-tapset.stp

probe method__entry = process("./ruby").provider("ruby").mark("method__entry")
{
classname = user_string($arg1)
methodname = user_string($arg2)
sourcefile = user_string($arg3)
sourceline = $arg4
}

probe method__return = process("./ruby").provider("ruby").mark("method__return")
{
classname = user_string($arg1)
methodname = user_string($arg2)
sourcefile = user_string($arg3)
sourceline = $arg4
}


probe cmethod__entry = process("./ruby").provider("ruby").mark("cmethod__entry")
{
classname = user_string($arg1)
methodname = user_string($arg2)
sourcefile = user_string($arg3)
sourceline = $arg4
}

probe cmethod__return = process("./ruby").provider("ruby").mark("cmethod__return")
{
classname = user_string($arg1)
methodname = user_string($arg2)
sourcefile = user_string($arg3)
sourceline = $arg4
}


probe require__entry = process("./ruby").provider("ruby").mark("require__entry")
{
filename = user_string($arg1)
sourcefile = user_string($arg2)
sourceline = $arg3
}

probe require__return = process("ruby").provider("ruby").mark("require__return")
{
filename = user_string($arg1)
}


probe find__require__entry = process("./ruby").provider("ruby").mark("find__require__entry")
{
filename = user_string($arg1)
sourcefile = user_string($arg2)
sourceline = $arg3
}

probe find__require__return = process("./ruby").provider("ruby").mark("find__require__return")
{
filename = user_string($arg1)
sourcefile = user_string($arg2)
sourceline = $arg3
}


probe load__entry = process("./ruby").provider("ruby").mark("load__entry")
{
filename = user_string($arg1)
sourcefile = user_string($arg2)
sourceline = $arg3
}

probe load__return = process("ruby").provider("ruby").mark("load__return")
{
filename = user_string($arg1)
}


probe raise = process("./ruby").provider("ruby").mark("raise")
{
classname = user_string($arg1)
sourcefile = user_string($arg2)
sourceline = $arg3
}


probe object__create = process("./ruby").provider("ruby").mark("object__create")
{
classname = user_string($arg1)
sourcefile = user_string($arg2)
sourceline = $arg3
}

probe array__create = process("./ruby").provider("ruby").mark("array__create")
{
capacity = $arg1
sourcefile = user_string($arg2)
sourceline = $arg3
}

probe hash__create = process("./ruby").provider("ruby").mark("hash__create")
{
size = $arg1
sourcefile = user_string($arg2)
sourceline = $arg3
}

probe string__create = process("./ruby").provider("ruby").mark("string__create")
{
length = $arg1
sourcefile = user_string($arg2)
sourceline = $arg3
}


probe parse__begin = process("./ruby").provider("ruby").mark("parse__begin")
{
sourcefile = user_string($arg1)
sourceline = $arg2
}

probe parse__end = process("./ruby").provider("ruby").mark("parse__end")
{
sourcefile = user_string($arg1)
sourceline = $arg2
}



probe insn = process("ruby").provider("ruby").mark("insn")
{
insns_name = user_string($arg1)
}

probe insn__operand = process("./ruby").provider("ruby").mark("insn__operand")
{
val = user_string($arg1)
insns_name = user_string($arg2)
}



probe gc__mark__begin = process("ruby").provider("ruby").mark("gc__mark__begin")
{
}

probe gc__mark__end = process("ruby").provider("ruby").mark("gc__mark__end")
{
}

probe gc__sweep__begin = process("ruby").provider("ruby").mark("gc__sweep__begin")
{
}

probe gc__sweep__end = process("ruby").provider("ruby").mark("gc__sweep__end")
{
}


$arg1 みたいなくそ使いにくい引数を probes.d の記述にもとづいて sourcefile とか sourcelineとか
いった変数に変換してくれる。これによって最初のスクリプトは以下のようにも書けるようになる

sudo stap -I. ../ruby2.stp -c "./ruby ../test.rb"



-I はさきほど作った ruby-tapset.stp があるディレクトリを指定すること。SystemTapのデフォルトサーチパス
(普通は /usr/share/systemtap/tapset/とかそのへん)に入れてしまえば不要だけどおすすめしない。

ruby2.stp

probe find__require__entry {
printf ("%s %s %d\n", filename, sourcefile, sourceline)
}


probe名からプロセス名とプロバイダ名が消え、引数アクセスもuser_string($arg1) といった記述が
消えているのが分かるだろうか。

言い換えると、最初の例が2.0での書き方、最後の例が2.1での書き方(の候補として検討中のもの)というわけである。

おしまい

まつもとゆきひろ名言集。

http://bugs.ruby-lang.org/projects/ruby/wiki/DevelopersMeeting20121210

[16:08:17] And I want to make sure as long as I live on this mortal
state, you need my approval before adding something as official
Ruby
[16:08:30] lol
[16:08:34] and don't try to kill me ;-)

ぼくが生きてる間は、Rubyの言語仕様に何かを加える前に僕の承認を得る必要があるよ。
でも、だからといって僕を殺そうとはしないでね

↑このページのトップヘ