debug is Ruby's new default debugger included in Ruby 3.1. This new debugger has replaced byebug in Rails 7. Not only does debug provide us with a wide range of functionality, but it also provides some advanced features.

In this article, we will explore and understand a few advanced features of debug.

1. Seamless integration with VSCode

Below steps can be followed to integrate debug gem in VSCode.

  • Install extension VSCode rdbg Ruby Debugger - Visual Studio Marketplace in your VSCode.
  • Open the file you want to debug in the VSCode.
  • Register the breakpoint by clicking on the line you want to set the breakpoint and press the F9 key.
  • Start the debugging by pressing the F5 key or by selecting Start debugging in the Run menu.

2. Postmortem debugging to debug the dead Ruby process

When you debug a program and an exception is raised, the debugger will exit immediately. To avoid this, we can make use of the postmortem feature.

Let's say we have a simple program to calculate the percentage.

  # computation.rb
  def percentage(numerator, denominator)
    (numerator / denominator) * 100.0
  end

  # call the above function by passing the denominator as 0
  percentage(10, 0)

When you run the program with the debugger and continue the debugging process, the debugger will suspend immediately throwing ZeroDivisionError.

  # start the debugging using `rdbg` command
  19:22:35:~/Users/Examples  >% rdbg computation.rb
  [1, 6] in computation.rb
  =>   1| def percentage(numerator, denominator)
       2|   (numerator / denominator) * 100.0
       3| end
       4|
       5|
       6| percentage(10, 0)
  (rdbg)

  # continue the debugging process
  (rdbg) continue
  Traceback (most recent call last):
    2: from computation.rb:6:in `<main>'
    1: from computation.rb:2:in `percentage'
  computation.rb:2:in `/': divided by 0 (ZeroDivisionError)

  # debugging process gets suspended
  19:54:15:~/Users/Examples

To remain in the debugging mode even after the exception is raised, we can use the postmortem feature. Run the program with the debugger, then set the configuration with the command config set postmortem true.

Start the program in debugging mode.

  # start the debugging using `rdbg` command
  20:02:38:~/Users/Examples  >% rdbg computation.rb
  [1, 6] in computation.rb
  =>   1| def def percentage(numerator, denominator)
       2|   (numerator / denominator) * 100.0
       3| end
       4|
       5|
       6| percentage(10, 0)
 (rdbg)

Enable the postmortem feature.

  # command to enable postmortem
  (rdbg) config set postmortem true
  postmortem = true

Continue the program and the exception will be raised, but the debugger won't be suspended.

  # continue the debugging process
  (rdbg) continue    # continue command
  Enter postmortem mode with #<ZeroDivisionError: divided by 0>
    computation.rb:2:in `/'
    computation.rb:2:in `percentage'
    computation.rb:6:in `<main>'

  # debugger is still active
    (rdbg:postmortem)

Backtrace the issue with bt command.

  # command to backtrace
  (rdbg:postmortem) bt
  =>#0  [C] Integer#/ at computation.rb:2
    #1  Object#percentage(numerator=10, denominator=0) at computation.rb:2
    #2  <main> at computation.rb:6
  (rdbg:postmortem)

3. Supports record & replay debugging

This feature allows recording the execution information. Using this we can go back to the execution again by using step back command. This will help us to check the last state of the program before the breakpoint.

Let's take a simple example that calculates simple interest. Add a breakpoint at the start of the function.

  # computation.rb
  def calculate_simple_interest
    debugger

    puts 'Enter investment:'
    investment = Integer(gets.chomp)

    puts 'Enter interest rate:'
    interest_rate = Integer(gets.chomp)

    puts 'Enter number of years:'
    time = Integer(gets.chomp)

    interest = (investment * interest_rate * time) / 100
    puts "Interest amount is: #{interest}"
  end

  # call to the above function
  calculate_simple_interest

Run the program and start the debugging.

  # start the debugging using `rdbg` command
  18:09:24:/Users/Examples  >% rdbg computation.rb
  [1, 10] in computation.rb
  =>   1| def calculate_simple_interest
       2|   debugger
       3|
       4|   puts 'Enter investment:'
       5|   investment = Integer(gets.chomp)
       6|
       7|   puts 'Enter interest rate:'
       8|   interest_rate = Integer(gets.chomp)
       9|
      10|   puts 'Enter number of years:'
  =>#0  <main> at computation.rb:1
  (rdbg)

To start the recording, execute the command record on.

# command to start the record
(rdbg) record on
Recorder for #<Thread:0x00007fef0c85fb58 run>: on (0 records)
(rdbg) 

Using next command go to the end of the program and check the values of all the variables using info command. You will be able to see the values of all four variables.

  # command to move the breakpoint to the new line
  (rdbg) next
  [9, 17] in computation.rb
       9|
      10|   puts 'Enter number of years:'
      11|   time = Integer(gets.chomp)
      12|
      13|   interest = (investment * interest_rate * time) / 100
  =>  14|   puts "Interest amount is: #{interest}"
      15| end
      16|

  # command to list the values of the variables
  (rdbg) info
  %self = main
  investment = 12
  interest_rate = 12
  time = 12
  interest = 17
  (rdbg)

Now, to go back to the last state, enter the command step back. Now you will only be able to see values of investment, interest_rate & time.

  # command to move to the previous state
  (rdbg) step back
  [replay] [8, 17] in computation.rb
  [replay]      8|   interest_rate = Integer(gets.chomp)
  [replay]      9|
  [replay]     10|   puts 'Enter number of years:'
  [replay]     11|   time = Integer(gets.chomp)
  [replay]     12|
  [replay] =>  13|   interest = (investment * interest_rate * time) / 100
  [replay]     14|   puts "Interest amount is: #{interest}"
  [replay]     15| end
  [replay]     16|
  [replay]     17| calculate_simple_interest

  # command to list the values of the variables
  (rdbg) info
  [replay] %self = main
  [replay] investment = 12
  [replay] interest_rate = 12
  [replay] time = 12
  [replay] interest = nil
  (rdbg)

You can move back again to the previous state by entering the command step back. Now you will only be able to see the value of investment & interest_rate.

  # command to move to the previous state
  (rdbg) step back
  [replay] [6, 15] in computation.rb
  [replay]      6|
  [replay]      7|   puts 'Enter interest rate:'
  [replay]      8|   interest_rate = Integer(gets.chomp)
  [replay]      9|
  [replay]     10|   puts 'Enter number of years:'
  [replay] =>  11|   time = Integer(gets.chomp)
  [replay]     12|
  [replay]     13|   interest = (investment * interest_rate * time) / 100
  [replay]     14|   puts "Interest amount is: #{interest}"
  [replay]     15| end

  # command to list the values of the variables
  (rdbg) info
  [replay] %self = main
  [replay] investment = 12
  [replay] interest_rate = 12
  [replay] time = nil
  [replay] interest = nil
  (rdbg)

Comparing the performance of the debug gem with the other debuggers

We already have existing debuggers like lib/debug.rb, byebug, debase, etc. The reason to use the new debugger is its performance.

Let's have a simple function to find the Fibonacci series.

def fibonacci(n)
  if n < 0
    raise # breakpoint
  elsif n < 2
    n
  else
    fibonacci(n - 1) + fibonacci(n - 2)
  end
end

# running above method in the irb
irb(main):010:0> require 'benchmark'
irb(main):011:0> Benchmark.bm { |x| x.report{ fibonacci(35) } }

Here are the benchmark scores in seconds for various debuggers for the above example.

Without Breakpoint (sec) With Breakpoint (sec)
rdbg(debug.gem) 0.92 0.92
Ruby < 3.1 0.93 N/A
RubyMine 0.97 22.6
Byebug 1.23 75.15
old lib/debug.rb 221.88 285.99

Looking at the comparison in the above benchmarks, we can clearly notice that the new debugger is much faster than the rest of the debuggers.


References