Ruby

Beyond `puts`: TruffleRuby’s Modern Debugger Using Chrome

We all write bugs. How quickly we can identify & understand them depends on the quality of our tools.

In this talk you'll be introduced to TruffleRuby's modern debugger, based on the Chrome browser's DevTools Protocol. TruffleRuby's uniquely powerful set of tools let you debug, profile, and inspect the memory usage of Ruby code, native extensions, and other embedded languages all at the same time. Support for those tools is zero-overhead so you can have them always enabled. I'll show you how it all works and how it lets you step through Ruby code, inspect local variables, evaluate expressions, and more.

RubyKaigi 2019 https://rubykaigi.org/2019/presentations/nirvdrum.html#apr19

RubyKaigi 2019

00:00:00.060 Looks like we're going to get started here. Hi everyone! For those of you who don't know me, my name is Kevin. I work at Oracle Labs on TruffleRuby.
00:00:05.370 In today's talk, I will discuss debugging. Before I get started, I need to inform you that what I'm presenting is research work coming out of a research group and should not be construed as a product announcement. Please do not buy, sell, or hold stock based on what you hear in this talk today.
00:00:17.730 I want to start by going through a common scenario with print statement debugging. I wanted to come up with something that looked somewhat realistic but also fit on a slide. Here we have a process method that takes an endpoint. We don't know what the type of it is, but we want to turn it into a URI so we can use it with Net::HTTP and check the error codes while updating some kind of database record.
00:00:28.080 This is your first foray into print statement debugging. The interpreter will provide you with information regarding any uncaught exception, and even if the code throws an error, it codifies it with the standard error stream. Rather than just setting the process status code to negative one and saying there was an error, Ruby informs you that there's an ArgumentError. It also gives you the line number where the issue originated and provides a backtrace to help diagnose the problem. In this case, we see it’s a bad argument, so naturally, I'm going to add a print statement to see what the endpoint is.
00:01:15.090 However, now that's not very useful. In this situation, I passed nil, but I don't know what value was coming in, so I'm unsure if it was nil or the empty string. I need to revisit that and define the endpoint properly. Upon calling it again with a real URL, I notice it returns an unexpected format because I used `p` instead of `puts`, but at least the exception is different. To simplify things at this point, I decided to wrap that endpoint check with a new logger statement. Logging is essentially a more sophisticated form of print statement debugging, and it is critical if you require something to monitor your application as it runs. I want to inspect the endpoint; thus, I will include the backtrace.
00:02:28.920 This log statement is bound to look terrible, but the information is there. I noticed an issue with the response code, so I decided to print the response prettily. However, the output is mostly useless unless you know it's a 301 redirect. If you're just starting with the API, I don’t think the information presented here is obvious. Given that I was encountering errors with the response code, I focused on pretty-printing that instead.
00:03:32.040 I realized, oh, the response code is returning a string instead of an integer, so I had to adjust all these conditions. However, now I realized that I wasn't handling the 300 series of codes properly. At this point, I've gathered so many branches in my code; this process can be really overwhelming. If you’ve been at this for a while, your print statements may become increasingly aggressive. Once I've included all of them, I need to `grep` for them because these print statements end up not meaning anything. They’re just ways to uniquely identify points in the code.
00:04:44.500 Print statement debugging is a good first step toward identifying an issue with your code, but you can find yourself in situations where it becomes impractical, especially if you're calling into Ruby gems. Modifying gem code can be very challenging; it's annoying enough just to locate the relevant files. If you're running in a staging environment, you might not even have the permissions to modify those files. Furthermore, when dealing with native extensions, having to constantly add `printf` statements into a C file and then recompile it becomes quite tedious. However, Ruby provides a rich set of APIs for inspecting the runtime.
00:05:56.180 I've listed a small selection of these APIs here, and if you followed the talk on debuggers yesterday, it provided a solid approach to how you can build debuggers on top of this foundation. I believe the next stage for many developers is to embrace debuggers. While researching Visual Studio Code, I found a humorous statement on their website. I don’t think print statement debugging is a thing of the past, but for many developers, I don’t think they have progressed past it. You can advance quite far in your career using print statements since it’s simple and works in every language. People tend to develop their own heuristics and approaches to debugging.
00:06:52.440 However, using specialized tools like a debugger can provide a richer debugging experience. If you’re unfamiliar with them, debuggers are tools that allow you to inspect a program's state and evaluate expressions without modifying the source code. In the screenshot, you can see a line of code being inspected while the program is running, showing the call stack and a set of local variables. Ruby has many debuggers available, but the problem is MRI seems to largely defer to third-party implementations. This has created a confusing situation; if you visit rubygems.org and search for debugger, over 100 responses will come back, many of which don’t even pertain to Ruby debugging.
00:08:09.300 It can be intimidating for someone new to know where to start, and much of the documentation available is outdated since many Ruby gems are only compatible with Ruby 1.8 and Ruby 1.9. I think a new standard for debuggers is emerging from the Chromium project called the Chrome DevTools Protocol. This protocol consists of a set of instrumentation and inspection APIs used by Chromium-based browsers and was designed to inspect JavaScript. Interestingly, they chose to define a communication layer instead of directly tying the UI to the runtime.
00:09:16.060 So, you can see this little interaction diagram where the JavaScript runtime talks to the browser through the Chrome DevTools Protocol. Since this is a protocol, it defines a way for two components to communicate; can we swap out the long-time code? I have a little demo here.
00:10:10.330 What you see here is a TruffleRuby program running within Chrome. The sample is relatively simple, but I want to show some different depths of the stack frame and how you can call up and manipulate them. The cool part is we can swap out the JavaScript runtime with TruffleRuby, which is great! But can we also swap out the Chromium-based browser?
00:11:00.740 It turns out that Visual Studio Code ships with a debugger that speaks the same protocol. Thus, with no additional effort from us, we now have a debugger that works with two different environments. I'm going to step through these, and we can observe the call stack and roll back through it. Because TruffleRuby is implemented about 50% in Ruby, we can also step into core methods that MRI would typically prevent you from executing, offering additional insights into how your application runs.
00:11:57.550 However, there's one slightly odd issue in Visual Studio Code; it assumes that the protocol is being spoken to by JavaScript. So, while everything works fine, it attempts to syntax highlight as JavaScript even though it's a `.rb` extension. We hope to work with them to resolve this. You can also see here how the filename shows as `string.rb` but is categorized as JavaScript.
00:12:56.940 Currently, we have TruffleRuby able to dynamically communicate with a variety of browsers based on Chromium and with Visual Studio Code, all thanks to our ability to communicate through this protocol. We do not need to build out a debugger UI ourselves, which can be a cumbersome task. In reality, it’s not just Truffle that’s handling this; it’s GraalVM. To elucidate, the Chromium project is a collection of many things, and I want to focus on three components. First, Truffle serves as a toolkit for building language runtimes based on self-optimizing AST interpreters, and it's essentially the first part of the name TruffleRuby.
00:14:59.500 It’s how we've implemented TruffleRuby, so every language built with Truffle and running on GraalVM is inherently polyglot. These languages derive from a common hierarchy of AST nodes to allow for mixing and matching between them. Finally, when the JIT gets involved, it can inline all of that, allowing for cross-language programming without additional overhead.
00:16:09.080 To support this polyglot functionality, we have developed a protocol for interoperability. This means one language can send messages to another language to inspect, read, write, and execute foreign objects. For instance, in Ruby, if we have a reference to a JavaScript array and we call `.size` on it, that translates into a `getSize` message, which is a part of our interoperability protocol. When JavaScript handles this request, it returns a set of nodes able to read the size of that JavaScript array, and these nodes get incorporated into the Ruby interpreter. This means you receive the benefits of debuggers for free if you build with Truffle.
00:17:51.020 Usually, when we talk about Truffle and Graal, we mention performance, emphasizing that you receive a compiler for free. However, because language interoperability is a core aspect of Truffle, you also receive the advantage of a debugger at no additional cost. I feel it's crucial to highlight that our goal isn’t merely to construct the fastest runtime; we want to create the best environment and platform for numerous languages while providing you with the tools to focus on the semantics of your language without a continuous need to reinvent the wheel.
00:19:19.300 Presently, GraalVM provides interpreters for JavaScript, TruffleRuby, R, Python, and a project called Seulong, which functions as an LLVM bitcode interpreter. We can compile C, C++, and Rust to LLVM bitcode. Because GraalVM offers a common interface, all of these languages come with free debugging capabilities. For a demonstration, I’ll utilize Sam Saffron's fast blank extension. This extension is straightforward and ideal for showcasing properties.
00:21:46.020 If you're not familiar with it, it's a native extension that replaces the blank method from Rails. Saffron discovered that this method was creating a bottleneck in Discourse, prompting him to develop a native extension. I will call the `blank` method and step into its implementation, and just like that, we have transferred over to C code, enabling us to debug extensions directly from Ruby. One slight inconvenience is that we recently improved compatibility with C extensions by providing our wrappers. When you see a string value in C code, it actually represents the wrapped object.
00:23:49.480 Upon delving deeper, you can see that it’s a foreign object. In this case, the Ruby string being processed in C code has the value `abc`. A moment ago, the encoding appeared as nil, but now we can see it's UTF-8. We need to tidy this up and simplify it by effectively unwrapping those value types, as the wrappers can complicate understanding. Furthermore, we can explore these stages, where each step leads to our intended outcome.
00:24:53.650 Next, I will check if we can execute C++. This turns out to be significantly more complex. I am utilizing the unf extension for this part.
00:26:00.360 Here, we can observe the extension initializing, creating the UNF module, and the normalization class. It then defines methods and subsequently constructs these constant values as symbols. We can step down to the next level, as this incorporates singletons along with additional functionalities. Finally, we can dive into the normalization methods, applying an extended check for the type of normalization being performed.
00:27:19.640 The current state of C++ integration isn't fully refined, but it works sufficiently for our purposes. To recap, I believe print statement debugging is an excellent first step toward debugging any issues. It's rapid and easy, with everyone familiar utilizing it effectively. However, it has limitations as it can be time-consuming to narrow down your focus. A debugger allows you to observe all local variables, method arguments, and even roll back stack traces.
00:28:59.060 I believe enhancing the developer experience encompasses a lot more than mere performance concerns. With TruffleRuby, we're attempting to tackle this from multiple angles. Similar to Rubinius, much of Ruby is implemented in Ruby itself. So when scrutinizing core methods, you get to see what your code is doing in detail. We've aimed to make seamless cross-language debugging achievable; while discussions about polyglot programming abound, the practice can be difficult as the separate languages and environments may not seamlessly integrate.
00:30:18.300 Being able to debug native extensions directly from Ruby is a critical advantage. This isn’t easily accomplished with MRI, where there exists a stiff division between the Ruby environment and native code. In conclusion, while debuggers benefit greatly from VM support, VM-specific support requires substantial effort. The challenge lies in not overfitting your debugging support to a specific bytecode.
00:31:21.260 Java has a robust debugging capability because every JVM has an outstanding debugger included. However, for any alternate programming language on the JVM, that debugger might not be as effective. Conversely, Ruby—specifically MRI—over-relies on third-party libraries. It's still early days, but if all Ruby implementations could reach an agreement and offer their support for the Chrome DevTools Protocol, we could streamline debugging. As it stands, existing debugging libraries tend to work with one alternative implementation.
00:32:13.380 I think the Chrome DevTools Protocol offers a straightforward method for all implementations to attain the same standard of support. Moreover, I want to extend my appreciation to the sliding team members for their assistance and contributions to this project. Thank you! I'm happy to take any questions.
00:34:52.129 So regarding your talk, I'm curious if there's any performance loss when running C or C++ on GraalVM?
00:35:20.000 Executing in the interpreter is relatively slow initially, but loading should optimize well with the JIT compiler. Extensions created in C and C++ essentially become other AST nodes that you can inline into Ruby calls. Thank you!
00:36:00.000 You mentioned two key topics: the Chrome DevTools protocol and polyglot debugging. If this can be implemented in Ruby MRI, what do you think would be the most challenging aspect?
00:37:22.000 Probably the most difficult part would be getting agreement from the community to implement it. There’s a risk involved with adopting a newer protocol and having to support it long-term.
00:38:01.600 So, could third parties make a Ruby debugger for Chrome DevTools?
00:39:00.000 They could, but MRI should provide better tools to create high-performance debuggers. It might involve solving existing issues with the Ruby debugging ecosystem first.
00:39:28.000 I was also curious if it's possible to roll back not just to the previous stack frame within the current language but also to the last call that got you into that language?
00:40:11.000 Yes, that’s attainable. It’s still worth noting that at present, we have a stack frame type that can manage stack traces and frames.
00:40:55.000 Could you explain a bit more about how rolling back stack frames operates in your specific setup?
00:41:45.000 Currently, Truffle Ruby provides support for its own stack frame type, which can manage stack traces and realize frames.
00:43:05.000 However, demonstrating it while talking might break my flow, so let me get back to that later.
00:43:50.000 Additionally, is there a way to access the previous stack frame without conducting any troublesome adjustments?
00:44:10.000 We have a built-in implementation of `binding_of_caller`, but we need to enhance its integration with our own implementation.
00:44:25.000 Lastly, while there exist certain parts of Truffle Ruby in Java, is there currently any method to access those features?
00:44:39.000 Not currently, but you are able to call all methods directly on Truffle Ruby.
00:44:56.000 Thank you for your insights, as I've found this conversation very informative!
00:45:18.000 Thank you, everyone!