Ruby (MRI) に新しい Probe を定義する
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