Time travel is the biggest thing to happen to debuggers in decades but cannot be used with Python.
Traditionally debuggers have let developers see what a program is doing; the new breed of time travel debuggers let them see what the program has done, winding back to see any line of code that executed and any variable’s value at any point in history says Magne Hov, Software Engineer at Undo.
Users report improving debug capability by 10x or even 100x. It’s not just a question of being more productive; now bugs that previously just would not have been fixed are being fixed says Hov.
It’s also much easier to work on an unfamiliar codebase.
- Tool debugs non-deterministic defects in multi-process
- IDE and debugger ease automotive software development
The standard Python debugger
Python ships with a debugger called pdb that lets developers interact with a program while it is running – i.e. so to see in detail what it’s doing. Using pdb is as simple as calling the built-in breakpoint() function for a debugger prompt, and from there provides access to commands for evaluating expressions, navigating the call stack, stepping through the code and setting up conditional breakpoints.
Debuggers like pdb work by running inside of the same Python interpreter as the program that is being executed. This makes it easy to inspect and manipulate objects, as the debugger can directly evaluate expressions and access Python’s traceback and introspection modules. However, this also rules out certain debugging workflows.
Time travel debuggers like UDB and rr work on the principle of first recording a program while it is running normally, and then replaying the execution while allowing the user to navigate and inspect state at different points in time. They work at the process level — rewinding and replaying the state of the entire Linux (or Windows) process.
While the program is being replayed it must follow the exact same path of execution as it took when it was recorded, and this prevents a debugger like pdb from being able to run at replay time Python functions that weren’t already executed at record time.
Technically, UDB does allow new code paths to be executed at replay-time, but these executions are isolated and all side-effects are discarded, so even if we were able to run code from the pdb module we wouldn’t be able to use features such as setting breakpoints.
Crash dumps like core files are also not compatible with pdb, because there is no live process in which to execute the pdb Python code.
A running Python program is ultimately just a process so when debugging a Python process with GDB (or UDB) directly means effectively debugging the cpython interpreter.
The debugger isn’t aware of any of the Python functions or variables inside a program. Luckily, the cpython project maintains a library of GDB extensions that give GDB the ability to understand Python code. The libpython.py library knows how to inspect the internal structures of cpython in order to present the state of the Python program to the user. The library is executed by a cpython interpreter inside the debugger process, meaning that no Python code needs to be executed inside the context of the program itself.
As the interpreter is implemented in the C programming language, the DWARF debug information has to be available for the Python executable for this to work, i.e the python executable must have been built with the -g compiler flag.
The code for the debugging is at github.com/undoio/python-debugging/tree/blog-post-2024-01-18 in the README.md file. undo.io/udb-free-trial for a free trial version of UDB with a race.py example Python 3.10 program that contains some simple function calls as well as a concurrency issue involving multiple threads.