Rubygrind

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

なにかあれば下記メールアドレスへ。
shinichiro.hamaji _at_ gmail.com
shinichiro.h