Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
gh-91048: Refactor and optimize remote debugging module
Completely refactor Modules/_remote_debugging_module.c with improved
code organization, replacing scattered reference counting and error
handling with centralized goto error paths. This cleanup improves
maintainability and reduces code duplication throughout the module while
preserving the same external API.

Implement memory page caching optimization in Python/remote_debug.h to
avoid repeated reads of the same memory regions during debugging
operations. The cache stores previously read memory pages and reuses
them for subsequent reads, significantly reducing system calls and
improving performance.

Add code object caching mechanism with a new code_object_generation
field in the interpreter state that tracks when code object caches need
invalidation. This allows efficient reuse of parsed code object metadata
and eliminates redundant processing of the same code objects across
debugging sessions.

Optimize memory operations by replacing multiple individual structure
copies with single bulk reads for the same data structures. This reduces
the number of memory operations and system calls required to gather
debugging information from the target process.

Update Makefile.pre.in to include Python/remote_debug.h in the headers
list, ensuring that changes to the remote debugging header force proper
recompilation of dependent modules and maintain build consistency across
the codebase.
  • Loading branch information
pablogsal committed May 25, 2025
commit 38f9ad0af76c6837396470e385558e9eec13c370
2 changes: 2 additions & 0 deletions Include/cpython/pystate.h
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ typedef struct _stack_chunk {
PyObject * data[1]; /* Variable sized */
} _PyStackChunk;

/* Minimum size of data stack chunk */
#define _PY_DATA_STACK_CHUNK_SIZE (16*1024)
struct _ts {
/* See Python/ceval.c for comments explaining most fields */

Expand Down
11 changes: 11 additions & 0 deletions Include/internal/pycore_debug_offsets.h
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ typedef struct _Py_DebugOffsets {
uint64_t gil_runtime_state_enabled;
uint64_t gil_runtime_state_locked;
uint64_t gil_runtime_state_holder;
uint64_t code_object_generation;
} interpreter_state;

// Thread state offset;
Expand Down Expand Up @@ -216,6 +217,11 @@ typedef struct _Py_DebugOffsets {
uint64_t gi_frame_state;
} gen_object;

struct _llist_node {
uint64_t next;
uint64_t prev;
} llist_node;

struct _debugger_support {
uint64_t eval_breaker;
uint64_t remote_debugger_support;
Expand Down Expand Up @@ -251,6 +257,7 @@ typedef struct _Py_DebugOffsets {
.gil_runtime_state_enabled = _Py_Debug_gilruntimestate_enabled, \
.gil_runtime_state_locked = offsetof(PyInterpreterState, _gil.locked), \
.gil_runtime_state_holder = offsetof(PyInterpreterState, _gil.last_holder), \
.code_object_generation = offsetof(PyInterpreterState, _code_object_generation), \
}, \
.thread_state = { \
.size = sizeof(PyThreadState), \
Expand Down Expand Up @@ -347,6 +354,10 @@ typedef struct _Py_DebugOffsets {
.gi_iframe = offsetof(PyGenObject, gi_iframe), \
.gi_frame_state = offsetof(PyGenObject, gi_frame_state), \
}, \
.llist_node = { \
.next = offsetof(struct llist_node, next), \
.prev = offsetof(struct llist_node, prev), \
}, \
.debugger_support = { \
.eval_breaker = offsetof(PyThreadState, eval_breaker), \
.remote_debugger_support = offsetof(PyThreadState, remote_debugger_support), \
Expand Down
2 changes: 2 additions & 0 deletions Include/internal/pycore_interp_structs.h
Original file line number Diff line number Diff line change
Expand Up @@ -843,6 +843,8 @@ struct _is {
/* The per-interpreter GIL, which might not be used. */
struct _gil_runtime_state _gil;

uint64_t _code_object_generation;

/* ---------- IMPORTANT ---------------------------
The fields above this line are declared as early as
possible to facilitate out-of-process observability
Expand Down
5 changes: 4 additions & 1 deletion Lib/asyncio/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@
from itertools import count
from enum import Enum
import sys
from _remote_debugging import get_all_awaited_by
from _remote_debugging import RemoteUnwinder

def get_all_awaited_by(pid):
Comment thread
pablogsal marked this conversation as resolved.
Outdated
unwinder = RemoteUnwinder(pid)
return unwinder.get_all_awaited_by()

class NodeType(Enum):
COROUTINE = 1
Expand Down
51 changes: 38 additions & 13 deletions Lib/test/test_external_inspection.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import importlib
import sys
import socket
import threading
from asyncio import staggered, taskgroups
from unittest.mock import ANY
from test.support import os_helper, SHORT_TIMEOUT, busy_retry
Expand All @@ -16,9 +17,7 @@

try:
from _remote_debugging import PROCESS_VM_READV_SUPPORTED
from _remote_debugging import get_stack_trace
from _remote_debugging import get_async_stack_trace
from _remote_debugging import get_all_awaited_by
from _remote_debugging import RemoteUnwinder
except ImportError:
raise unittest.SkipTest("Test only runs when _remote_debugging is available")

Expand All @@ -33,6 +32,17 @@ def _make_test_script(script_dir, script_basename, source):
"Test only runs on Linux, Windows and MacOS",
)

def get_stack_trace(pid):
unwinder = RemoteUnwinder(pid, all_threads=True)
return unwinder.get_stack_trace()

def get_async_stack_trace(pid):
unwinder = RemoteUnwinder(pid)
return unwinder.get_async_stack_trace()

def get_all_awaited_by(pid):
unwinder = RemoteUnwinder(pid)
return unwinder.get_all_awaited_by()

class TestGetStackTrace(unittest.TestCase):

Expand All @@ -46,7 +56,7 @@ def test_remote_stack_trace(self):
port = find_unused_port()
script = textwrap.dedent(
f"""\
import time, sys, socket
import time, sys, socket, threading
# Connect to the test process
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(('localhost', {port}))
Expand All @@ -61,7 +71,7 @@ def baz():
def foo():
sock.sendall(b"ready"); time.sleep(10_000) # same line number

bar()
t = threading.Thread(target=bar); t.start(); t.join()
Comment thread
pablogsal marked this conversation as resolved.
Outdated
"""
)
stack_trace = None
Expand Down Expand Up @@ -94,13 +104,20 @@ def foo():
p.terminate()
p.wait(timeout=SHORT_TIMEOUT)

expected_stack_trace = [
thread_expected_stack_trace = [
("foo", script_name, 14),
("baz", script_name, 11),
("bar", script_name, 9),
('Thread.run', threading.__file__, ANY)
]
main_thread_stack_trace = [
(ANY, threading.__file__, ANY),
("<module>", script_name, 16),
]
self.assertEqual(stack_trace, expected_stack_trace)
self.assertEqual(stack_trace, [
(ANY, thread_expected_stack_trace),
(ANY, main_thread_stack_trace),
])

@skip_if_not_supported
@unittest.skipIf(
Expand Down Expand Up @@ -700,13 +717,21 @@ async def main():
)
def test_self_trace(self):
stack_trace = get_stack_trace(os.getpid())
self.assertEqual(stack_trace[0][0], threading.get_native_id())
self.assertEqual(
stack_trace[0],
(
"TestGetStackTrace.test_self_trace",
__file__,
self.test_self_trace.__code__.co_firstlineno + 6,
),
stack_trace[0][1][:2],
[
(
"get_stack_trace",
__file__,
get_stack_trace.__code__.co_firstlineno + 2,
),
(
"TestGetStackTrace.test_self_trace",
__file__,
self.test_self_trace.__code__.co_firstlineno + 6,
),
]
)


Expand Down
1 change: 1 addition & 0 deletions Makefile.pre.in
Original file line number Diff line number Diff line change
Expand Up @@ -1206,6 +1206,7 @@ PYTHON_HEADERS= \
$(srcdir)/Include/unicodeobject.h \
$(srcdir)/Include/warnings.h \
$(srcdir)/Include/weakrefobject.h \
$(srcdir)/Python/remote_debug.h \
\
pyconfig.h \
$(PARSER_HEADERS) \
Expand Down
Loading