TracePointを使用したRubyでのデバッグへのアプローチの変更
Rubyは、開発者にもたらす生産性で常に知られています。洗練された構文、豊富なメタプログラミングサポートなど、コードを書くときに生産性を高める機能に加えて、TracePoint
と呼ばれる別の秘密兵器もあります。 これにより、「デバッグ」を高速化できます。
この投稿では、簡単な例を使用して、デバッグについて見つけた2つの興味深い事実を示します。
- ほとんどの場合、バグ自体を見つけるのは難しいことではありませんが、プログラムがどのように機能するかを詳細に理解することは難しいことではありません。これを深く理解すると、通常はすぐにバグを見つけることができます。
- メソッド呼び出しレベルまでプログラムを監視することは時間がかかり、デバッグプロセスの主要なボトルネックです。
次に、TracePoint
の方法を説明します プログラムが何をしているのかを「教えて」くれるようにすることで、デバッグへの取り組み方を変えることができます。
デバッグとは、プログラムとその設計を理解することです
plus_1
というRubyプログラムがあるとしましょう。 そしてそれは正しく機能していません。これをどのようにデバッグしますか?
# plus_1.rb
def plus_1(n)
n + 2
end
input = ARGV[0].to_i
puts(plus_1(input))
$ ruby plus_1.rb 1
3
理想的には、3つのステップでバグに対処できるはずです:
- デザインからの期待を学ぶ
- 現在の実装を理解する
- バグを追跡する
設計からの期待を学ぶ
ここで期待される動作は何ですか? plus_1
1
を追加する必要があります コマンドラインからの入力である引数に。しかし、どうすればこれを「知る」ことができますか?
実際のケースでは、テストケース、ドキュメント、モックアップを読んだり、他の人にフィードバックを求めたりすることで、期待を理解できます。理解は、プログラムがどのように「設計」されているかによって異なります。
このステップは、デバッグプロセスの最も重要な部分です。プログラムがどのように機能するかを理解していないと、デバッグすることはできません。
ただし、チームの調整、開発ワークフローなど、このステップの一部となる可能性のある多くの要因があります。TracePoint
これらの問題についてはサポートできませんので、今日はこれらの問題について詳しく説明しません。
現在の実装を理解する
プログラムの予想される動作を理解したら、現時点でプログラムがどのように機能するかを学ぶ必要があります。
ほとんどの場合、プログラムがどのように機能するかを完全に理解するには、次の情報が必要です。
- プログラムの実行中に呼び出されるメソッド
- メソッド呼び出しの呼び出しと戻りの順序
- 各メソッド呼び出しに渡される引数
- 各メソッド呼び出しから返される値
- 各メソッド呼び出し中に発生した副作用。例:データの変更またはデータベースのリクエスト
上記の情報を使用して例を説明しましょう:
# plus_1.rb
def plus_1(n)
n + 2
end
input = ARGV[0].to_i
puts(plus_1(input))
$ ruby plus_1.rb 1
3
-
plus_1
というメソッドを定義します - 入力を取得します(
"1"
)ARGV
から -
to_i
を呼び出します"1"
に 、1
を返します -
1
を割り当てます ローカル変数input
へ -
plus_1
を呼び出しますinput
を使用したメソッド (1
)その引数として。パラメータn
1
の値を保持するようになりました -
+
を呼び出します1
のメソッド 引数付き2
、結果を返します3
-
3
を返します ステップ5の場合 -
puts
を呼び出します -
to_s
を呼び出します3
に 、"3"
を返します -
"3"
に合格puts
に 手順8から呼び出します。これにより、文字列をStdoutに出力する副作用がトリガーされます。次に、nil
を返します。 。
説明は100%正確ではありませんが、簡単な説明には十分です。
バグへの対処
プログラムがどのように機能し、実際にどのように機能するかを学習したので、バグを探し始めることができます。取得した情報を使用して、メソッド呼び出しを上向き(ステップ10から開始)または下向き(ステップ1から開始)に実行することで、バグを検索できます。この場合、最初に3を返したメソッド(1 + 2
)にさかのぼってそれを行うことができます。 step 6
で 。
これは現実からかけ離れています!
もちろん、実際のデバッグは、例でわかるほど単純ではないことは誰もが知っています。実際のプログラムと私たちの例の決定的な違いはサイズです。 5行のプログラムを説明するために10のステップを使用しました。小さなRailsアプリにはいくつのステップが必要ですか?例のように実際のプログラムを詳細に分析することは基本的に不可能です。プログラムを詳細に理解しないと、明らかなパスでバグを追跡することができないため、想定を立てる必要があります。または推測します。
情報は高価です
おそらくすでにお気づきかもしれませんが、デバッグの重要な要素は、持っている情報の量です。しかし、それだけの情報を取得するには何が必要ですか?見てみましょう:
# plus_1_with_tracing.rb
def plus_1(n)
puts("n = #{n}")
n + 2
end
raw_input = ARGV[0]
puts("raw_input: #{raw_input}")
input = raw_input.to_i
puts("input: #{input}")
result = plus_1(input)
puts("result of plus_1 #{result}")
puts(result)
$ ruby plus_1_with_tracing.rb 1
raw_input: 1
input: 1
n = 1
result of plus_1: 3
3
ご覧のとおり、ここでは2種類の情報しか取得できません。いくつかの変数の値とputs
の評価順序です。 (これは、プログラムの実行順序を意味します。)
この情報にはどれくらいの費用がかかりますか?
def plus_1(n)
+ puts("n = #{n}")
n + 2
end
-input = ARGV[0].to_i
-puts(plus_1(input))
+raw_input = ARGV[0]
+puts("raw_input: #{raw_input}")
+input = raw_input.to_i
+puts("input: #{input}")
+
+result = plus_1(input)
+puts("result of plus_1: #{result}")
+
+puts(result)
4つのputs
を追加する必要があるだけではありません ただし、値を個別に出力するには、一部の値の中間状態にアクセスするためにロジックを分割する必要もあります。この場合、8行の変更で内部状態の4つの追加出力を取得しました。これは、平均して1行の出力に対して2行の変更です。また、変更の数はプログラムのサイズに比例して増加するため、O(n)
と比較できます。 操作。
デバッグに費用がかかるのはなぜですか?
私たちのプログラムは、保守性、パフォーマンス、シンプルさなど、多くの目標を念頭に置いて作成できますが、通常は「トレーサビリティ」ではありません。つまり、検査用の値を取得します。これには通常、コードの変更が必要です。連鎖メソッド呼び出しを分割します。
- 取得する情報が多いほど、コードに追加/変更を加える必要があります。
ただし、取得する情報量が一定のポイントに達すると、それを効率的に処理することができなくなります。そのため、情報を除外するか、ラベルを付けて理解しやすくする必要があります。
- 情報が正確であればあるほど、コードに追加/変更を加える必要があります。
最後に、作業にはコードベースへのアクセスが含まれるため(これはバグ(コントローラーとモデルロジックなど)によって大きく異なる可能性があります)、自動化するのは困難です。コードベースがトレースに適している場合でも(たとえば、厳密に「デメテルの法則」に準拠している場合)、ほとんどの場合、異なる変数/メソッド名を手動で入力する必要があります。
(実際、Rubyには、これを回避するためのいくつかのトリックがあります。たとえば、__method__
。ただし、ここでは複雑にしないでください。)
TracePoint:救い主
ただし、Rubyは、コストを大幅に削減できる優れたツールTracePoint
を提供します。 。私はあなたのほとんどがすでにそれを聞いたことがあるか、以前にそれを使用したに違いありません。しかし、私の経験では、この強力なツールを日常のデバッグの実践に使用する人は多くありません。
情報をすばやく収集するための使用方法を紹介します。今回は、既存のロジックに触れる必要はありません。その前にいくつかのコードが必要です:
TracePoint.trace(:call, :return, :c_call, :c_return) do |tp|
event = tp.event.to_s.sub(/(.+(call|return))/, '\2').rjust(6, " ")
message = "#{event} of #{tp.defined_class}##{tp.callee_id} on #{tp.self.inspect}"
# if you call `return` on any non-return events, it'll raise error
message += " => #{tp.return_value.inspect}" if tp.event == :return || tp.event == :c_return
puts(message)
end
def plus_1(n)
n + 2
end
input = ARGV[0].to_i
puts(plus_1(input))
コードを実行すると、次のように表示されます。
return of #<Class:TracePoint>#trace on TracePoint => #<TracePoint:c_return `trace'@plus_1_with_trace_point.rb:1>
call of Module#method_added on Object
return of Module#method_added on Object => nil
call of String#to_i on "1"
return of String#to_i on "1" => 1
call of Object#plus_1 on main
return of Object#plus_1 on main => 3
call of Kernel#puts on main
call of IO#puts on #<IO:<STDOUT>>
call of Integer#to_s on 3
return of Integer#to_s on 3 => "3"
call of IO#write on #<IO:<STDOUT>>
3
return of IO#write on #<IO:<STDOUT>> => 2
return of IO#puts on #<IO:<STDOUT>> => nil
return of Kernel#puts on main => nil
私たちのコードは今でははるかに読みやすくなっています。すごいじゃないですか。それは多くの詳細でプログラム実行のほとんどを印刷します!以前の実行の内訳とマッピングすることもできます:
-
plus_1
というメソッドを定義します - 入力を取得します(
"1"
)ARGV
から -
to_i
を呼び出します"1"
に 、1
を返します -
1
を割り当てます ローカル変数input
へ -
plus_1
を呼び出しますinput
を使用したメソッド (1
)その引数として。パラメータn
値1
を運ぶようになりました -
+
を呼び出します1
のメソッド 引数付き2
、結果を返します3
-
3
を返します ステップ5の場合 -
puts
を呼び出します -
to_s
を呼び出します3
に 、"3"
を返します -
"3"
に合格puts
に 手順8から呼び出します。これにより、文字列をStdoutに出力する副作用がトリガーされます。そして、nil
を返します 。
# ignore this, it's TracePoint tracing itself ;D
return of #<Class:TracePoint>#trace on TracePoint => #<TracePoint:c_return `trace'@plus_1_with_trace_point.rb:1>
call of Module#method_added on Object # 1. Defines a method called `plus_1`.
return of Module#method_added on Object => nil
call of String#to_i on "1" # 3-1. Calls `to_i` on `"1"`
return of String#to_i on "1" => 1 # 3-2. which returns `1`
call of Object#plus_1 on main # 5. Calls `plus_1` method with `input`(`1`) as its argument.
return of Object#plus_1 on main => 3 # 7. Returns `3` for step 5
call of Kernel#puts on main # 8. Calls `puts`
call of IO#puts on #<IO:<STDOUT>>
call of Integer#to_s on 3 # 9. Calls `to_s` on `3`, which returns `"3"`
return of Integer#to_s on 3 => "3"
call of IO#write on #<IO:<STDOUT>> # 10-1. Passes `"3"` to the `puts` call from step 8
# 10-2. which triggers a side effect that prints the string to Stdout
3 # original output
return of IO#write on #<IO:<STDOUT>> => 2
return of IO#puts on #<IO:<STDOUT>> => nil
return of Kernel#puts on main => nil # 10-3. And then it returns `nil`.
前に言ったよりも詳細だとさえ言えます!ただし、ステップ2、4、および6が出力から欠落していることに気付く場合があります。残念ながら、TracePoint
では追跡できません。 次の理由で:
-
- 入力を取得します(
"1"
)ARGV
から
-
ARGV
および次の[]
現在、call/c_callとは見なされていません
- 入力を取得します(
-
-
1
を割り当てます ローカル変数input
へ
- 現在、変数割り当てのイベントはありません。
line
で(一種の)追跡できます イベント+正規表現ですが、正確ではありません
-
-
-
+
を呼び出します1
のメソッド 引数付き2
、結果を返します3
- 組み込みの
+
などの特定のメソッド呼び出し または属性アクセサメソッドは現在追跡できません
-
O(n)からO(log n)へ
前の例からわかるように、TracePoint
を適切に使用します 、プログラムが何をしているのかを「教えて」くれるようになります。必要な行数のため、TracePoint
プログラムのサイズに比例して大きくなることはありません。プロセス全体がO(log(n))
になると思います 操作。
次のステップ
この記事では、デバッグの主な問題について説明しました。うまくいけば、TracePoint
ゲームチェンジャーになる可能性があります。ただし、TracePoint
を試してみると 今のところ、それはおそらくあなたを助ける以上にあなたを苛立たせるでしょう。
TracePoint
からの情報量 、あなたはすぐに騒音に圧倒されるでしょう。新しい課題は、貴重な情報を残して、ノイズを除去することです。たとえば、ほとんどの場合、特定のモデルまたはサービスオブジェクトのみを考慮します。このような場合、次のように、受信者のクラスで通話をフィルタリングできます。
TracePoint.trace(:call) do |tp|
next unless tp.self.is_a?(Order)
# tracing logic
end
もう1つ覚えておくべきことは、TracePoint
に定義するブロックです。 何万回も評価される可能性があります。この規模では、フィルタリングロジックを実装する方法が、アプリのパフォーマンスに大きな影響を与える可能性があります。たとえば、これはお勧めしません:
TracePoint.trace(:call) do |tp|
trace = caller[0]
next unless trace.match?("app")
# tracing logic
end
これらの2つの問題について、典型的なRuby/Railsアプリケーションに役立つ定型文で見つけたいくつかのトリックと落とし穴を知らせる別の記事を用意しました。
また、この概念がおもしろいと思われる場合は、実装の煩わしさをすべて隠すtapping_deviceというgemも作成しました。
結論
デバッガーとトレースはどちらもデバッグのための優れたツールであり、私たちは長年それらを使用してきました。ただし、この記事で示したように、これらを使用するには、デバッグプロセス中に多くの手動操作が必要です。ただし、TracePoint
の助けを借りて 、それらの多くを自動化して、デバッグパフォーマンスを向上させることができます。 TracePoint
を追加できるといいのですが デバッグツールボックスに移動して、試してみてください。
-
RubyでN-Queensの問題を解決する
N-Queensは、 N*NボードにN個のクイーンを配置する必要がある興味深いコーディングチャレンジです。 。 次のようになります: 女王はすべての方向に移動できます: 垂直 水平 対角線 解決策(多くの場合があります)は、すべてのクイーンをボードに配置する必要があります。 &すべての女王は他のすべての女王の手の届かないところにいる必要があります。 この記事では、私がどのようにして解決策を思いついたのかを学びます。 計画 この種の課題を解決するときは、まず、計画を平易な英語で書き留めることから始めるのがよいでしょう。 これは、問題が何であるか、およびそれを解決するための手
-
Ruby転置法を使用して行を列に変換する
今日は、Ruby転置法を使用してRubyでグリッドを処理する方法を学習します。 多次元配列の形をした完璧なグリッド、たとえば3×3の正方形があると想像してみてください。 そして、行を取得して列に変換する 。 なぜあなたはそれをしたいのですか? 1つの用途は、古典的なゲームであるtic-tac-toeです。 ボードをグリッドとして保存します。次に、勝利の動きを見つけるには、行を確認する必要があります 、列 &対角線 。 問題は、グリッドを配列として格納している場合、行に直接アクセスすることしかできないことです。 コラムズザハードウェイ 「直接アクセス」とは、配列を(eachで)調べ