Why I use a debugger
blog.pnkfx.org
blog.pnkfx.org
This ignores the typical scenario where you are working in a business application that you've never seen most of the code, only the relevant parts of whatever tasks you have done in that program, and all of the sudden you are asked to solve a bug or make a change in some place that you didn't know even existed, the original developer is long gone, is not documented, and chances are that there are many great coding horrors. A debugger can be really helpful to uncover how that code works.
Also it assumes that the only possible use of a debugger is set a breakpoint and then follow every next step until the end, but this is actually not the case. A debugger allows you set conditional breakpoints, skip whole sections of code, evaluate code using the actual context that the application had when it was stopped, make changes to variables while the program is being executed, it is a great tool to explore code and behavior.
I respect that some people may not like them and don't want to use them, but I find pretty dumb the idea that not using them is superior and you are a worse programmer if you do.
1.) Older hacker types for whom it was previously unavailable or difficult to set up, so they learned to work (well) without it.
2.) Juniors/fresh grads, who often seem intimidated by it, likely because teachers and online resources didn’t emphasize it sufficiently.
I think the first group would benefit from introducing a debugger into their work process, but for the second group it should be essential.
3.) Folks that are, for whatever reason, satisfied with log statements and can't be bothered to learn something new
4.) TDD die-hards where a debugger is evidence of a failure to follow the TDD manifesto
I'm a huge proponent of debuggers but I've encountered Senior SWEs that are not old-hackers or just-out-of-college.
Cut the interview short. Thank them for their time then escort to the exit.
Anyone can get bouts of error-free code. It happens on a regular basis that I write dozens of lines of code and everything just works, without any debugging.
However, it's not just the code you write, there are also bugs in code that someone wrote 5 years ago. Or a bug in a library that you have no idea about. A lot of times, these can be solved with a debugger and a few WTFs or with a week of trying to understand every line of code, it's states and the transition between those states.
Let's be realistic here.
You can solve bugs by thinking about / reading code. I'd say that's the way I solve the vast majority of my bugs.
Also "using a debugger" and "debugging" are not synonymous.
Or, in other words, you just said a bunch of seemingly-relevant keywords, but without actually making any sense. Your comment looks a lot like what GPT-3 might have to say about this discussion.
Still, there are cases where you will build a mental model that is wrong either due to minor bugs or larger design problems.
As a former physicist, I think of this like theoretical vs experimental physics. First you build the theory (by reviewing the code), then you run experiments (by first running the code). If you encounter surprises, you run more detailed experiements to pinpoint where your theory is wrong, and for this you can use print/log statements and/or a debugger.
In my experience, print/log statements are ok for relatively linear logic (immutable or functional patterns, for instance in a data pipeline), while the debugger may be more helpful for highly non-sequential patterns, where it is difficult to grasp what states the program may end up in (complex state machines driven by random/user input, for instance).
Then there are cases that are complicated by asynchronous, often high frequency input or that involves third party services that you cannot control (ie trading systems, logistics systems and similar systems that use a lot of concurrent transaction management, and are often connected to external systems, or even generic software like OS's, database engines, etc). Those may be impossible to reproduce properly in a debugger, and may instead need some kind of statistical approach, by bombarding the software with either live or synthetic input, and use collected metrics to build a theory that can explain the problem. (Which will trigger further experiments to validate.)
Depending on what software you are writing, using a debugger can be anything from a superpower to nearly useless.
The ability to reason about code, on the other hand, is universally useful. Arguably even for commercial software where you do not have access to the code. (As a physicist, I could not "see" quarks directly, but I could still detect them statistically by properly designed experiments.)
In my experience the vast majority of those cases can be solved with quick search of the Github issue tracker to find someone else who has already debugged the issue, followed by an upgrade of the library to a newer version that's already been released to fix the issue, or if you're unlucky manually applying a patch.
Of course, that person's probably used a debugger to solve the issue. And sometimes you need to be that person. But IMO if you're using libraries that regular require you to pull out a debugger, then maybe you ought to be using better libraries.
I want a tool which takes a rr-like trace, then generates a trace of which functions call which other functions, and how work is divided between callers and callees, which loop iterations and branches are taken (which may be hundreds or thousands of pages long), then lets me subset the function calls or control flow I care about, as a starting point for me to take notes about a particular cross-cutting aspect (in the aspect-oriented programming sense, like "all calls to alsa-lib and all their call stacks" or "cross-thread shared memory accesses") or workflow (eg. startup and shutdown processes) of code execution.
I want a hybrid of architectural documentation (for implementers rather than users), personal notes, and a "debugger" for introspection/observability on code execution. I feel Pernosco aims to be the latter, and in my usage so far it's a poor choice for automating the tedium of building/codifying bird's-eye architectural understanding and global reasoning, but it might grow on me over time as a debugger for tracing data. (Functional programming promises to avoid the need for global reasoning, but I haven't looked into it.)
- Me: Just use the debugger...
- Him: But it's hard and annoying to use the debugger
- Me: It's hard and annoying not having the skills or reflex to use the debugger by default
- Him: ... ok I agree ... continues rolling face on the keyboard and add print statements everywhere
I think much of the sentiment in these comments is sounding like "that's not how I work so I will defend myself". Just learn how to use your debugger and integrate it into your work flow. You don't need a special IDE to use a debugger in most languages if that's the perceived problem.The solution is probably employer-funded sabbaticals but since those are rare people just settle for quitting and poking around at their own projects for a while.
Now that I’m more of what you’d call a “senior” developer I realize the absolute importance of taking time to set up your development environment so compiling, debugging, unit tests, prototyping etc. are as seamless as possible - the benefits to productivity and general happiness are manifold! But having the clout to invest more “non-productive” time upfront so your overall productivity is much higher is not usually afforded to devs fresh out of school who tend to just grind it out.
For me, the debugger only comes out when those tools don't work quickly, I'm pretty lost or I'm inspecting the code flow of third party code.
Likewise, I don't feel debuggers are a superpower. It's rare for them to "save the day". It's just a tool that I sometimes go for. The last time I remember using a debugger was a few months ago to figure out a memory leak. hexdump would've worked just as well.
I've seen quite a few threads on r/php where individuals introduce a newer, supposedly better, library that can be used instead of `var_dump`, and there's always the inevitable "why not use the debugger?" to which many reply "because it's hard to set up" or "var_dump is better" or something silly to that effect.
You also wouldn't generally be debugging a request from random clients. You'd generally be debugging in your local dev environment.
Those who can't code, debug. :-)
Joke apart it's a style thing, tracing and thinking gets you a long way, though virtuoso debugger users can be productive too no doubt. It's a bit like GUI vs CLI.
In practice, most software is a stack of abstractions and bugs can be caused by anything at any level of that pile, including libraries. The ability to quickly inspect the state at a given execution step is not a tool that can realistically be dispensed by just thinking hard about the code.
There's another great benefit to a debugger, and that is troubleshooting long-running code or hard to prime iterations. Like for example, debugging a checkout flow can be very cumbersome as you have to prime the state for every test. There are plenty of workarounds if you have to log your way through it, but wouldn't it just be easier to use a tool designed for the job?
If anything, I think debuggers are nice for simple, borderline trivial programs where everything fits on a screen and there's a handful of variables to keep track of. (These tend to be the programs I rarely need a debugger for though, and the debugger isn't necessarily any faster than a bunch of print statements). They're nice for getting stack traces or figuring out the contents of RAM and sometimes that's just what you need, but often that's just working your way backwards towards the real issue, without having a way to rewind time.
rr might change a lot of that, if it's available for your platform..
Put a breakpoint early on startup, once hit put a data breakpoint on the part of the state which messed up, reproduce the bug, and there's very high chance debugger will take you to the code which broke the state.
This will even happen if the code which breaks the state is a memory corruption bug in different thread, in the code written in another language, from a third-party DLL.
> debuggers are nice for simple, borderline trivial programs where everything fits on a screen and there's a handful of variables to keep track of
It's funny I think it's the opposite. When there's only a few variables, one can print/log the complete state pretty often. When the state takes a gigabyte of memory and changes often, similar amount of logging going to produce too many terabytes of logs to be useful. Debugging is interactive, you can inspect the complete state and find the most relevant pieces to watch.
Except when you find the data you're after doesn't exist early at startup, it's allocated on the fly as connections come and go, and you need thousands of connections to come and go before the bug manifests, and the data that eventually gets messed up may first be used thousands of times before it's wrong all of a sudden. IME it's precisely these multi-threaded memory corruption bugs that really resist debuggers. And as is often the case, debugger changes timing so much that the bug doesn't even reproduce.
Even if you find the code that corrupts the memory, it might be totally okay, it just somehow got passed the wrong memory long ago through no fault of its own (or you're looking at a use-after-free).
But when the one in my comment does work, the saved debugging time can be measured in days.
Speaking about debugging tactics, there’re way more than just two. There’re OS-level tools like process monitor on Windows / strace on Linux. There’re specialized tools like RenderDoc or Wireshark. For different types of bugs, these tools can sometimes also save days of work compared to other methods.
That's why I don't really like an absolutist stance (if you rarely use debugger you're dumb / if you always poke around with a debugger you're dumb). I just happen to fall in the "rarely uses debugger" camp myself.
> I’ve done hundreds of interviews like this, and it’s fascinating watching what people do. Do they read the code first? Fire up a debugger? Add print statements and binary search by hand? [..] After hundreds of interviews I still couldn’t tell you which approach is best.
Knowing which part of the state messed up is like 90% of the debugging. Most of the time the part of the state you see messed up is like that because another part of the state is also messed up. Given that neither debuggers nor logging can go back in time to backtrack the state (well, time travel debuggers exist but aren't common or cover all cases), it's usually faster to inspect the code manually to try to find the original bug instead of re-running continuously to backtrack the state.
I’m not sure how usual that is. Sometimes “the code” is too many thousands of lines to inspect. Other times, “the code” is only the machine code but not the source code, inspecting megabytes of disassembly is not fun.
> inspecting megabytes of disassembly is not fun.
Well, of course there you need the debugger and memory watchers. But I think that fits into the "unusual" slot, most people aren't doing that.
This is not a judgement, just an observation. I always assumed debuggers were just part of a tool set but it appears (in my experience) many individuals were never taught that it exists.
I also do not claim its a magical tool that can solve all problems. There are plenty of times i do a "print 'i'm here' + var".
I also do a lot of low level code (kernel/bootloader) where debuggers can be available but are often a lot harder to setup. Keep in mind that I also don't like IDEs and my coding setup is mostly vim + ctags + terminal.
I really agree with the quote in TFA, when I write rust code a few judiciously placed dbg!() calls are generally all I need to identify the issue.
I often can figure it out by reading the code (I'm the quickest jump-to-source in the wild west!) quicker than I can from inserting print stmts or hooking up a debugger.
I've also decided that I dislike having my IDE be my debugger, I prefer having an entirely separate UI for that. Maybe this is because I use Emacs and DAP mode and gdb is quite poor there, I'd rather use something like gdbgui.
Blind probing with a debugger is not likely to be productive. It is more helpful to use the debugger to discover the state of the program when it fails, then think about how the failure could have happened. Debuggers can be arcane and difficult programs, and especially for beginners may provide more confusion than help. If you ask the wrong question, they will probably give you an answer, but you may not know it's misleading.
In the ~15 years since I read this I've seen dozens of colleagues try to use a debugger in place of thinking harder, and it never goes well. Sometimes, but much more rarely, I see someone use a debugger after thinking hard for a while. That usually goes excellently, and probably creates some unrealistic view of the tool's effectiveness because other programmers see the debugger and not the thinking. 99% of problems are solved during the "think harder" stage, and if I can't think my way out of some local problem I probably wrote too-complicated code anyway. (And if it's a non-local problem, the debugger may not help too much.)
The goal of most IDEs seems to be first help with a debugger / executing code/tests, second help with writing code, and only thirdly help with reading code. The older I get, the more those priorities seem inverted to me.
I'd be interested in developing more tools based on abstract interpretation and symbolic execution of the programs themselves and generally be able to query your tools more.
For example, let's say that you find a program state with the debugger which is invalid, the next question is then "How can this function reach this state?". This can be answered by the symbolic execution engine, giving you a function-local path. Then you can look at the callers of this function and ask the engine to do the same resolution on that level.
Being able to formulate questions and have them answered would lead to a question/reply-driven method of working, which I think would be very helpful.
We must not be using the same IDEs. To me “IDE functionality” is almost synonymous with “code navigation”: jump to definition, find references, etc., which are purely “code reading” features.
I tend to use a debugger when I start to log too many things. It's a sign I should use a debugger to run the code, stop the execution where I'd put the logs and then explore the context and stack trace.
I also use Emacs (I'm mainly a Scala dev) and still fire up Intellij for debugging most of the time.
Also very good for crashes or asserts or whatever. The offending line, and the stack trace, are shown immediately.
> You don't click it, even out of desperation at any point in your career?
I think I've never used an IDE since the (good) times of borland C++.
> I get really tired and demotivated working with people who are like that
You would probably hate working with me then... sorry about that.
If you already knew which if statement to look at, it's irrelevant whether you use a debugger or printf().
Actually, it becomes even less relevant when you have thousands of dynamically allocated objects running the same statement but only one of them goes wonky and you only know which one it is later at runtime. In the end you end up doing the same logging and tracing with the debugger that you can do with a printf().
That's exactly when it matters, when you don't know where the problem is. You have a big ball of spaghetti business logic and a customer object that is passing through it. At some point the customers total balance is screwed up. You can:
- Read a lot of code and guess where that happens and set a printf or two, if you find it great, if not read more and set more printf's
- Set a watcher on the customers total, and review the logic each place it is modified and ensure it is correct before moving on
The first way you have to build the project over and over again, and you might miss something. The second way guarantees you find the problem eventually, and you only end up building a few times. If we have a project with a 5 minute build time, and a really thorny problem this can be a massive difference.
That isn't to mention the value of doing this in concert with the creation of a unit test that recreates the bug to further reduce the bugfix feedback loop.
Now the question is, how do you tell from the trace that the total is wrong? If you can write a function to check it in the debugger, you probably can -- with roughly the same effort -- write an assert() or (asserts) in the program where that variable is modified, and that'll ensure you'll be immediately notified the next time it goes wrong even before anyone notices let alone has time to dig out a debugger. I'd say the value of creating asserts is up there with unit tests.
You can object that actually the problem is in the complicated business logic and is already gone by when the assert fires and the wrong value is finally assigned/updated to the variable, but that same problem would come up in a debugger too. At one point your debugger notices that whoops it's gone wrong, but you don't know how exactly how you got there? Like I said in another thread here, things like rr can really change debugging (allowing you to rewind time, sort of) when they become available to your platform & language, but until then, I just don't find very compelling arguments why inspecting bad state in business logic would be so much faster in a debugger than with printfs. I find debuggers useful mainly for some niche things (ok, which NULL or freed pointer did I dereference and segfault on?).
Now if you have to make an argument about performance, go ahead, but that's kind of orthogonal and it goes both ways. Some programs run too damn slow under a debugger, sometimes you have to make a separate build with debugging info and different build options (again possibly with a massive performance impact) and figure out how to use all that in conjunction with the target system that only has a few dozen megabytes of spare RAM.. if running a debugger requires none of that but builds are slow for you, go for it.
Adding traces (probably over several iterations to narrow down things), rebuilding, rerunning and inspecting those traces would've probably taken multiples of the time to build and run everything up to the point of failure.
Signed,
Someone who actually likes debuggers but doesn't see you making a solid case for them at all.
Agree that’s not the first use case I’d think of, but I do think debuggers can quickly pinpoint that kind of thing as you step through code… if you know roughly where the problem is.
The truth is a bit less dramatic than you made it. Wasnt the best scallops chef in the world, at most the best of 5 trainees at a certain restaurant.
Makes me optimistic. Maybe there still is a world class programmer who doesnt use a debugger!
What makes me pessimistic is the lack of any usable rr equivalent for python. Think rpython had it or something but not the latest version.
It's a curious observation. If it's true your work environment must be very different to mine.
You just need a:
- ability to describe different points in the evaluation graph as one investigation set
- ability to bookmark restore these investigation set
- ability to visualize them in batch
bonus point:
- extract a patch from debugger interaction so you don't type again what you 'xplained to the debugger
But not all cooking pans are the same. The non-stick pan might not stick, but does it heat as evenly? Does it control the heat with the same stability? i.e. Gordon might have been being a dick.
I especially do this when the code has destructive side-effects (a file gets moved/deleted, a database get updated, whatever), so I can skip over any external actions I don't actually want to occur until I'm ready for a full test run.
Everybody does this, right? Right???
I always wrap these statements in a function/if-statement/similar thing that allows me to run the tool in "dry mode" so changes are not done but just logged. It's pretty useful because I can reuse that mode whenever I want without reattaching the debugger and stepping again through everything.
My laziness often precludes me from taking that approach until it's clear that it's warranted (but hopefully before I nuke anything critical).