あんま深く考えず valgrind を Ruby の head のテストに適用してみたところ、結構もにょもにょ漏れてるもんだなぁと気付いたので、いくつか修正してみたりしたのですが、その時案外困るのが、リークする最小のコードが簡単に作れない、ってことでした。 valgrind は C 言語的にどこで malloc を呼んだかは教えてくれるものの、 Ruby コードでどこだったかは教えてくれないからです。修正はできたけど具体的にどこで漏れてるかはよくわからん、ということさえありました。

というわけで、 Ruby 的にどこで漏れたかを教えてくれる valgrind 用の tool 、 Rubygrind を作ってみました。

http://shinh.skr.jp/binary/rmemcheck.tgz

これを valgrind-3.3.1 のディレクトリに展開して、

> diff -u configure.in\~ configure.in --- configure.in~ 2008-06-01 10:39:06.000000000 +0900 +++ configure.in 2008-08-03 23:39:57.000000000 +0900 @@ -1037,6 +1037,13 @@ exp-drd/Makefile exp-drd/docs/Makefile exp-drd/tests/Makefile + rmemcheck/Makefile + rmemcheck/tests/Makefile + rmemcheck/tests/amd64/Makefile + rmemcheck/tests/ppc32/Makefile + rmemcheck/tests/ppc64/Makefile + rmemcheck/tests/x86/Makefile + rmemcheck/docs/Makefile ) cat<<EOF > diff -u Makefile.am\~ Makefile.am --- Makefile.am~ 2008-06-01 10:39:06.000000000 +0900 +++ Makefile.am 2008-08-03 23:34:27.000000000 +0900 @@ -9,7 +9,8 @@ massif \ lackey \ none \ - helgrind + helgrind \ + rmemcheck EXP_TOOLS = exp-omega \ exp-drd

って感じで configure.in と Makefile.am をいじって、あとは autoconf automake-1.9 (うちだと automake だと 1.10 が動いて怒られた) とかして ./configure make make install で動くんじゃないかなと思います。ウソです。知りません。確認してません。

で、どういうふうに動くか、ですが、

> valgrind --leak-check=full --tool=rmemcheck ./ruby1.9 test/strscan/test_stringscanner.rb

とかしてやると、

==7902== 442 (440 direct, 2 indirect) bytes in 1 blocks are definitely lost in loss record 11 of 34 ==7902== Ruby test/strscan/test_stringscanner.rb:547 ==7902== at 0x4C245D3: malloc (mc_replace_strmem.c:1127) ==7902== by 0x4704BB: onig_alloc_init (regcomp.c:5563) ==7902== by 0x4758A0: onig_new (regcomp.c:5601) ==7902== by 0x46D8C4: rb_reg_prepare_re (re.c:1232) ==7902== by 0x6665C56: strscan_do_scan (strscan.c:418) ==7902== by 0x4B1E5D: vm_call_method (vm_insnhelper.c:378) ==7902== by 0x4B3A76: vm_eval (insns.def:999) ==7902== by 0x4B852C: vm_eval_body (vm.c:1060) ==7902== by 0x4B8B13: invoke_block_from_c (vm.c:472) ==7902== by 0x4B9343: rb_yield (vm.c:502) ==7902== by 0x4C75E0: rb_ary_each (array.c:1135) ==7902== by 0x4B1E5D: vm_call_method (vm_insnhelper.c:378)

とか出てきます。 test_stringscanner.rb を実行した時に 547 行目にある Ruby コードをひきずり出せばリークを再現させられそうです。ひきずり出してきたこんなコード

require ' strscan ' ss = StringScanner .new( "\xA1\xA2" .force_encoding( " euc-jp " )) t = ss.scan( /./ )

を同じように実行してやると、

==7808== 442 (440 direct, 2 indirect) bytes in 1 blocks are definitely lost in loss record 14 of 31 ==7808== Ruby leak_strscan.rb:3 ==7808== at 0x4C245D3: malloc (mc_replace_strmem.c:1127) ==7808== by 0x4704BB: onig_alloc_init (regcomp.c:5563) ==7808== by 0x4758A0: onig_new (regcomp.c:5601) ==7808== by 0x46D8C4: rb_reg_prepare_re (re.c:1232) ==7808== by 0x6463C56: strscan_do_scan (strscan.c:418) ==7808== by 0x4B1E5D: vm_call_method (vm_insnhelper.c:378) ==7808== by 0x4B3A76: vm_eval (insns.def:999) ==7808== by 0x4B852C: vm_eval_body (vm.c:1060) ==7808== by 0x4B8736: rb_iseq_eval (vm.c:1265) ==7808== by 0x4C24442: rb_iseq_eval (mc_replace_strmem.c:1170) ==7808== by 0x418CCB: ruby_exec_node (eval.c:217) ==7808== by 0x41A602: ruby_run_node (eval.c:245)

と言われました。やった。簡単に小さい再現コードが作れました。

肝心の修正してみたコードは以下みたいな感じだけど、 re.c から適当に取ってきただけなので、また考える。

Index: ext/strscan/strscan.c =================================================================== --- ext/strscan/strscan.c (revision 18666) +++ ext/strscan/strscan.c (working copy) @@ -407,6 +407,7 @@ struct strscanner *p; regex_t *re; int ret; + int tmpreg; Check_Type(regex, T_REGEXP); GET_SCANNER(self, p); @@ -416,6 +417,9 @@ return Qnil; } re = rb_reg_prepare_re(regex, p->str); + tmpreg = re != RREGEXP(regex)->ptr; + if (!tmpreg) RREGEXP(regex)->usecnt++; + if (headonly) { ret = onig_match(re, (UChar* )CURPTR(p), (UChar* )(CURPTR(p) + S_RESTLEN(p)), @@ -427,6 +431,16 @@ (UChar* )CURPTR(p), (UChar* )(CURPTR(p) + S_RESTLEN(p)), &(p->regs), ONIG_OPTION_NONE); } + if (!tmpreg) RREGEXP(re)->usecnt--; + if (tmpreg) { + if (RREGEXP(regex)->usecnt) { + onig_free(re); + } + else { + onig_free(RREGEXP(regex)->ptr); + RREGEXP(regex)->ptr = re; + } + } if (ret == -2) rb_raise(ScanError, "regexp buffer overflow"); if (ret < 0) {

ちなみに以下が memcheck との diff 。やりたいことはごく簡単な変更なのに、結構色々やる必要があって、 valgrind 大変だなぁと思いました。でも内部構造がだいたい見えてきたのは非常に良かった。感想としては valgrind すごい。

http://shinh.skr.jp/binary/memcheck_rmemcheck.diff.gz

TODO: 良い子のための valgrind tool 書き方講座、というか valgrind の基礎。