Make pdb faster with PEP 669

PEP 669 brought a new set of APIs to do low cost monitoring. pdb can take advantage of it, especially for breakpoints. Currently, once any breakpoint is set, pdb has to do a callback on every single line event which introduces a significant overhead (>100x in extreme case, easily 20x). With local events of PEP 669, we can get rid of it and achieve free breakpoint.

Multiple ideas were brought up during a couple of discussions, but the current target is to keep pdb as much as possible, including the full user interface, and replace bdb (its underlying mechanism) with a new class that uses PEP 669. The original class bdb will be kept to provide full backward compatibility, and the new alternate would be available to debugger developers that want to utilize PEP 669.

Please feel free to share thoughts about this :slight_smile:

Tian

4 Likes

Exciting!

Just curious, how hard would it be to reimplement the existing bdb interface using PEP 669? Difficult, inefficient, impossible…?

Actually, I was thinking about refactoring the whole thing to support new features and provide more flexibility so I just assumed bdb needs a change. I might be able to keep bdb interface (at least most of it). The one thing I know that’s gonna change is that pdb/bdb is going to be singletons due to the restrictions of monitoring system.

One thing about bdb is it provides a way to overwrite the dispatching mechanism - the class inherits it can do whatever they wanted for the “line” event from sys.settrace - that will not be compatible because we don’t want to generate those events with PEP 669. We can keep plenty of the interface like all the functions dealing with breakpoints, but we would have to get rid of some stuff that’s related to the old settrace framework.

Is that part of the Bdb spec, or just a consequence of inheritance?

How about only calling the overridden method, if it is actually overridden?
Something like:

def line_handler(self, code, line):
    if type(self).dispatch_line != Bdb.dispatch_line:
        self.dispatch_line(sys._getframe())
    elif self.break_here(code, line):
        self.user_line(sys._getframe())
    else:
        return DISABLE

This assumes we use global line event, and only take advantage of DISABLE to make it faster, which is not the ambition. In order to do real (nearly)zero-cost breakpoint, we should fire as less as possible. Same thing with next - the function being stepped over should not fire any event at all, not firing one and disabled. I’m using local events for most of the functions because even with DISABLE, the cost of instrumenting a code block, firing and disabling is still significant (also I believe even after DISABLE, there’s still a cost to execute the bytecode).

With my ideal underlying mechanism, there will be no line events for lines that are not “interesting”, there will be no break_here() because that is not needed anymore, there will only be line events that actually matter - which breaks the whole dispatch system because it’s not there anymore.

We can do the safest way - basically reimplementing sys.settrace with PEP 669 in bdb and do the exact approach as before, with some improvements from PEP 669 (it’ll still be significant when executing in a loop with some breakpoints set), but I kind of want to do this right.

All this makes me think that maybe we should not strive to improve the bdb/pdb combo that’s in the stdlib – instead, you would be much more productive if you created a new debugger (possibly derived from the pdb/bdb code) as a 3rd party project, released via PyPI. That way you can have users test the new interface much sooner (no need to wait for 3.13 to be released), you can update your design, interface and implementation much quicker (no need to be strictly backwards compatible, no need to wait for bugfix releases or even feature releases), and you’ll be altogether more productive and happier.

Yes, the pdb in the stdlib will not be as fast or cool, but it won’t break for anyone who is using the current API either. There could be an evolutionary path where we provide a new command line experience in the stdlib quite similar to current pdb (possibly in the same module) but implemented on top of PEP 669, without adhering to strict backwards compatibility for the bdb/pdb interface, and deprecate bdb and the classes defined in pdb; but that will take time – in the meantime work on a new debugger can proceed much quicker without being tied to the stdlib.

5 Likes

In the current effort, the interface of pdb will be kept and I plan to make all the existing pdb test pass. As far as I know, IDLE is the only one using bdb. (There are not a lot of debuggers around). We can either migrate IDLE, or just kept the original bdb to make IDLE work.
How great it is if pdb can be as fast and still not break anyone’s code?

Update:

I’m able to make a prototype that can pass ALL existing pdb tests except for the recursive debug one. The idea of recursive debugger is a bit off to me, we may be able to make it work but currently it relies on sys.call_tracing - there’s no equivalent thing in PEP 669 so we probably won’t able to do it in a decent way if we want to utilize PEP 669 at all.

bdb will lose some of its APIs, like all the dispatch_* functions, because they are not used anymore. The core user_* functions and set_* functions are kept and work as before. All the breakpoints work as before (a few changes were made but user should not notice).

Some other APIs are not used anymore like bdb.effective().

The code still needs polish but I’m fairly confident that we can keep at least pdb almost the same with PEP 669 (with a few exceptions). We can either do a new bdb for the new pdb, or replace the old one while keep most of the important APIs.

With the example given on pdb/bdb changes for PEP 669 · Issue #103103 · python/cpython · GitHub, the new version is about 100x faster than the old one.

2 Likes

As i mentioned in this comment, I hope that IDLE should be able to use the new bdb. It overrides user_line and _exception; uses set_break, _continue, _next, _quit, _return, and _step; and also uses get_stack, run, clear_break, and clear_all_file_breaks. If or when these are all implemented, I would be happy to test bdbx as a drop-in replacement. If only some break functions are missing, this could be tested now.

At some point, I might ask whether any of the features in pdb and not in i(dle)db are useful enough for GUI users that they should be added for IDLE.

These are all currently implemented, but I’m not 100% sure they work with IDLE (there could even be unknown issues with pdb, but they at least work for the current test suite).

The code is super raw now but you can probably try it out - GitHub - gaogaotiantian/cpython at pep669-pdb . The only thing you need to do is instead of import bdb, do import bdbx as bdb.

Again, I have not tested with IDLE so I don’t know if it just works, or there are some essential problems. But some tests on IDLE would be appreciated! Let me know if you need any help with it.