Ruby の TracePoint について調べてみた

Ruby の TracePoint について調べてみました。

# Ruby 2.0 から導入された機能

TracePoint は Ruby 2.0 から使えるようになった機能です。様々なイベントをフックすることが出来ます。


# トレースの開始方法

トレースの開始するには 2通りの方法があります。

## 1. `trace` を利用して開始する

`trace` を使用した場合はトレースが即座に有効状態になります。

```ruby
TracePoint.trace do |tp|
  p tp
end
```

返り値として TracePoint オブジェクトを返すので、トレースを無効にしたい場合は `disable` を実行します。

```ruby
trace = TracePoint.trace  do |tp|
  p tp
end

trace.enabled? #=> true
trace.disable
trace.enabled? #=> false
```

## 2. `new` で TracePoint オブジェクトを作成して `enable` で開始する

もう一つは `new` で TracePoint オブジェクトを生成して `enable` を実行する方法です。

```ruby
trace = TracePoint.new do |tp|
  p tp
end

trace.enable
```

`disable` を使用するとトレースが終了します。（トレースが有効かどうかを調べるには `disabled?` を使用する。


```ruby
trace = TracePoint.new do |tp|
  p tp
end

trace.enabled? #=> false
trace.enable
trace.enabled? #=> true
trace.disable
trace.enabled? #=> false
```

# トレースするイベントを指定

`trace` や `new` では引数を渡すことでトレースするイベントを指出することが出来ます。

何も指定しない場合は全てのイベントがトレースされるのですが、引数でトレースするイベントの種類を指定することで、そのイベントだけがトレースされるようになります。

```ruby
TracePoint.trace(:call) do |tp|
  p [tp.self, tp.event, tp.method_id]
end

def say
  'hello'
end

say
```

実行結果

```
[main, :call, :say]
```


## トレース出来るイベントの種類

TracePoint では Ruby の様々なイベントを指定することが可能です。

| イベント         | 説明                                               |
|:-----------------|:---------------------------------------------------|
| :line            | 式の評価                                           |
| :class           | クラス定義、特異クラス定義、モジュール定義への突入 |
| :end             | クラス定義、特異クラス定義、モジュール定義の終了   |
| :call            | Ruby で記述されたメソッドの呼び出し                |
| :return          | Ruby で記述されたメソッド呼び出しからのリターン    |
| :c_call          | Cで記述されたメソッドの呼び出し                    |
| :c_return        | Cで記述されたメソッド呼び出しからのリターン        |
| :raise           | 例外の発生                                         |
| :b_call          | ブロックの開始                                     |
| :b_return        | ブロックの終了                                     |
| :thread_begin    | スレッドの開始                                     |
| :thread_end      | スレッドの終了                                     |
| :fiber_switch    | コンテキストの切替時                               |
| :script_compiled | スクリプトが ISeq に変換された後                   |


## line

式の評価毎に発生するイベント。

```ruby
TracePoint.trace(:line) do |tp|
  puts "#{tp.lineno}: exec"
end

a = 1
b = 2
c = 3
```

実行結果

```
5: exec
6: exec
7: exec
```


## class, end

`class` はクラスやモジュールの定義の開始、 `end` はクラスやモジュールの定義の終了の際に発生するイベントです。

```ruby
TracePoint.trace(:class, :end) do |tp|
  p [tp.lineno, tp.self, tp.event]
end

class A
  def say
  end
end
```

実行結果

```
[5, A, :class]
[8, A, :end]
```

`self` を利用すると、どのクラス・モジュールが定義されたのかがわかります。


## call, return

`call` は Ruby で記述されたメソッドの呼び出し、 `return` は Ruby で記述されたメソッドからのリターン時に呼び出されるイベントです。 `method_id` を使用することで、どのメソッドから呼び出されたのかがわかります。


```ruby
TracePoint.trace(:call, :return) do |tp|
  p [tp.lineno, tp.event, tp.method_id]
end

def say
  'hello'
end

say
```

実行結果

```
[5, :call, :say]
[7, :return, :say]
```


## c_call, c_return

`c_call` はC で記述されたメソッドの呼び出し、 `c_return` はC で記述されたメソッド呼び出しからのリターン時に呼び出されるイベントです。

```ruby
TracePoint.trace(:c_call, :c_return) do |tp|
  p [tp.lineno, tp.event, tp.method_id]
end

'100'.to_i
```

実行結果

```
[1, :c_return, :trace]
[5, :c_call, :to_i]
[5, :c_return, :to_i]
```


## raise

`raise` は例外発生時に呼び出されるイベントです。発生した例外は `raised_exception` で取得することが出来ます。

```ruby
TracePoint.trace(:raise) do |tp|
  p [tp.lineno, tp.event, tp.raised_exception]
end

raise 'error'
```

実行結果

```
[5, :raise, #<RuntimeError: error>]
Traceback (most recent call last):
raise.rb:5:in `<main>': error (RuntimeError)
```


## b_call, b_return

`b_call` はブロックの開始、 `b_return` はブロックの終了時に呼び出されるイベントです。

```ruby
TracePoint.trace(:b_call, :b_return) do |tp|
  p [tp.lineno, tp.event]
end

[1, 2, 3].each do |n|
  p n
end
```

実行結果

```
[5, :b_call]
1
[7, :b_return]
[5, :b_call]
2
[7, :b_return]
[5, :b_call]
3
[7, :b_return]
```

## thread_begin, thread_end

`thread_begin` はスレッドの開始、 `thread_end` はスレッドの終了時に呼び出されるイベントです。

```ruby
TracePoint.trace(:thread_begin, :thread_end) do |tp|
  p tp.event
end

thr = Thread.new { puts 'Hello World!' }
thr.join
```

実行結果

```
:thread_begin
Hello World!
:thread_end
```

## fiber_switch

`fiber_switch` はコンテキストの切り替えが発生したタイミングに発生するイベントです。

```ruby
TracePoint.trace(:fiber_switch) do |tp|
  p [tp.lineno, tp.event]
end

f = Fiber.new do
  n = 0

  loop do
    Fiber.yield(n)
    n += 1
  end
end

3.times do
 p f.resume
end
```

実行結果

```
[0, :fiber_switch]
[15, :fiber_switch]
0
[9, :fiber_switch]
[15, :fiber_switch]
1
[9, :fiber_switch]
[15, :fiber_switch]
2
```


# TracePoint で使用できるメソッド

TracePoint では様々なメソッドが使用できます

| メソッド名       | 説明                                          | 備考             |
|:-----------------|:----------------------------------------------|:-----------------|
| binding          | binding オブジェクトを返す                    |                  |
| defined_class    | メソッドを定義したクラスかモジュールを返す    |                  |
| disable          | トレースを無効にする                          |                  |
| enable           | トレースを有効にする                          |                  |
| enabled?         | トレースが有効かどうかを調べる                |                  |
| event            | 発生したイベントの種類を返す                  |                  |
| inspect          | self の状態を人間が読みやすいものにしてくれる |                  |
| lineno           | イベントが発生した行番号を返す                |                  |
| method_id        | イベントが発生したメソッド名を返す            |                  |
| parameters       | 引数の情報を返す                              | 2.6 から使用可能 |
| path             | イベントが発生したファイルのパスを返す        |                  |
| raised_exception | 発生した例外を返す                            |                  |
| return_value     | メソッドやブロックの戻り値を返す              |                  |
| self             | イベントを発生させたオブジェクトを返す        |                  |


## binding

binding オブジェクトを取得することが出来ます。

### binding でさらに便利になる TracePoint

Binding オブジェクトで利用できるメソッドを使用することで、TracePoint がさらに強力になります。

| メソッド名              | 説明                                                          |
|:------------------------|:--------------------------------------------------------------|
| eval                    | self をコンテキストとして渡された文字列を評価しその結果を返す |
| local_variable_defined? | 引数で渡された名前の変数が定義されているかどうかを判定する    |
| local_variable_get      | 引数で渡された名前の変数の値を取得する                        |
| local_variable_set      | 第一引数で指定した名前の変数に第二引数で渡された値を設定する  |
| local_variables         | ローカル変数の一覧を取得します                                |
| receiver                | 保持するコンテキスト内での self を返します                    |


例) メソッドの引数の値をチェックする

```ruby
TracePoint.trace(:call) do |tp|
  puts "#{tp.method_id} : arguments"

  tp.binding.local_variables.each do |name|
    puts "  #{name} = #{tp.binding.local_variable_get(name).inspect}"
  end
end

def say(message)
  puts message
end

say('hello')
```

実行結果

```
say : arguments
  message = "hello"
hello
```

## defined_class

メソッドを定義したクラスかモジュールを返します。

```ruby
TracePoint.trace(:call) do |tp|
  p tp.defined_class
end

class A
  def say
    'hi'
  end
end

A.new.say
```

実行結果

```
A
```

## disable

トレースを無効にします。

```ruby
trace = TracePoint.trace { |tp| }
 
trace.enabled? #=> true
trace.disable
trace.enabled? #=> false
```

`disable` にブロックを渡すことで、そのブロックの範囲だけトレースを無効にすることが出来ます。

```ruby
trace = TracePoint.trace { |tp| }
 
trace.enabled? #=> true
trace.disable { }
trace.enabled? #=> true
```


## enable

トレースを有効にします。

```ruby
trace = TracePoint.new { |tp| }
 
trace.enabled? #=> false
trace.enable
trace.enabled? #=> true
```

`enable` にブロックを渡すことで、そのブロックの範囲だけトレースを有効にすることが出来ます。

```ruby
trace = TracePoint.new { |tp| }
 
trace.enabled? #=> false
trace.enable { }
trace.enabled? #=> false
```

## enabled?

トレースが有効かどうかを返します。

### トレースが有効のままだと遅くなる？

トレースが有効な状態はトレースが無効な状態に比べて実行速度が遅くなってしまうので、使う時はなるべく必要な範囲だけを有効にしたほうが良さそうです。

```ruby
require 'benchmark'

trace = TracePoint.new { |tp| }

result1 = Benchmark.measure do 
  1000000.times { 1 + 1 }
end

result2 = Benchmark.measure do 
  trace.enable { 1000000.times { 1 + 1 } }
end

puts Benchmark::CAPTION
puts result1
puts result2
```

結果

```
      user     system      total        real
  0.052800   0.000084   0.052884 (  0.053058)
  0.339369   0.000967   0.340336 (  0.341383)
```


## event

発生したイベントの種類を返します。

## inspect

self の状態を人間が読みやすいものにしてくれます。

```ruby
TracePoint.trace do |tp|
  p tp
end
```

実行結果

```
#<TracePoint:c_return `trace'@inspect.rb:1>
```

## lineno

イベントが発生した行番号を返します。 `path` と一緒に使用することでどこで発生したイベントなのかを判別することが出来ます。

```ruby
TracePoint.trace(:line) do |tp|
  p tp.lineno
end

a = 1
b = 2
c = 3
```

実行結果

```
5
6
7
```


## method_id

イベントが発生したメソッド名を返します。

```ruby
TracePoint.trace(:call) do |tp|
  p tp.method_id
end

class A
  def say
    'hi'
  end
end

A.new.say
```

実行結果

```
:say
```

## parameters

メソッドの引数の情報を取得します。帰ってくる値の形式は `[引数の種類, 引数名]` の配列になっているので、引数の値を取得したい場合は別途 `local_variable_get` などを使用する必要があります。


```ruby
TracePoint.trace(:call) do |tp|
  pp tp.parameters

  tp.parameters.each do |type, name|
    puts "parameter #{name}: #{tp.binding.local_variable_get(name).inspect}"
  end
end

def foo(msg, name = 'bob'); end

foo('hello')
```

```
[[:req, :msg], [:opt, :name]]
parameter msg: "hello"
parameter name: "bob"
```

## path

イベントが発生したファイルのパスを返します。


## raised_exception

発生した例外を返します。 `raise` イベント以外で使用した場合は `RuntimeError` が発生します。


## return_value

メソッドやブロックの返り値を取得することが出来ます。このイベントが利用出来るのは `return`, `c_return`, `b_return` の 3つで、これ以外のイベントで使用すると `RuntimeError` が発生します。

```ruby
TracePoint.trace(:return) do |tp|
  p tp.return_value
end

def message
  'hi'
end

message
```

実行結果

```
"hi"
```

### return_value を書き変える

この `return_value` の値を破壊的に変更することで、メソッドの返り値を変更することが出来ます。

```ruby
TracePoint.trace(:c_return) do |tp|
  tp.return_value.downcase! if tp.method_id == :upcase
end

p 'Hello'.upcase
```

実行結果

```
"hello"
```


## self

イベントを発生させたオブジェクトを返します。

```ruby
TracePoint.trace(:call) do |tp|
  p tp.self
  p tp.binding.eval('self')
  p tp.binding.receiver
end

class A
  def say
  end
end

A.new.say
```

実行結果

```
#<A:0x00007fa099020198>
#<A:0x00007fa099020198>
#<A:0x00007fa099020198>
```

# サンプルコード

TracePoint を使って色々コードを書いてみました。

* [Ruby でオープンクラスに警告を出す](https://qiita.com/siman/items/cbc7b85ff45eb5fc58d5)
* [Ruby でクラスのソースを表示するコード](https://qiita.com/siman/items/d3cf0d815c3c3b811725)
* [Ruby で遅延 Proc 呼び出し](https://qiita.com/siman/items/54b8e7a2e503a5c3ba5e)
* [コールスタックを tree コマンド風に表示する gem を作った](https://qiita.com/siman/items/20b55ca9ba82c730a4de)

# 参考サイト

* [TracePoint](https://docs.ruby-lang.org/ja/latest/class/TracePoint.html)
* [Ruby 2.5 の改善を自慢したい](http://techlife.cookpad.com/entry/2017/12/28/113839)
* [Ruby VM アドベントカレンダー #12 TracePoint の紹介 (1)](http://www.atdot.net/~ko1/diary/201212.html#d12)
* [Ruby VM アドベントカレンダー #13 TracePoint の紹介 (2)](http://www.atdot.net/~ko1/diary/201212.html#d13)
* [Ruby VM アドベントカレンダー #14 TracePoint の紹介 (3)](http://www.atdot.net/~ko1/diary/201212.html#d14)