00:00:00.780
Hello everyone, welcome to my talk about Ruby Debug, the best investment for your productivity.
00:00:04.140
Let me start by introducing myself. My name is Stan. I'm Taiwanese but I just moved to London last year.
00:00:11.160
I have attended every single RubyKaigi, in person or online, since 2015. So, this is my eighth RubyKaigi in a row, and it's also my third time speaking here. Thanks so much for hosting such an amazing conference and for having me back.
00:00:28.980
Currently, I work on Shopify's Ruby Developer Experience team. We build and maintain tools like the Ruby LSP, the Ruby Extensions Pack for VS Code, and other related tools like Tapioca.
00:00:40.920
I'm an active contributor to Ruby's shiny new debugger, Ruby Debug, and this is why I'm giving this talk today. I'm also a maintainer of the Century Ruby SDK.
00:00:54.180
After I moved to the UK, I started a new hobby to explore pubs, especially in London. My goal is to visit at least 100 pubs by the end of this year, and currently, I have visited 87 of them.
00:01:11.280
If you want to ask me any questions or just want to have a drink together when you visit London, feel free to ping me on Twitter.
00:01:25.979
So, why is debugging important? I wanted to start this talk by addressing that question, but I realized it's actually not the right question to ask, because we already know how crucial debugging is in our work.
00:01:50.220
The real question is, do we keep investing in our debugging skills and keep them sharp? I want to talk about this because in the first seven years of my programming career, I only knew one tool and one way to debug, which was using Pry.
00:02:06.420
I only knew two commands, which were 'ls' and 'show source.' I'm not saying they didn't work; otherwise, I wouldn't still be a Ruby programmer today. But after I learned some common debugger features like state debugging and frame navigation, I really regretted not spending more time on my debugging skills.
00:02:25.680
So, that's how I used to debug in the past. How about other Ruby developers? Let's see some data.
00:02:32.640
The data I'm showing you is from the latest Ruby community survey by Planet Argon. It collected more than 2,000 participants' answers, so I think it should be very representative.
00:02:42.959
According to the results, half of the participants use Pry as their go-to debugger, while Byebug and Ruby Debug together account for about 43 percent of last year.
00:02:54.360
But what surprised me here is the question about what people use as their debuggers. The most popular answer is actually not a debugger; in fact, it never claimed to be one. It's Ruby's `irb` or `pry`, which aims to be an IRB alternative.
00:03:19.740
This made me realize that perhaps many Ruby developers, just like I used to, are missing out on all the convenient features that conventional debuggers have. That's why in this talk, I will show you some of the most important debugger features. At least in my opinion, they are step debugging, frame navigation, and breakpoint commands.
00:03:37.080
I promise you that once you learn how to use them, you'll be able to greatly increase your debugging productivity. These are very common debugger features, and both Byebug and Ruby Debug support them in very similar ways.
00:03:51.480
Debuggers in other programming languages also support them in one way or another, so learning these skills will be very helpful, regardless of what tool you end up picking, as long as it's a debugger.
00:04:03.660
In addition to these features, I will also show you one of Ruby Debug's best features: scriptable breakpoints. But before I demonstrate the features, let's take a look at the example code.
00:04:20.640
First, it loads leap.rb to simulate library loading, and then it calls a method that comes from the library. We will do a simple comparison to see if the result is what we expected.
00:04:34.640
Because we want to use Ruby Debug as our debugger, we first need to require it, and then to start a debugging session, we place a breakpoint before the first method.
00:04:46.440
If you are a Pry or Byebug user, this usage should feel very familiar to you. After we run the program and hit the breakpoint, the debugger should open up a console.
00:04:50.280
We should stop at the breakpoint we set, as you can see on the slide. But before we proceed, let me quickly go through a few of its UI components.
00:05:07.320
The first thing we will notice is the display of the program's source code. Ruby Debug displays 10 lines of source code by default, and in the upper left corner, it will show us the file and line numbers we are looking at.
00:05:27.480
On the left-hand side, we have a small arrow as the line indicator. At the bottom is the console's prompt, and finally, just above the prompt, the debugger displays the closest two frames.
00:05:42.300
In this case, it only displays one because we are not inside any method or block yet. So, the first feature I'm showing you is step debugging.
00:05:50.640
When we do step debugging, it's like micromanaging our program; it only moves when we tell it to, moving by the exact distance it's told—usually a single method or a line.
00:06:14.280
We always follow our program to its next step, and it mainly relies on two commands: `step` and `next`, which I'm going to demonstrate here.
00:06:30.540
First, because there's nothing else on line four other than the breakpoint, when we run the step command, it will just step to the next line, and we are now at line five.
00:06:43.920
Sorry, but it is at the beginning of the line. When we do step debugging, we usually land at the beginning of a line, so at this moment, the full method is actually not code yet.
00:07:03.600
Now, let's step into the full method. After stepping, we are now at the full method's definition in the leap.rb, and that's the beauty of step debugging.
00:07:17.880
We follow the program's steps instead of trying to intercept it by placing breakpoints in different files. We can also do multiple steps at a time.
00:07:34.020
Since we know there is nothing special in the `bob` method, we can use `step` to enter the `bob` method directly.
00:07:48.300
Now we are in the `bob` method, which has two lines. Assuming we are not interested in stepping again, we just want to go to the next line; in this case, we can use the `next` command.
00:08:05.760
It will execute the `plus_one` method, wait for it to return, and then go to the next line. Now we are still inside the same method but at a different line.
00:08:23.520
Let's use Ruby Debug's `info` command to see what variables are available here. The first line is a special hint from the debugger; the percentage symbol always represents the current scope itself, and in this case, it is the main object.
00:08:39.660
The next line shows `n = 100`, which is the method argument, and the final line shows that the `num` variable is 99. This means the `plus_one` method did exactly what it's supposed to do.
00:08:52.920
If you are as lazy as I am, using the `info` command to get a quick overview on variables will be very handy when debugging.
00:09:11.640
In addition to using debugger commands in the console, we can also inspect variables directly or evaluate Ruby code like we do in an IRB.
00:09:25.440
Finally, there is another one of my favorite commands, which is `ls`. It works the same as the ones in IRB and Pry; it shows you all the available methods and state of an object in the current scope.
00:09:39.720
When we look at step debugging from the stack perspective, we see that when we step into methods, we always stay at the top of the stack. But what if we want to go back to a previous frame?
00:09:56.760
Well, then we need the next debugger feature: frame navigation. It's the best friend of step debugging because we usually use them together.
00:10:05.520
Frame navigation allows us to move back to previous frames. You can use it to investigate things like how arguments were generated or what value triggered an `if` statement, etc.
00:10:22.080
But we can only do this in frames that are still on the stack. For example, we can't access the `plus_one` method's context anymore.
00:10:39.000
To move between frames, we use the `up` and `down` commands. But there's a catch: to move down a frame, we actually need to type `up`, even though visually we're moving down, which could be confusing.
00:10:54.420
But I also didn't draw the stacks wrong. If we type the `bt` (backtrace) command, we can see the frames are displayed upwards too, and many debuggers—including Byebug and debuggers in other programming languages—work like this.
00:11:12.960
Unfortunately, I have no good answer to explain this difference in direction, but I have a tip to help us remember it.
00:11:25.440
When we look at the output of the `bt` command, we see that each frame has an ID. So when we type `up`, it doesn't mean the direction on the stack; instead, it means to go up by the frame's ID.
00:11:42.180
The same applies to the `down` command. It is to go down to a frame with a lower ID. If you still can't remember this, it is like using a computer with a different mouse scroll direction, and you'll just get used to it after a while.
00:11:54.960
Let's see how it looks inside Ruby Debug's console. Assume we have stepped into the double method. If we type `up`, we will move back to the `bob` frame, and to return to the top of the stack, we can type `down`.
00:12:11.700
Finally, we can move to any frame by checking its ID with the `bt` command we just used. Then we type the `frame` command with the ID. In this case, if I type `frame 4`, it will take us all the way back to the main frame.
00:12:24.540
With this technique, we have another direction to investigate what happens inside our program.
00:12:42.660
So when we do step debugging, we can observe our program going forward, and then by inspecting the frames left on the stack, we can, to some degree, go backwards as well.
00:12:59.760
If we finish the investigation and want to resume the program, we can just type `continue` or `c` to continue the program.
00:13:02.700
Here are all the commands we have used so far.
00:13:04.940
While the two techniques we just learned are already super powerful, they only concentrate on a very small area of our program. When debugging real-world applications, we also need a way to move around different parts of our codebase.
00:13:29.130
Let me explain this with an example. Let's say we just stopped at the `posts_controller`'s `show` method, and after some investigation, we suspect that the `book` may be related to the post's `title` method.
00:13:47.040
How do we investigate it? You may think, 'Okay, I'm going to step into it.' But if the method call happens a few hundred calls away, like in a view file, then you need to step a few hundred times before you reach it.
00:14:05.880
It's very inefficient, and it's bad for your keyboard. There is also another way that many people may choose, which is to open up the `post.rb` file and set another breakpoint in the `title` method.
00:14:25.920
I call this the 'private way' because this is what I always did when I used Pry. It may be quicker than stepping all the way there, but it's still a lot of manual work and switching between the terminal and editor.
00:14:42.540
Let me propose a better way. Since we already know where we want to go, why don't we let the debugger take us there? We can tell the debugger to step into the `title` method when it's called by typing `break post.title`, which will set a breakpoint there.
00:14:59.640
It's similar to the Pry way, but this time we can do it with one command. Once we set the breakpoint, we can simply continue the program and wait for the method to be called.
00:15:20.760
Ruby Debug supports many types of breakpoint targets. We can specify locations, methods, or even exceptions with the commands to add breakpoints. You also have commands to help you list or delete breakpoints.
00:15:36.780
After learning about breakpoint commands, we can have a complete debugging workflow inside a single debugging session, which works like this: First, we investigate at our initial breakpoint.
00:15:55.560
When we have collected the information we need, we tell the debugger our next destination by setting a breakpoint with commands. Then we continue the program and stop at the destination.
00:16:12.480
After that, we start another round of investigation. During this entire process, we can investigate multiple locations without leaving the debugger.
00:16:29.760
This will help us reduce the number of program executions and also reduce distractions caused by switching between the editor and terminal.
00:16:46.440
Now that we have learned some common debugger features, I want to show you one kind of feature of Ruby Debug. By using it with the debugging techniques we just learned, we can build a powerful debugging workflow.
00:17:07.920
After a breakpoint, a typical debugging session usually consists of two parts: known actions and unknown actions. Common known actions are inspecting variables, inspecting a certain message result, or setting breakpoints via commands.
00:17:24.600
Even though we already know what we want to do here, we still need to type and execute them manually, one by one, and learn in every single debugging session.
00:17:43.440
This can be annoying and also prone to human errors like typos. Fortunately, in Ruby Debug, we can automate this.
00:17:59.520
When we write down our debugging commands using `biden.b`, we can also program our debugging workflow. Whenever we use Ruby Debug's breakpoints, we can provide either a `pre` or a `do` keyword argument.
00:18:18.180
For `pre`, the debugger will execute the command when the breakpoint is hit and then stop there. For the `do` keyword, the debugger executes the command and then continues the program.
00:18:35.940
When we provide input to those keywords, it can be one or multiple commands; we need to use double semicolons to separate multiple commands. Finally, both the `pre` and `do` keywords are available for breakpoint commands too.
00:18:56.880
Let me demonstrate this feature using ActiveSupport's message encrypter. We start by initializing two message encryptors, and for the new encryptor, we tell it to rotate to the old secret if it fails the initial decryption.
00:19:16.440
To test if the rotation actually works, we let the new encryptor decrypt a message from the old encryptor, which is up-to-date, and we expect the script to output 'message is new' on the screen.
00:19:37.500
But when we actually run the script, it raises an exception instead. So, how can we investigate this?
00:19:53.640
I usually start my debugging session by inspecting the surrounding variables, so I will start with the `info` command. Then I want to investigate the `decrypt_and_verify` method, so I would set a breakpoint there.
00:20:08.520
Once I'm inside that method, I want to run the `info` command again. Let's tell the debugger to do all this for us.
00:20:32.520
First, we start by using the `do` keyword because I want to enter the next breakpoint once the commands here are completed. We want it to run the `info` command in the current context first.
00:20:52.920
Then, we set the breakpoint on the `decrypt_and_verify` method. Lastly, we can use the same `pre` keyword for the breakpoint command.
00:21:15.060
Once we enter the new breakpoint, it will run the `info` command again. When we run the program, it first executes the `info` command and then stops at the breakpoint.
00:21:44.040
After finishing the scripted commands, the debugger continues the program and triggers the `decrypt_and_verify` method, at which point it runs the second `info` command.
00:21:57.420
Now we can start typing in the console with all the information that has been presented to us.
00:22:09.420
What are the benefits of scripting our debugging steps? First, it reduces manual typing in the console, which minimizes human errors and frustrations caused by typos.
00:22:31.080
Secondly, it helps us plan our debugging process ahead of time. Third, by writing these commands inside an editor, we may benefit from things like auto-completion, or we can even write snippets for frequently used commands.
00:22:50.280
Finally, writing down our workflow commands helps others understand and even reproduce it. This has great potential, as it can also be used in bug reports.
00:23:05.760
Imagine not only providing a reproduction script but also an investigation script to walk through the problem with more details.
00:23:30.960
Before the end of this talk, let me quickly explain why Ruby Debug should be your go-to debugger. Let's talk about its advantages first.
00:23:54.840
First, it is maintained by the Ruby core team, so it constantly adopts new functionalities added in new Ruby versions, such as new TracePoint events.
00:24:12.420
Second, it has colorized output and powerful breakpoints, as I showed you in this talk. Additionally, it has powerful tracers which I don't have time to mention today, but I'll save that for another talk or a few articles.
00:24:27.840
It also has advanced remote debugging support. This feature is increasingly important as we move towards containerized or even cloud-based development environments, making remote connection to our debugger very helpful.
00:24:47.760
Finally, it integrates natively with VS Code, which is the most popular editor in the community right now.
00:25:10.200
However, it also has a few disadvantages, at least for now. Firstly, it does not work with fibers yet. So if your application uses the Async library or the Falcon web server, it won't work with Ruby Debug.
00:25:23.640
Secondly, compared to Byebug, Ruby Debug's thread control is less flexible. It always stops all threads and doesn't allow resuming or stopping individual threads.
00:25:40.200
However, for most use cases, this may not be too critical. Finally, since Ruby Debug is still quite new—just about a year old—it doesn't have much learning material available yet.
00:25:57.840
I hope we can all improve this together by starting to use it and sharing our experiences with the community.
00:26:18.720
Finally, as this is too important, I accidentally placed the same slide twice, so let's go through them again. You should really use Ruby Debug as your go-to debugger.
00:26:34.080
If you use Byebug and want to migrate to Ruby Debug, I wrote an article with side-by-side command comparisons that should make this migration easier.
00:26:46.560
Lastly, let's do a quick recap. If you can take away only one thing from this talk, it's to take state debugging and frame navigation.
00:26:51.340
I count them as one thing, as they are usually used together. With them, you will be able to do more detailed investigations while debugging.
00:27:01.020
If you can take away another thing, please also try breakpoint commands. Being able to set breakpoints in the console will keep our flow uninterrupted.
00:27:14.220
Finally, use scriptable breakpoints to program our debugging workflow. We like to automate what we do, so why not do it for debugging as well?
00:27:26.220
Thank you so much for listening to my talk. Happy debugging!