Stan Lo

ruby/debug - The best investment for your productivity

RubyKaigi 2022

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!