K.Sasada's Home Page

こめんとのついか

こめんとこめんと!

message

please set comment :).

_12(Wed)

Ruby VM アドベントカレンダーの 12 日目です.

今日は Ruby 2.0 から導入された TracePoint についてご紹介します.

従来より,set_trace_func(proc_object) というインターフェースがありました.るりまから引用します.

Ruby インタプリタのイベントをトレースする Proc オブジェクトとして指定された proc を登録します。 proc に nil を指定した場合でトレースをオフにします。ブロックを指定された場合はそのブロックをトレースする Proc オブジェクトとして登録します。

登録したトレース(Proc オブジェクト)は,イベントが発生するたびに呼ばれます.イベントの種類は次のとおりです(るりまから引用します).

1.9 では,これを利用して,debug.rb や profile.rb などが実装されていました.

(ちなみに,Thread#set_trace_func などもあるんですが,今回は省略)


set_trace_func は(怪しいことをするには)便利な機能なのですが,いくつか問題がありました.

(1) は,例えば line イベントだけ処理したいな,という場合も,すべてのイベントでトレースが呼ばれてしまうため,line だけの場合は,例えば次のように書く必要がありました.

set_trace_func(proc{|event, file, line, id, binding, klass|
  case line
  when 'line'
    # ...
  else
    # 他は無視する
  end
})

大きな問題ではないですが,ちょっと面倒くさいですね.

(2) の「複数のトレースを,それぞれ有効にしたり無効にしたりできない」という問題ですが,複数のトレースを追加することはできます.ただ,それらを個別に無効にする方法はなく,全て無効にする方法しかありません.

set_trace_func(proc{|*args| p [1, args]})
set_trace_func(proc{|*args| p [2, args]})

p :hello

set_trace_func(nil) # ここで無効にする

p :world

#=>
c:/ko1/ruby/clean-trunk/bin/ruby: warning: -K is specified; it is for 1.8 compatibility and may cause odd behavior
ruby 2.0.0dev (2012-12-21 trunk 38515) [i386-mswin32_100]
[1, ["c-return", "t.rb", 1, :set_trace_func, #<Binding:0x6fbf1c>, Kernel]]
[1, ["line", "t.rb", 2, nil, #<Binding:0x6fbb84>, nil]]
[1, ["c-call", "t.rb", 2, :proc, #<Binding:0x6fb9a4>, Kernel]]
[1, ["c-return", "t.rb", 2, :proc, #<Binding:0x6fb580>, Kernel]]
[1, ["c-call", "t.rb", 2, :set_trace_func, #<Binding:0x6faf68>, Kernel]]
[2, ["c-return", "t.rb", 2, :set_trace_func, #<Binding:0x6fa874>, Kernel]]
[2, ["line", "t.rb", 4, nil, #<Binding:0x6fa324>, nil]]
[2, ["c-call", "t.rb", 4, :p, #<Binding:0x6f9d84>, Kernel]]
[2, ["c-call", "t.rb", 4, :hash, #<Binding:0x6f96b8>, Kernel]]
[2, ["c-return", "t.rb", 4, :hash, #<Binding:0x6f8cb8>, Kernel]]
[2, ["c-call", "t.rb", 4, :inspect, #<Binding:0x6f81b4>, Symbol]]
[2, ["c-return", "t.rb", 4, :inspect, #<Binding:0x6f37f4>, Symbol]]
:hello
[2, ["c-return", "t.rb", 4, :p, #<Binding:0x6f331c>, Kernel]]
[2, ["line", "t.rb", 6, nil, #<Binding:0x6f2d18>, nil]]
[2, ["c-call", "t.rb", 6, :set_trace_func, #<Binding:0x6f228c>, Kernel]]
:world

1番目と2番目のトレースがそれぞれ実行されているのがわかると思いますが,1つめだけ無効にする,という方法がないということがわかります.

(3) の遅いというのは,言わずもがななのですが,もっとも許されざるべき大罪です.プロファイラやデバッガを作りたい,などという用途では,トレースは出来るだけ対象プログラムに影響を与えないことが求められます.

そもそも,この機能はイベントが発生するたびにトレースを実行しますので,その起動コストがかかります.しかも,(1) で述べた通り,興味のないイベントが発生しても,毎回トレースを実行させます.例えば line イベントだけトレースしたい,という場合も,他のイベントは遠慮無く発生しますんで,無視するだけでもコストがかかることになります.

起動コストについて,もう少し細かくみてみますと,そもそも Proc オブジェクトを call するコストがかかります.もちろん,長い時間かかるものでもありませんが,チリも積もれば山となります.実は,メソッド呼び出しよりも遅いです(これは,今後の課題です).また,ブロックの引数に渡されるオブジェクトの準備が大変です.とくに,イベントが発生した箇所の Binding オブジェクトを生成しますが,これが重いです.デバッガ用途で利用するときは,Binding オブジェクトが必要になりますが,プロファイラ用途では不要な場合が多いです.というわけで,必要だったり必要じゃなかったりする引数の準備を,毎回丁寧にしているので遅いです.

実は,仕様策定および実装者としてもう一つ問題があって,それが (4) イベントがこれ以上増やせない,という互換性の問題なのでした.新しいイベントを追加しようと考えたとき,先ほどのような,興味のないイベントを無視するようなプログラムばかりならよいのですが,現在の 8 つのイベントでトレースが実行されることしか考慮していないプログラムが想定され,互換性に問題が生じます.というか,イベントを追加したいなーと思ってたのですよね,ぶっちゃけ.


さて,set_trace_func の問題を解決するための新たな機能が TracePoint になります.

例えば,行が実行されたときにファイル名と行番号,そしてメソッド名を表示するプログラムは,set_trace_func で書くと下記のようになります.

def m
  p :hello
  p :world
end

set_trace_func(proc{|event, file, line, id, binding, klass|
  puts "#{file}:#{line} #{id}" if event == 'line'
})

m

#=>
t.rb:10 
t.rb:2 m
:hello
t.rb:3 m
:world

これを TracePoint で書き換えると次のようになります.

def m
  p :hello
  p :world
end

trace = TracePoint.trace(:line){|tp|
  puts "#{tp.path}:#{tp.lineno} #{tp.method_id}"
}

m

#=>
t.rb:10 
t.rb:2 m
:hello
t.rb:3 m
:world

あまり,違いはないような気もしますが,細かいところが変わっています.ちょっと記事が長くなってしまったので,具体的な TracePoint の紹介は明日に行うことにします.

では今日はこの辺で.


好きにコメントを編集してください。ただし、あまり他の人のコメントを書き換えることは感心しません。



back

tton 記述が使えます。YukiWikiな記述してりゃ問題ありません。

「行頭に#code」 と、「行頭に#end」 で挟むと、その間の行は pre で囲まれます。プログラムのソースを書くときに使ってください。

例:

#code

(なんかプログラム書く)

#end

リンクは

[[なまえ|http://www.example.org]]

とか

[[http://www.example.org]]

で貼れます。

$Date: 2003/04/28 10:27:51 $