Ruby Debugger Internals
This is a modal window.
The media could not be loaded, either because the server or network failed or because the format is not supported.
Formal Metadata
Title |
| |
Title of Series | ||
Part Number | 67 | |
Number of Parts | 94 | |
Author | ||
License | CC Attribution - ShareAlike 3.0 Unported: You are free to use, adapt and copy, distribute and transmit the work or content in adapted or unchanged form for any legal and non-commercial purpose as long as the work is attributed to the author in the manner specified by the author or licensor and the work or content is shared also in adapted form only under the conditions of this | |
Identifiers | 10.5446/30699 (DOI) | |
Publisher | ||
Release Date | ||
Language |
Content Metadata
Subject Area | ||
Genre | ||
Abstract |
|
RailsConf 201567 / 94
1
4
7
8
9
10
11
13
14
16
17
19
21
24
25
29
30
33
34
35
36
37
39
40
42
47
48
49
50
51
53
54
55
58
59
61
62
64
65
66
67
68
70
71
77
79
81
82
85
86
88
92
94
00:00
Line (geometry)CodeMultiplication signMetropolitan area networkFunctional (mathematics)Heat transferSystem call2 (number)Variable (mathematics)Group actionDifferent (Kate Ryan album)Event horizonSocial classObject (grammar)Parameter (computer programming)Software bugMeasurementIterationPerformance appraisalCartesian coordinate systemOrder (biology)outputFunction (mathematics)Block (periodic table)DebuggerComputer fileKeyboard shortcutFrame problemExpressionComputer programmingMoment (mathematics)Virtual machineSequelCore dumpMotion capturePosition operatorContext awarenessSoftware frameworkPoint (geometry)Exception handlingSound effectView (database)CASE <Informatik>Default (computer science)Directed graphAutocovariancePressureTrailNetwork topologyInformationKernel (computing)BootingThread (computing)BenchmarkAdditionWrapper (data mining)Demo (music)Computer animationLecture/Conference
09:58
DebuggerBlock (periodic table)Wrapper (data mining)Event horizonInformationFrame problemHookingKeyboard shortcutPointer (computer programming)Pairwise comparisonMoment (mathematics)Computer fileGraph (mathematics)Scripting languagePoint (geometry)Line (geometry)Different (Kate Ryan album)Multiplication signElectronic mailing listNetwork topologyObject (grammar)Goodness of fitState of matterVirtual machineObject-oriented programmingBefehlsprozessor2 (number)Graph coloringSheaf (mathematics)Computer programmingSet (mathematics)Interpreter (computing)Tracing (software)Software bugTwitterSocial classBitLimit (category theory)BenchmarkOpen setMotion captureSuite (music)Digital Equipment CorporationBasis <Mathematik>ArmMetropolitan area networkOrder (biology)Front and back endsControl flowSlide ruleMereologySystem callPresentation of a groupOptical disc drivePower (physics)Computer animation
19:45
NumberComputer animation
Transcript: English(auto-generated)
00:13
My name is Denis Ushakov and I live in St. Petersburg, Russia and basically that's awesome
00:23
because in summertime St. Petersburg looks like this. But I should warn you, if you want to go to St. Petersburg in any other season, you probably would be disappointed because it looks like this.
00:44
So I've been working at JetBrains for six years and I do lots of different stuff and one of the things I do is working on debugging support for RubyMine. So I decided that would be a good opportunity to tell you about the things I know
01:05
about debugging API and debugging internals. So, if you want to do a debugger, you need to be able to answer two basic questions. The first one is where are we?
01:22
So in order to debug to hit your breakpoint or provide you some information, it should know where in the code it's positioned, like the file and the line. And the second question is what's going on? We need to know whether we have an exception raised and what variable values we have
01:44
and stuff like that. So let me start with a super small demo. So here's a really, really small program. And that's how we look the most simple scenario of debugging.
02:05
We put a breakpoint on some line and then we execute our program and it stops on that line. And we can see variable values. We can see the global variables.
02:20
We can see the stack frames and we can switch between the stack frames and also we can evaluate different expressions. So that was the two big questions.
02:43
But there's also one very, very important side question. That's the speed because we don't want debugger to be super slow. We want it to be as fast as possible, ideally as fast as the program without debugger.
03:02
So during the talk, I'm going to be doing some measurements and I'll take a small, really simple program with lots of iterations. And in order to avoid input-output effects, I will do no output in that program.
03:32
And I'm going to be measuring the execution using the default Ruby benchmark framework.
03:42
So here is our really simple, really small program with lots of iterations. To set the benchmarking, I'll need to run it without any debugging API enabled. And on my machine, it takes about five seconds.
04:03
Not bad. So how are we going to understand where we are at the moment? Ruby 1.0 introduces really simple and handy function. It's called setTraceFunc. It takes a proc and every time an event happens, a Ruby VM event happens,
04:26
your block gets called and you get event, file, line, ID, binding, and class name. File and line are pretty obvious. ID is the method name that's currently executed.
04:42
Class name is the class of the object in which that method is executed. And let's take a deeper look at the two other arguments. The first one is event, and there are seven groups of different events.
05:02
The most basic and the most often happening event is line event. It gets called on every line, on almost every line of your code. And sometimes it gets called twice for a line, but usually one line, one line event. The second group is call and return.
05:23
These two are generated when a Ruby VM calls a method or returns from a Ruby method. SQL and C return. These are basically the same except they are generated when you are executing a C function.
05:46
Class and end are generated when a Ruby VM starts interpreting the class body and end when the class body ends. Rise event is generated when the exception happens.
06:02
And Ruby 2.0 generates two additional classes, like bcall and breturn, which are generated when the execution enters the block or leaves the block. And thread begin, thread end when you start or end the thread.
06:21
I should notice that the set trace func doesn't know about these two new classes. So the other interesting parameter is binding. Basically, that's the same you would get with kernel binding. And it captures the execution context, such as variables, methods, and their values.
06:44
So you can reuse that to perform evaluations later on. So let's add some output to see how our program looks from a debugging point of view. So we see that, first of all, we call method our action on an object.
07:06
Then line event gets generated for that method. Then we have a SQL as we are entering times method. You may notice that that's a SQL because that's a core method and it's implemented in C.
07:22
Then we get line event for times block. Then we get call event for go to RailsConf method, line event for that method, and basically we are returning from all of those. So does anybody have an idea how long would it take?
07:46
Yeah, definitely. It takes about two minutes. Like, two minutes from five seconds. What's going on? Basically, the problem is with the binding.
08:00
Evaluating binding is really, really expensive. So to evaluate the binding, we need to walk the stack to gather all the available variables. We need to store their values, and it takes lots of time. So what should we do? Like, we can just keep calm and wait for the Rails. To be honest, I was able to boot the simple Rails application
08:30
with the setTraceFunc enabled once. Haven't tried that anymore. So what should we do?
08:40
And Ruby 1.8.3 introduces a new method. It's called rbAddEventHook. You may notice that the code below doesn't look like Ruby. Because that's C, yeah. And like, you can specify the function that's basically a callback that would be called.
09:07
And one big difference is that you can also specify events. So for example, if you don't need a line event, you can just specify that you want all other events, and that would make execution faster.
09:21
So let's try executing the same program with the empty callback, but using all events. And it takes ten and a half seconds. Not bad, not bad. So generally, when we have a C API,
09:42
there should be a wrapper gem that you can use. Unfortunately, that's not the case. There is a gem, but it's only compatible with Ruby 1.8. So at some point, I thought, okay, I'll just do some stuff
10:05
and fix the compatibility and get it ready for 1.9 and 2. And well, but actually, I'm quite a lazy person. I really love being lazy.
10:21
And thanks to Koichi, I can't be lazy, because he did all that for Ruby 2.0. And he brought us new APIs that's called TracePoint at Debug Inspector. So what's the TracePoint?
10:42
Basically, that's the wrapper around the event hook with the good object-oriented Ruby API. So what you need to do is just specify events you want to listen for, and you'll get your block called
11:03
every time that event happens. And you can get from the TracePoint object, you can get all the information you can get from this at TraceFunk. Let's try our program with TracePoint. So it takes about 30 seconds,
11:23
which is not as good as event hook, but still pretty good. And that's true, unless we want to access binding, because it makes the program run as slow
11:42
as we have with the setTraceFunk. So the biggest difference between the TracePoint API and setTraceFunk is that the binding is evaluated lazily, so we don't spend lots of time for that. And the second API I was talking about
12:02
is Debug Inspector, and it's also about being lazy. So when you have setTraceFunk or event hook, you'll need to continuously maintain all your frames, list of the frames, and continuously capture the state of the virtual machine
12:22
to understand what's going on, and to present all the frames at the time we hit the breakpoint. With Debug Inspector, it isn't so. When we reach the breakpoint, we can just call the Debug Inspector open, and our callback will receive an object
12:42
with the backtrace and all the M internal information about that. But you may have noticed that it's also a C API. And as we know from the previous slides, if there is C API, then we have probably a wrapper gem for that,
13:02
and it's obsolete. Well, no. There is a pretty handy Debug Inspector gem, and if you want to access all those APIs from Ruby, it's quite easy to do so. So you just call Debug Inspector open,
13:23
and you get your object, and you get your locations, and you can get the bindings from the frames or class and stuff like that. The only limitation of this API is that you cannot use the object that you get from the block.
13:44
You cannot use it outside that block. So if you, for example, need to access the bindings outside of that block, you need to capture and store them elsewhere. So here comes a small performance summary.
14:04
We get five seconds with a simple program run. Set trees func is really, really slow. It's two minutes. And add event hook is super fast.
14:21
And trace point is in between. I should add that it's possible to get almost the same performance with the trace point as we get with the event hook when we are using the C block.
14:41
Because unfortunately, when we run a debugger, we have to watch for all events. And as you have seen, the small program generates nine events per two calls, and that's a lot.
15:01
For else, that's even bigger. So who's using what? So set trace func is actually used by debugger B that comes with your Ruby interpreter. And that's why it's so slow.
15:22
Event hook is used by Ruby debug base and debugger gems. Ruby debug base is basically the debugger front end for Ruby debug and Ruby debug ID. And it's also used for aircraft,
15:41
which was used to capture coverage for 1.8 until we got some sweet coverage API that's used by simple graph. And trace point is, and debug inspector are used by dbase,
16:01
by bug and binding off color gem. So they are like 2.0 only. And dbase gem is front end for Ruby debug ID. But it seems that under the hood,
16:21
everyone is using event hook. Because set trace func generates a new hook which is executed on every event and it generates the binding eagerly.
16:40
And set trace func is doing the same but it generates the object which evaluates binding lazily. So let's continue speaking about being lazy. I love being lazy. But you know what? Your CPU also loves being lazy.
17:03
If you don't push him too hard, he can do things faster. So with the trace point API, we still have to check on every event that we are on a breakpoint line or not.
17:22
So that adds a pretty big performance impact. So I was measuring all the stuff with empty hooks and you can imagine, if you're going to check on every event, it's going to be even longer. So here comes Rubinius.
17:44
Because in Rubinius, you don't need to check explicitly for a line and a file. What you do is basically, when your script is compiled, you get an execution block
18:02
and you get the instruction for that particular line. So here's how it looks, the debase section for Rubinius. This hook is called when the script is compiled and we check in that the script matches our file path
18:23
and if that's so, we locate in the line for locating the execution block for our line and instruction pointer. If we find that, we're just setting our breakpoint
18:42
and that makes Rubinius debugging super, super fast. Here's the comparison between simple run and full debugger enabled because we don't have to check basically anything
19:00
and our breakpoints are called at the moment we are reaching them. So it would be cool if we could have such API in Ruby. So basically, I think that's all I know about the Ruby debugger.
19:21
So you may find me on Twitter on GitHub and you can take a look at the examples and the benchmarks I was using during this talk. And maybe experiment a little bit and see what's going on inside of your Ruby.