あんま深く考えず 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 の基礎。