Rubyで利用できるUSDTは probes.d
というファイルに定義されている。
USDTとは:
以下の記事はbpftraceとUSDTを理解している前提で書いていきます。
Rubyにおけるprobeの定義箇所
probes.d
は以下のような記法。
provider ruby { /* ruby:::method-entry(classname, methodname, filename, lineno); This probe is fired just before a method is entered. * `classname` name of the class (a string) * `methodname` name of the method about to be executed (a string) * `filename` the file name where the method is _being called_ (a string) * `lineno` the line number where the method is _being called_ (an int) */ probe method__entry(const char *classname, const char *methodname, const char *filename, int lineno); /* ruby:::method-return(classname, methodname, filename, lineno); This probe is fired just after a method has returned. The arguments are the same as "ruby:::method-entry". */ probe method__return(const char *classname, const char *methodname, const char *filename, int lineno); //... }
Linux環境の場合、 dtrace
というコマンド(systemtap-sdt-dev
あるいはそれに準じたパッケージと一緒に入る)を噛ませることでCのヘッダファイルに変換される。
$ dtrace -h -s probes.d -o /dev/stdout | head -24 /* Generated by the Systemtap dtrace wrapper */ #define _SDT_HAS_SEMAPHORES 1 #define STAP_HAS_SEMAPHORES 1 /* deprecated */ #include <sys/sdt.h> /* RUBY_METHOD_ENTRY ( const char *classname, const char *methodname, const char *filename, int lineno ) */ #if defined STAP_SDT_V1 #define RUBY_METHOD_ENTRY_ENABLED() __builtin_expect (method__entry_semaphore, 0) #define ruby_method__entry_semaphore method__entry_semaphore #else #define RUBY_METHOD_ENTRY_ENABLED() __builtin_expect (ruby_method__entry_semaphore, 0) #endif __extension__ extern unsigned short ruby_method__entry_semaphore __attribute__ ((unused)) __attribute__ ((section (".probes"))); #define RUBY_METHOD_ENTRY(arg1, arg2, arg3, arg4) \ DTRACE_PROBE4 (ruby, method__entry, arg1, arg2, arg3, arg4)
*.d
ファイルから、sys/sdt.hで定義される DTRACE_PROBE*
マクロをラップしたマクロを定義している。実際のRubyのビルドでは、マクロの名前をsedで置き換えているようだ。
./configure --enable-dtrace
したRubyの中間生成物である probes.h
は以下のようになっている。
//... /* RUBY_DTRACE_METHOD_ENTRY ( const char *classname, const char *methodname, const char *filename, int lineno ) */ #if defined STAP_SDT_V1 #define RUBY_DTRACE_METHOD_ENTRY_ENABLED() __builtin_expect (method__entry_semaphore, 0) #define ruby_method__entry_semaphore method__entry_semaphore #else #define RUBY_DTRACE_METHOD_ENTRY_ENABLED() __builtin_expect (ruby_method__entry_semaphore, 0) #endif __extension__ extern unsigned short ruby_method__entry_semaphore __attribute__ ((unused)) __attribute__ ((section (".probes"))); #define RUBY_DTRACE_METHOD_ENTRY(arg1, arg2, arg3, arg4) \ DTRACE_PROBE4 (ruby, method__entry, arg1, arg2, arg3, arg4) //... #define RUBY_DTRACE_METHOD_RETURN(arg1, arg2, arg3, arg4) \ DTRACE_PROBE4 (ruby, method__return, arg1, arg2, arg3, arg4) //... #define RUBY_DTRACE_CMETHOD_ENTRY(arg1, arg2, arg3, arg4) \ DTRACE_PROBE4 (ruby, cmethod__entry, arg1, arg2, arg3, arg4) //...
ただ、実際にコードではこのマクロが直接使われるというより、さらに一段噛ませたマクロを使っている模様。
$ grep RUBY_DTRACE_ -r *.c | head -20 array.c: RUBY_DTRACE_CREATE_HOOK(ARRAY, 0); array.c: RUBY_DTRACE_CREATE_HOOK(ARRAY, capa); array.c: RUBY_DTRACE_CREATE_HOOK(ARRAY, capa); eval.c: RUBY_DTRACE_HOOK(RAISE, rb_obj_classname(ec->errinfo)); gc.c:#define RUBY_DTRACE_GC_HOOK(name) \ gc.c: do {if (RUBY_DTRACE_GC_##name##_ENABLED()) RUBY_DTRACE_GC_##name();} while (0) gc.c: RUBY_DTRACE_GC_HOOK(MARK_BEGIN); gc.c: RUBY_DTRACE_GC_HOOK(MARK_END); gc.c: RUBY_DTRACE_GC_HOOK(SWEEP_BEGIN); gc.c: RUBY_DTRACE_GC_HOOK(SWEEP_END); hash.c: RUBY_DTRACE_CREATE_HOOK(HASH, 0); load.c: RUBY_DTRACE_HOOK(LOAD_ENTRY, RSTRING_PTR(orig_fname)); load.c: RUBY_DTRACE_HOOK(LOAD_RETURN, RSTRING_PTR(orig_fname)); load.c: RUBY_DTRACE_HOOK(REQUIRE_ENTRY, RSTRING_PTR(fname)); load.c: RUBY_DTRACE_HOOK(FIND_REQUIRE_ENTRY, RSTRING_PTR(fname)); load.c: RUBY_DTRACE_HOOK(FIND_REQUIRE_RETURN, RSTRING_PTR(fname)); load.c: RUBY_DTRACE_HOOK(REQUIRE_RETURN, RSTRING_PTR(fname)); object.c: RUBY_DTRACE_CREATE_HOOK(OBJECT, rb_class2name(klass)); parse.c:#define RUBY_DTRACE_PARSE_HOOK(name) \ parse.c: if (RUBY_DTRACE_PARSE_##name##_ENABLED()) { \
大体以下のようなマクロを噛ませている。詳細は probes_helper.h
/ internal/vm.h
/ gc.c
などにある。
- メソッドのentry, return -
RUBY_DTRACE_(C)?METHOD_(ENTRY|RETURN)_HOOK()
- オブジェクトや特定のクラス(Arrayなど)のオブジェクトの生成 -
RUBY_DTRACE_CREATE_HOOK()
- そのほか、トレースしたいイベント -
RUBY_DTRACE_HOOK()
- GCに関するイベント -
RUBY_DTRACE_GC_HOOK()
probes.d
をいじってコンパイルしてみる
で、今回以下のような ruby::ractor__create
というProbeを追加した。
provider ruby { // ... /* ruby:::gc-sweep-end(); Fired at the end of a sweep phase. */ probe gc__sweep__end(); probe ractor__create(int ractor_id, const char *filename, int lineno); // added };
この状態でmakeし直すとprobes.hに新しいマクロが定義されている。
$ tail probes.h #if defined STAP_SDT_V1 #define RUBY_DTRACE_RACTOR_CREATE_ENABLED() __builtin_expect (ractor__create_semaphore, 0) #define ruby_ractor__create_semaphore ractor__create_semaphore #else #define RUBY_DTRACE_RACTOR_CREATE_ENABLED() __builtin_expect (ruby_ractor__create_semaphore, 0) #endif __extension__ extern unsigned short ruby_ractor__create_semaphore __attribute__ ((unused)) __attribute__ ((section (".probes"))); #define RUBY_DTRACE_RACTOR_CREATE(arg1, arg2, arg3) \ DTRACE_PROBE3 (ruby, ractor__create, arg1, arg2, arg3)
ただ、この状態ではバイナリにはUSDTの情報はない。
他の定義よろしく RUBY_DTRACE_CREATE_HOOK(RACTOR, id)
を適切な場所に挿入する。執筆時点のmasterでは以下の関数内に定義すれば良さそう。
ruby/ractor.c at 7b9476fbfab738d1eb01b4b4c4af9a1680513019 · ruby/ruby · GitHub
こう書き換えた。
#include "probes.h" //... static VALUE ractor_create(rb_execution_context_t *ec, VALUE self, VALUE loc, VALUE name, VALUE args, VALUE block) { VALUE rv = ractor_alloc(self); rb_ractor_t *r = RACTOR_PTR(rv); ractor_init(r, name, loc); // can block here r->pub.id = ractor_next_id(); RUBY_DEBUG_LOG("r:%u", r->pub.id); RUBY_DTRACE_CREATE_HOOK(RACTOR, r->pub.id); // ここに追加した r->r_stdin = rb_io_prep_stdin(); r->r_stdout = rb_io_prep_stdout(); r->r_stderr = rb_io_prep_stderr(); rb_ractor_t *cr = rb_ec_ractor_ptr(ec); r->verbose = cr->verbose; r->debug = cr->debug; rb_thread_create_ractor(r, args, block); RB_GC_GUARD(rv); return rv; }
これでコンパイルが通り、バイナリ側でも ruby:ractor__create
というUSDTが定義される。
$ readelf -n ./ruby | grep -C 2 ractor__create stapsdt 0x00000049 NT_STAPSDT (SystemTap probe descriptors) Provider: ruby Name: ractor__create Location: 0x000000000013d0f8, Base: 0x00000000003950c0, Semaphore: 0x00000000003fc2cc Arguments: 4@8(%rbp) 8@%rax -4@16(%rsp)
新しいProbeの動作確認
Ubuntu Groovyで bpftrace
パッケージを入れる。バージョンは v0.11.0
。
$ sudo apt install bpftrace
bpftraceからは usdt:/path/to/miniruby:ruby:ractor__create
でアタッチできるらしい。ちなみにminirubyにも同じUSDTがあるので、いったんminirubyで試す。
$ sudo bpftrace -l 'usdt:./miniruby' usdt:./miniruby:ruby:array__create usdt:./miniruby:ruby:raise usdt:./miniruby:ruby:gc__sweep__end usdt:./miniruby:ruby:gc__sweep__begin usdt:./miniruby:ruby:gc__mark__begin usdt:./miniruby:ruby:gc__mark__end usdt:./miniruby:ruby:hash__create usdt:./miniruby:ruby:load__return usdt:./miniruby:ruby:load__entry usdt:./miniruby:ruby:find__require__return usdt:./miniruby:ruby:require__entry usdt:./miniruby:ruby:find__require__entry usdt:./miniruby:ruby:require__return usdt:./miniruby:ruby:object__create usdt:./miniruby:ruby:parse__end usdt:./miniruby:ruby:parse__begin usdt:./miniruby:ruby:ractor__create usdt:./miniruby:ruby:string__create usdt:./miniruby:ruby:symbol__create usdt:./miniruby:ruby:cmethod__entry usdt:./miniruby:ruby:cmethod__return usdt:./miniruby:ruby:method__return usdt:./miniruby:ruby:method__entry
公式ドキュメント のサンプルにあるような以下のコードを実行する。
puts "Pid: #{$$}" gets # block until keyhit pipe = Ractor.new do loop do Ractor.yield Ractor.receive end end RN = 10 rs = RN.times.map{|i| Ractor.new pipe, rand(RN*128) do |pipe, i| pipe << i end } p RN.times.map{ pipe.take }.sort
あるターミナルでサンプルコードを実行する。PIDを吐き出してブロックするので、
$ ./miniruby ractor-sample.rb Pid: 80186
そのPIDに対して別のターミナルでアタッチする。
$ sudo bpftrace -p 80186 \ -e 'usdt:./miniruby:ruby:ractor__create { printf("Ractor created: id=%d file=%s:L%d\n", arg0, str(arg1), arg2) }' Attaching 1 probe...
ブロックした方のターミナルでリターンキーを押し、動かすと、生成されたRactorの情報が取得できる。ただ、ロケーション情報はinternalで、 racrot.rb
の __builtin_ractor_create が呼ばれた行番号になってしまう。ArrayやHashと比べ初期化が特殊なようなので何か考慮する必要がある模様。
Ractor created: id=2 file=<internal:ractor>:L267 Ractor created: id=3 file=<internal:ractor>:L267 Ractor created: id=4 file=<internal:ractor>:L267 Ractor created: id=5 file=<internal:ractor>:L267 Ractor created: id=6 file=<internal:ractor>:L267 Ractor created: id=7 file=<internal:ractor>:L267 Ractor created: id=8 file=<internal:ractor>:L267 Ractor created: id=9 file=<internal:ractor>:L267 Ractor created: id=10 file=<internal:ractor>:L267 Ractor created: id=11 file=<internal:ractor>:L267 Ractor created: id=12 file=<internal:ractor>:L267
まとめや所感
probes.d
を加工すればRubyに新しいUSDTのProbeを導入できる- 生成されたマクロそのままではなくRuby内の規約に沿って記述する必要がある(いくつかの規約はundocumented)。
- (苦労して)USDTじゃなくて、内部関数のエントリポイントは普通にuprobe/uretprobeが存在する模様なので、それをトレースしてもいい。
- ただ、それではractor idが簡単に取れないかも?
$ sudo bpftrace -l 'uprobe:./miniruby' | grep ':ractor' | head -20 uprobe:./miniruby:ractor_basket_accept uprobe:./miniruby:ractor_basket_accept.cold uprobe:./miniruby:ractor_close_outgoing.constprop.0 uprobe:./miniruby:ractor_create uprobe:./miniruby:ractor_create.cold uprobe:./miniruby:ractor_free uprobe:./miniruby:ractor_init uprobe:./miniruby:ractor_local_storage_free_i uprobe:./miniruby:ractor_local_storage_mark_i uprobe:./miniruby:ractor_local_value uprobe:./miniruby:ractor_local_value.cold uprobe:./miniruby:ractor_local_value_set uprobe:./miniruby:ractor_local_value_set.cold uprobe:./miniruby:ractor_mark uprobe:./miniruby:ractor_memsize uprobe:./miniruby:ractor_moved_missing uprobe:./miniruby:ractor_queue_enq.constprop.0 uprobe:./miniruby:ractor_receive_if uprobe:./miniruby:ractor_receive_if.cold uprobe:./miniruby:ractor_receive_wait.isra.0