Skip to content

Commit 5c41be6

Browse files
committed
debugpy: Performance improvements.
Signed-off-by: Jos Verlinde <jos_verlinde@hotmail.com>
1 parent 3435c93 commit 5c41be6

4 files changed

Lines changed: 114 additions & 89 deletions

File tree

Lines changed: 48 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,67 @@
11
"""Constants used throughout debugpy."""
2+
from micropython import const
23

34
# Default networking settings
45
DEFAULT_HOST = "127.0.0.1"
56
DEFAULT_PORT = 5678
67

78
# DAP message types
8-
MSG_TYPE_REQUEST = "request"
9-
MSG_TYPE_RESPONSE = "response"
10-
MSG_TYPE_EVENT = "event"
9+
MSG_TYPE_REQUEST = const("request")
10+
MSG_TYPE_RESPONSE = const("response")
11+
MSG_TYPE_EVENT = const("event")
1112

1213
# DAP events
13-
EVENT_INITIALIZED = "initialized"
14-
EVENT_STOPPED = "stopped"
15-
EVENT_CONTINUED = "continued"
16-
EVENT_THREAD = "thread"
17-
EVENT_BREAKPOINT = "breakpoint"
18-
EVENT_OUTPUT = "output"
19-
EVENT_TERMINATED = "terminated"
20-
EVENT_EXITED = "exited"
14+
EVENT_INITIALIZED = const("initialized")
15+
EVENT_STOPPED = const("stopped")
16+
EVENT_CONTINUED = const("continued")
17+
EVENT_THREAD = const("thread")
18+
EVENT_BREAKPOINT = const("breakpoint")
19+
EVENT_OUTPUT = const("output")
20+
EVENT_TERMINATED = const("terminated")
21+
EVENT_EXITED = const("exited")
2122

2223
# DAP commands
23-
CMD_INITIALIZE = "initialize"
24-
CMD_LAUNCH = "launch"
25-
CMD_ATTACH = "attach"
26-
CMD_SET_BREAKPOINTS = "setBreakpoints"
27-
CMD_CONTINUE = "continue"
28-
CMD_NEXT = "next"
29-
CMD_STEP_IN = "stepIn"
30-
CMD_STEP_OUT = "stepOut"
31-
CMD_PAUSE = "pause"
32-
CMD_STACK_TRACE = "stackTrace"
33-
CMD_SCOPES = "scopes"
34-
CMD_VARIABLES = "variables"
35-
CMD_EVALUATE = "evaluate"
36-
CMD_DISCONNECT = "disconnect"
37-
CMD_CONFIGURATION_DONE = "configurationDone"
38-
CMD_THREADS = "threads"
39-
CMD_SOURCE = "source"
24+
CMD_INITIALIZE = const("initialize")
25+
CMD_LAUNCH = const("launch")
26+
CMD_ATTACH = const("attach")
27+
CMD_SET_BREAKPOINTS = const("setBreakpoints")
28+
CMD_CONTINUE = const("continue")
29+
CMD_NEXT = const("next")
30+
CMD_STEP_IN = const("stepIn")
31+
CMD_STEP_OUT = const("stepOut")
32+
CMD_PAUSE = const("pause")
33+
CMD_STACK_TRACE = const("stackTrace")
34+
CMD_SCOPES = const("scopes")
35+
CMD_VARIABLES = const("variables")
36+
CMD_EVALUATE = const("evaluate")
37+
CMD_DISCONNECT = const("disconnect")
38+
CMD_CONFIGURATION_DONE = const("configurationDone")
39+
CMD_THREADS = const("threads")
40+
CMD_SOURCE = const("source")
4041

4142
# Stop reasons
42-
STOP_REASON_STEP = "step"
43-
STOP_REASON_BREAKPOINT = "breakpoint"
44-
STOP_REASON_EXCEPTION = "exception"
45-
STOP_REASON_PAUSE = "pause"
46-
STOP_REASON_ENTRY = "entry"
43+
STOP_REASON_STEP = const("step")
44+
STOP_REASON_BREAKPOINT = const("breakpoint")
45+
STOP_REASON_EXCEPTION = const("exception")
46+
STOP_REASON_PAUSE = const("pause")
47+
STOP_REASON_ENTRY = const("entry")
4748

4849
# Thread reasons
49-
THREAD_REASON_STARTED = "started"
50-
THREAD_REASON_EXITED = "exited"
50+
THREAD_REASON_STARTED = const("started")
51+
THREAD_REASON_EXITED = const("exited")
5152

5253
# Trace events
53-
TRACE_CALL = "call"
54-
TRACE_LINE = "line"
55-
TRACE_RETURN = "return"
56-
TRACE_EXCEPTION = "exception"
54+
TRACE_CALL = const("call")
55+
TRACE_LINE = const("line")
56+
TRACE_RETURN = const("return")
57+
TRACE_EXCEPTION = const("exception")
58+
59+
# Step modes
60+
STEP_INTO = const("into")
61+
STEP_OVER = const("over")
62+
STEP_OUT = const("out")
63+
5764

5865
# Scope types
59-
SCOPE_LOCALS = "locals"
60-
SCOPE_GLOBALS = "globals"
66+
SCOPE_LOCALS = const("locals")
67+
SCOPE_GLOBALS = const("globals")

python-ecosys/debugpy/debugpy/common/messaging.py

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -86,25 +86,46 @@ def recv_message(self):
8686
if self.closed:
8787
return None
8888

89+
# Quick bail-out: if buffer is empty, do a non-blocking peek to see if data is available
90+
if not self._recv_buffer:
91+
try:
92+
# Try to read a small amount non-blocking to see if anything is available
93+
peek_data = self.sock.recv(1)
94+
if not peek_data:
95+
return None # No data available
96+
# Put the peeked data back into buffer
97+
self._recv_buffer = peek_data
98+
except OSError as e:
99+
# Handle non-blocking socket errors (no data available)
100+
if hasattr(e, "errno") and e.errno in (11, 35): # EAGAIN, EWOULDBLOCK
101+
return None # No data available, quick exit
102+
# Other errors
103+
self.closed = True
104+
return None
105+
106+
# Cache frequently accessed attributes
107+
recv_buffer = self._recv_buffer
108+
sock_recv = self.sock.recv
109+
89110
try:
90111
# Read headers
91-
while b"\r\n\r\n" not in self._recv_buffer:
112+
while b"\r\n\r\n" not in recv_buffer:
92113
try:
93-
data = self.sock.recv(1024)
114+
data = sock_recv(1024)
94115
if not data:
95116
self.closed = True
96117
return None
97-
self._recv_buffer += data
118+
recv_buffer += data
98119
except OSError as e:
99120
# Handle timeout and other socket errors
100121
if hasattr(e, "errno") and e.errno in (11, 35): # EAGAIN, EWOULDBLOCK
101122
return None # No data available
102123
self.closed = True
103124
return None
104125

105-
header_end = self._recv_buffer.find(b"\r\n\r\n")
106-
header_str = self._recv_buffer[:header_end].decode("utf-8")
107-
self._recv_buffer = self._recv_buffer[header_end + 4 :]
126+
header_end = recv_buffer.find(b"\r\n\r\n")
127+
header_str = recv_buffer[:header_end].decode("utf-8")
128+
recv_buffer = recv_buffer[header_end + 4 :]
108129

109130
# Parse Content-Length
110131
content_length = 0
@@ -114,24 +135,26 @@ def recv_message(self):
114135
break
115136

116137
if content_length == 0:
138+
self._recv_buffer = recv_buffer
117139
return None
118140

119141
# Read body
120-
while len(self._recv_buffer) < content_length:
142+
while len(recv_buffer) < content_length:
121143
try:
122-
data = self.sock.recv(content_length - len(self._recv_buffer))
144+
data = sock_recv(content_length - len(recv_buffer))
123145
if not data:
124146
self.closed = True
125147
return None
126-
self._recv_buffer += data
148+
recv_buffer += data
127149
except OSError as e:
128150
if hasattr(e, "errno") and e.errno in (11, 35): # EAGAIN, EWOULDBLOCK
151+
self._recv_buffer = recv_buffer
129152
return None
130153
self.closed = True
131154
return None
132155

133-
body = self._recv_buffer[:content_length]
134-
self._recv_buffer = self._recv_buffer[content_length:]
156+
body = recv_buffer[:content_length]
157+
self._recv_buffer = recv_buffer[content_length:]
135158

136159
# Parse JSON
137160
try:

python-ecosys/debugpy/debugpy/server/debug_session.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -424,11 +424,13 @@ def _handle_source(self, seq, args):
424424
# message=f"Could not read source: {e}"
425425
)
426426

427-
def _trace_function(self, frame, event, arg):
427+
def _trace_function(self, frame, event:str, arg):
428428
"""Trace function called by sys.settrace."""
429+
# https://docs.python.org/3/library/sys.html#sys.settrace
430+
global _twiddel
429431
# Process any pending DAP messages frequently
432+
430433
self.process_pending_messages()
431-
432434
# Handle breakpoints and stepping
433435
if self.pdb.should_stop(frame, event, arg):
434436
self._send_stopped_event(
@@ -441,6 +443,10 @@ def _trace_function(self, frame, event, arg):
441443
# Wait for continue command
442444
self.pdb.wait_for_continue()
443445

446+
# The trace function is invoked (with event set to 'call') whenever a new local scope is entered;
447+
# it should return a reference to a local trace function to be used for the new scope,
448+
# or None if the scope shouldn’t be traced.
449+
444450
return self._trace_function
445451

446452
def _send_stopped_event(self, reason):

python-ecosys/debugpy/debugpy/server/pdb_adapter.py

Lines changed: 24 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,13 @@
33
import sys
44
import time
55
import os
6-
import json
6+
from micropython import const
77

88
Any = object
99
from ..common.constants import (
10+
STEP_INTO,
11+
STEP_OUT,
12+
STEP_OVER,
1013
TRACE_CALL,
1114
TRACE_LINE,
1215
TRACE_RETURN,
@@ -15,14 +18,14 @@
1518
SCOPE_GLOBALS,
1619
)
1720

18-
VARREF_LOCALS = 1
19-
VARREF_GLOBALS = 2
20-
VARREF_LOCALS_SPECIAL = 3
21-
VARREF_GLOBALS_SPECIAL = 4
21+
VARREF_LOCALS = const(1)
22+
VARREF_GLOBALS = const(2)
23+
VARREF_LOCALS_SPECIAL = const(3)
24+
VARREF_GLOBALS_SPECIAL = const(4)
2225

2326
# New constants for complex variable references
24-
VARREF_COMPLEX_BASE = 10000 # Base for complex variable references
25-
MAX_CACHE_SIZE = 50 # Limit cache size for memory constraints
27+
VARREF_COMPLEX_BASE = const(10000) # Base for complex variable references
28+
MAX_CACHE_SIZE = const(50) # Limit cache size for memory constraints
2629

2730

2831
class VariableReferenceCache:
@@ -54,17 +57,12 @@ def _cleanup_oldest(self) -> None:
5457
"""Remove oldest entries to free memory - optimized for MicroPython."""
5558
if not self.cache or not self.insertion_order:
5659
return
57-
58-
# More aggressive cleanup for memory-constrained environments
59-
to_remove = max(1, len(self.cache) // 3) # Remove 1/3 instead of 1/4
60-
60+
to_remove = max(1, len(self.cache) // 3)
6161
# Direct list slicing is more memory efficient than iteration
6262
keys_to_remove = self.insertion_order[:to_remove]
63-
6463
# Batch delete for efficiency
6564
for key in keys_to_remove:
6665
self.cache.pop(key, None) # Use pop with default to avoid KeyError
67-
6866
# Update insertion order in one operation
6967
self.insertion_order = self.insertion_order[to_remove:]
7068

@@ -188,16 +186,8 @@ def set_breakpoints(self, filename: str, breakpoints: list[dict]):
188186
if local_name != filename:
189187
self.breakpoints[local_name] = {}
190188
self._debug_print(f"[>>>] Setting breakpoints for local: {local_name}:{line}")
191-
self.breakpoints[local_name][line] = {
192-
"line": line,
193-
"verified": True,
194-
"source": {"path": filename},
195-
}
196-
self.breakpoints[filename][line] = {
197-
"line": line,
198-
"verified": True,
199-
"source": {"path": filename},
200-
}
189+
self.breakpoints[local_name][line] = {}
190+
self.breakpoints[filename][line] = {}
201191
actual_breakpoints.append(
202192
{"line": line, "verified": True, "source": {"path": filename}}
203193
)
@@ -208,20 +198,19 @@ def set_breakpoints(self, filename: str, breakpoints: list[dict]):
208198

209199
def should_stop(self, frame, event: str, arg):
210200
"""Determine if execution should stop at this point."""
201+
# HOT path - no debug printing here
211202
self.current_frame = frame
212203
self.hit_breakpoint = False
213204

214-
# Get frame information
215-
filename = frame.f_code.co_filename
205+
# Cache frame attributes to reduce lookup overhead
206+
frame_code = frame.f_code
207+
filename = frame_code.co_filename
216208
lineno = frame.f_lineno
209+
217210
# Check for exact filename match first
218211
if filename in self.breakpoints and lineno in self.breakpoints[filename]:
219-
self._debug_print(f"[PDB] HIT BREAKPOINT (exact match) at {filename}:{lineno}")
220-
# Record the path mapping (in this case, they're already the same)
221-
# self.file_mappings[filename] = self._filename_as_debugger(filename)
222212
self.hit_breakpoint = True
223213
return True
224-
# path/file.py matched - but not the line number - keep running
225214
else:
226215
# file not (yet) matched - this is slow so we do not want to do this often.
227216
# TODO: use builins - sys.path method to find the file
@@ -230,17 +219,17 @@ def should_stop(self, frame, event: str, arg):
230219
self.breakpoints[filename] = {} # Ensure the filename is in the breakpoints dict
231220
if not filename in self.file_mappings:
232221
self.file_mappings[filename] = self._filename_as_debugger(filename)
233-
self._debug_print(
234-
f"[PDB] add mapping for :'{filename}' -> '{self.file_mappings[filename]}'"
235-
)
222+
# self._debug_print(
223+
# f"[PDB] add mapping for :'{filename}' -> '{self.file_mappings[filename]}'"
224+
# )
236225

237226
# Check stepping
238-
if self.step_mode == "into":
227+
if self.step_mode == STEP_INTO:
239228
if event in (TRACE_CALL, TRACE_LINE):
240229
self.step_mode = None
241230
return True
242231

243-
elif self.step_mode == "over":
232+
elif self.step_mode == STEP_OVER:
244233
if event == TRACE_LINE and frame == self.step_frame:
245234
self.step_mode = None
246235
return True
@@ -251,7 +240,7 @@ def should_stop(self, frame, event: str, arg):
251240
else:
252241
self.step_mode = None
253242

254-
elif self.step_mode == "out":
243+
elif self.step_mode == STEP_OUT:
255244
if event == TRACE_RETURN and frame == self.step_frame:
256245
self.step_mode = None
257246
return True

0 commit comments

Comments
 (0)