I really like the useful information added to the backtrace (like arguments, block parameters...etc.). It's something I've been looking for for a long time.
And because I saw @ko1 asking for feedback on the backtrace format in his tweet, I think I can provide my 2 cents here.
Example
This is the example script I'll use to generate the output for my proposal:
class Foo
def first_call
second_call(20)
end
def second_call(num)
third_call_with_block do |ten|
forth_call(num, ten)
end
end
def third_call_with_block(&block)
@ivar1 = 10; @ivar2 = 20
yield(10)
end
def forth_call(num1, num2)
require "debug" # <= breakpoint
end
end
Foo.new.first_call
And this is the output it generates for the bt
command.
=>#0 Foo#forth_call(num1=20, num2=10) at foo.rb:21 #=> true
#1 block{|ten=10|} in second_call at foo.rb:8
#2 Foo#third_call_with_block(block=#<Proc:0x00007fc645174d10 foo.rb:7>) at foo.rb:15
#3 Foo#second_call(num=20) at foo.rb:7
#4 first_call at foo.rb:3
#5 <main> at foo.rb:24
Introduction of Components
I know most people reading this issue are already contributor to it and are familiar with the information contained in the backtrace. But I still want to do a short introduction to those who're not that familiar with it yet.
Trace Type - Call With Arguments
=>#0 Foo#forth_call(num1=20, num2=10) at foo.rb:21 #=> true
=>#0
- prompt + frame index
Foo#forth_call
- class + callee
(num1=20, num2=10)
- args
at foo.rb:21
- location
#=> true
- return value
Trace Type - Block Evaluation
#1 block{|ten=10|} in second_call at foo.rb:8
#1
- prompt + frame index
block{|ten=10|} in second_call
- block label & parameters
at foo.rb:8
- location
So, the Format
Ok, let's back to the backtrace:
=>#0 Foo#forth_call(num1=20, num2=10) at foo.rb:21 #=> true
#1 block{|ten=10|} in second_call at foo.rb:8
#2 Foo#third_call_with_block(block=#<Proc:0x00007fc645174d10 foo.rb:7>) at foo.rb:15
#3 Foo#second_call(num=20) at foo.rb:7
#4 first_call at foo.rb:3
#5 <main> at foo.rb:24
To me, the main drawback of the current format is: it's hard to read the full trace at once.
This is the normal Ruby backtrace for the same script:
foo.rb:8:in `block in second_call'
foo.rb:15:in `third_call_with_block'
foo.rb:7:in `second_call'
foo.rb:3:in `first_call'
foo.rb:24:in `<main>'
As a Rubyist, I'm used to having the call site upfront so I can read them directly, from top to bottom.
But in the new format, call site is placed after information like class, callee, and args. So it'll appear in the different column in different lines.
This means I need to perform a search on each line:
- Searching for the pattern of a trace.
- Sometime this doesn't work that well. For example, the
#2
frame has 2 traces. One of the Proc object and one for the actual call location.
- Or searching for the
at
keyword.
To improve this, I want to propose the following options as a start:
Option 1 - Don't Change Information Order. Just Replace at
With Different Symbols
We're better at distinguishing symbols then letters in a pile of texts. So if we don't want to change the order of information, replacing at
with a symbol should help locating the call site too. Some examples:
|
=>#0 Foo#forth_call(num1=20, num2=10) | foo.rb:21 #=> true
#1 block{|ten=10|} in second_call | foo.rb:8
#2 Foo#third_call_with_block(block=#<Proc:0x00007fc645174d10 foo.rb:7>) | foo.rb:15
#3 Foo#second_call(num=20) | foo.rb:7
#4 first_call | foo.rb:3
#5 <main> | foo.rb:24
@
=>#0 Foo#forth_call(num1=20, num2=10) @ foo.rb:21 #=> true
#1 block{|ten=10|} in second_call @ foo.rb:8
#2 Foo#third_call_with_block(block=#<Proc:0x00007fc645174d10 foo.rb:7>) @ foo.rb:15
#3 Foo#second_call(num=20) @ foo.rb:7
#4 first_call @ foo.rb:3
#5 <main> @ foo.rb:24
#
=>#0 Foo#forth_call(num1=20, num2=10) # foo.rb:21 #=> true
#1 block{|ten=10|} in second_call # foo.rb:8
#2 Foo#third_call_with_block(block=#<Proc:0x00007fc645174d10 foo.rb:7>) # foo.rb:15
#3 Foo#second_call(num=20) # foo.rb:7
#4 first_call # foo.rb:3
#5 <main> # foo.rb:24
Options 2 - Display Call Site Upfront
With Separator (Not Aligned)
=>#0 foo.rb:21 | Foo#forth_call(num1=20, num2=10) #=> true
#1 foo.rb:8 | block{|ten=10|} in second_call
#2 foo.rb:15 | Foo#third_call_with_block(block=#<Proc:0x00007fc645174d10 foo.rb:7>)
#3 foo.rb:7 | Foo#second_call(num=20)
#4 foo.rb:3 | first_call
#5 foo.rb:24 | <main>
With Separator (Aligned)
=>#0 foo.rb:21 | Foo#forth_call(num1=20, num2=10) #=> true
#1 foo.rb:8 | block{|ten=10|} in second_call
#2 foo.rb:15 | Foo#third_call_with_block(block=#<Proc:0x00007fc645174d10 foo.rb:7>)
#3 foo.rb:7 | Foo#second_call(num=20)
#4 foo.rb:3 | first_call
#5 foo.rb:24 | <main>
This looks good but not really practical because the length of call site also can vary a lot, like
from /Users/st0012/.rbenv/versions/3.0.1/lib/ruby/3.0.0/bundler/rubygems_integration.rb:390:in `block in replace_bin_path'
from /Users/st0012/.rbenv/versions/3.0.1/bin/pry:23:in `<top (required)>'
from /Users/st0012/.rbenv/versions/3.0.1/lib/ruby/3.0.0/bundler/cli/exec.rb:63:in `load'
from /Users/st0012/.rbenv/versions/3.0.1/lib/ruby/3.0.0/bundler/cli/exec.rb:63:in `kernel_load'
from /Users/st0012/.rbenv/versions/3.0.1/lib/ruby/3.0.0/bundler/cli/exec.rb:28:in `run'
from /Users/st0012/.rbenv/versions/3.0.1/lib/ruby/3.0.0/bundler/cli.rb:494:in `exec'
from /Users/st0012/.rbenv/versions/3.0.1/lib/ruby/3.0.0/bundler/vendor/thor/lib/thor/command.rb:27:in `run'
from /Users/st0012/.rbenv/versions/3.0.1/lib/ruby/3.0.0/bundler/vendor/thor/lib/thor/invocation.rb:127:in `invoke_command'
from /Users/st0012/.rbenv/versions/3.0.1/lib/ruby/3.0.0/bundler/vendor/thor/lib/thor.rb:392:in `dispatch'
from /Users/st0012/.rbenv/versions/3.0.1/lib/ruby/3.0.0/bundler/cli.rb:30:in `dispatch'
from /Users/st0012/.rbenv/versions/3.0.1/lib/ruby/3.0.0/bundler/vendor/thor/lib/thor/base.rb:485:in `start'
from /Users/st0012/.rbenv/versions/3.0.1/lib/ruby/3.0.0/bundler/cli.rb:24:in `start'
from /Users/st0012/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/bundler-2.2.15/libexec/bundle:49:in `block in <top (required)>'
from /Users/st0012/.rbenv/versions/3.0.1/lib/ruby/3.0.0/bundler/friendly_errors.rb:130:in `with_friendly_errors'
from /Users/st0012/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/bundler-2.2.15/libexec/bundle:37:in `<top (required)>'
from /Users/st0012/.rbenv/versions/3.0.1/bin/bundle:23:in `load'
from /Users/st0012/.rbenv/versions/3.0.1/bin/bundle:23:in `<main>'
2 Lines
With the amount of information to display, I think having 2 lines for each trace is a reasonable option too.
With Separator
=>#0 foo.rb:21
Foo#forth_call(num1=20, num2=10) #=> true
#1 foo.rb:8
block{|ten=10|} in second_call
#2 foo.rb:15
Foo#third_call_with_block(block=#<Proc:0x00007fc645174d10 foo.rb:7>)
#3 foo.rb:7
Foo#second_call(num=20)
#4 foo.rb:3
first_call
#5 foo.rb:24
<main>
With a 2nd-line indicator (my preference)
I find having an indicator helps me read the lines better. Among all the options, this is my favorite.
=>#0 foo.rb:21
| Foo#forth_call(num1=20, num2=10) #=> true
#1 foo.rb:8
| block{|ten=10|} in second_call
#2 foo.rb:15
| Foo#third_call_with_block(block=#<Proc:0x00007fc645174d10 foo.rb:7>)
#3 foo.rb:7
| Foo#second_call(num=20)
#4 foo.rb:3
| first_call
#5 foo.rb:24
| <main>
Other Options
This issue is just to continue the discussion with some of my ideas. And I believe there are many better options out there. So if you also have any thoughts on this, please leave a comment too π
Update
@ko1 and I have colorized the backtrace and it now looks a lot better π