Considering that it is so useful we want the debugger to have lots and lots of features:
- manage breakpoints (conditional/data breakpoints)
- control execution (step through code, step over/into functions, etc.)
- field watches
- take snapshots
- inspect variables
- modify variables
- monitor the call-stack
- code edit/swap at runtime
- intuitive GUIs
- remote debugging
- support for working with threads
- reversible/back-in-time debugging
By all means this is not an exhaustive list. What it shows is that we expect a lot from a good debugger. The best part is today’s debuggers do provide most of these features.
But what happens when we want something that’s not there, which is bound to happen sooner, rather than later? First, we might go in denial: ‘This features has to be somewhere. I just have to find it. I can’t be the first one to have this problem.’ So we start looking. We search online, ask colleagues, read documentation, etc. In the end, given the huge amount of information that’s out there, we find something that addresses our problem. It can be a workaround, a hack, a pice of code that we do not understand but works, some cleaver way of combining several tools, etc. It can be a strange or ugly solution, but hey, if it solves the problem it is perfect.
I follow this reasoning a lot of times. I’m sure a lot of you do too. However, there is one small thing that few of us take into account: if there is a feature missing in the debugger why don’t I just implement it? I believe most answers boil down to ‘Because it’s hard’.
The debugger is just a software application. To extend it we have to understand it. And as it turns out this can be rather difficult. To see this let’s look at how the debugger is implemented in Pharo 2.0. As Pharo is a *Smalltalk inspired* environment, and Smalltalk prides itself on its live environment, one might think that extending the debugger is straightforward.
As it turns out the entire debugger is implemented in a single class, named Debugger. This contains both the low level logic of working with a process and the logic for creating and updating the user interface. Everything is nicely tight together and heavily optimised. The class has 1453 lines of code grouped in 158 methods that access 23 attributes (including inherited ones); it has 120 lines of documentation. To give you a feel of how the class is organised here is a Class Blueprint and a System Attraction view showing its internal structure:
We can see that methods are heavily interconnected. There are large methods both in the public and private interface. Attributes are accessed both directly and using accessor methods.
Does this mean this implementation of the debugger is wrong? After all it provides most of the features mentioned above, the code is in one place and it has been like that for a while. Like many other things in software I believe the answer is change: as long as we don’t need to modify it or extended it, the implementation is just fine. However, as soon as we need to add new features, the current implementation only gets in the way.
Now the question becomes: ‘Why would one want to modify the debugger?’. Why not continue using the available one and when new problems appears hope to find somewhere something someone thought can be of help to you?
To answer this, consider for a moment the explosion of new frameworks and libraries that happened in the last years. With the right library or framework all our problems can be solved in one or two lines. We add layers over layers of abstraction to reduce the number of lines of code we need to write. Just then what happens when things do not work? Most of the times we are lost in a world of abstractions. The debugger does not serves its purpose anymore. The fixed semantics of what it does and show are so far away from the reality of our systems that it becomes less and less effective. And, given the highly particular situations we encounter, searching for solutions revels less and less useful insight.
“We become what we behold. We shape our tools and thereafter our tools shape us”.
Said almost fifty hears ago in the context of media it describes well how we currently work with debuggers. We create large monolithic debuggers with lots of features and little support for extension. Then, when we encounter new situations we haven’t considered before, we try to solve them using the available ones even if they are ill suited for that. We don’t attempt to adapt the debugger, as we created a debugger that does not allow us to do that.
What would happen if instead of creating large debuggers with lots of functionalities we would instead focus on moldable debuggers that have extensibility as their main feature?
Would we then, when building a large and complex framework, invest perhaps a fraction of our energy in creating a dedicated debugger for that framework? Now only could this help us to better understand the framework, but would surely make the life of our users much easier. And wouldn’t this then increase the value of our framework? Let’s say you have to choose between to parsers. They have more or less the same functionalities, but one also ships with a dedicated infrastructure for debugging; the other has none. Which one would you choose?
To make this vision possible we need to build moldable debuggers from the start. We saw in this post that the current implementation of the Pharo debugger is a rigid one. In the next post we will look at a new design of the debugger that encourages extensibility.