Skip to content

Commit 6d743f9

Browse files
committed
add DISABLE support for PY_UNWIND
1 parent 1a3e828 commit 6d743f9

File tree

2 files changed

+99
-8
lines changed

2 files changed

+99
-8
lines changed

Lib/test/test_monitoring.py

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,11 +198,11 @@ def test_c_return_count(self):
198198

199199
EXCEPT_EVENTS = [
200200
(E.RAISE, "raise"),
201-
(E.PY_UNWIND, "unwind"),
202201
(E.EXCEPTION_HANDLED, "exception_handled"),
203202
]
204203

205204
SIMPLE_EVENTS = INSTRUMENTED_EVENTS + EXCEPT_EVENTS + [
205+
(E.PY_UNWIND, "unwind"), # local event (not in EXCEPT_EVENTS: DISABLE is legal)
206206
(E.C_RAISE, "c_raise"),
207207
(E.C_RETURN, "c_return"),
208208
]
@@ -1484,6 +1484,72 @@ def test_set_non_local_event(self):
14841484
with self.assertRaises(ValueError):
14851485
sys.monitoring.set_local_events(TEST_TOOL, just_call.__code__, E.RAISE)
14861486

1487+
def test_local_py_unwind(self):
1488+
"""PY_UNWIND fires as a local event only for the instrumented code object."""
1489+
1490+
def foo():
1491+
raise RuntimeError("test")
1492+
1493+
def bar():
1494+
raise RuntimeError("test")
1495+
1496+
events = []
1497+
1498+
def callback(code, offset, exc):
1499+
events.append(code.co_name)
1500+
1501+
try:
1502+
sys.monitoring.register_callback(TEST_TOOL, E.PY_UNWIND, callback)
1503+
sys.monitoring.set_local_events(TEST_TOOL, foo.__code__, E.PY_UNWIND)
1504+
1505+
try:
1506+
foo()
1507+
except RuntimeError:
1508+
pass
1509+
1510+
try:
1511+
bar() # should NOT trigger the callback
1512+
except RuntimeError:
1513+
pass
1514+
1515+
self.assertEqual(events, ['foo'])
1516+
finally:
1517+
sys.monitoring.set_local_events(TEST_TOOL, foo.__code__, 0)
1518+
sys.monitoring.register_callback(TEST_TOOL, E.PY_UNWIND, None)
1519+
1520+
def test_local_py_unwind_disable(self):
1521+
"""Returning DISABLE from a PY_UNWIND callback disables it for that code object."""
1522+
1523+
call_count = 0
1524+
1525+
def foo():
1526+
raise RuntimeError("test")
1527+
1528+
def callback(code, offset, exc):
1529+
nonlocal call_count
1530+
call_count += 1
1531+
return sys.monitoring.DISABLE
1532+
1533+
try:
1534+
sys.monitoring.register_callback(TEST_TOOL, E.PY_UNWIND, callback)
1535+
sys.monitoring.set_local_events(TEST_TOOL, foo.__code__, E.PY_UNWIND)
1536+
1537+
try:
1538+
foo()
1539+
except RuntimeError:
1540+
pass
1541+
self.assertEqual(call_count, 1) # fired once
1542+
1543+
try:
1544+
foo()
1545+
except RuntimeError:
1546+
pass
1547+
self.assertEqual(call_count, 1) # not fired again — disabled by DISABLE return
1548+
1549+
finally:
1550+
sys.monitoring.set_local_events(TEST_TOOL, foo.__code__, 0)
1551+
sys.monitoring.register_callback(TEST_TOOL, E.PY_UNWIND, None)
1552+
14871553
def line_from_offset(code, offset):
14881554
for start, end, line in code.co_lines():
14891555
if start <= offset < end:

Python/instrumentation.c

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1139,6 +1139,25 @@ static const char *const event_names [] = {
11391139
[PY_MONITORING_EVENT_STOP_ITERATION] = "STOP_ITERATION",
11401140
};
11411141

1142+
/* Disable a local-but-not-instrumented event (e.g. PY_UNWIND) for a single
1143+
* tool on this code object. Must be called with the world stopped or the
1144+
* code lock held. */
1145+
static void
1146+
remove_local_tool(PyCodeObject *code, PyInterpreterState *interp,
1147+
int event, int tool)
1148+
{
1149+
ASSERT_WORLD_STOPPED_OR_LOCKED(code);
1150+
assert(PY_MONITORING_IS_LOCAL_EVENT(event));
1151+
assert(!PY_MONITORING_IS_INSTRUMENTED_EVENT(event));
1152+
assert(code->_co_monitoring);
1153+
code->_co_monitoring->local_monitors.tools[event] &= ~(1 << tool);
1154+
/* Recompute active_monitors for this event as the union of global and
1155+
* (now updated) local monitors. */
1156+
code->_co_monitoring->active_monitors.tools[event] =
1157+
interp->monitors.tools[event] |
1158+
code->_co_monitoring->local_monitors.tools[event];
1159+
}
1160+
11421161
static int
11431162
call_instrumentation_vector(
11441163
_Py_CODEUNIT *instr, PyThreadState *tstate, int event,
@@ -1183,7 +1202,19 @@ call_instrumentation_vector(
11831202
}
11841203
else {
11851204
/* DISABLE */
1186-
if (!PY_MONITORING_IS_INSTRUMENTED_EVENT(event)) {
1205+
if (PY_MONITORING_IS_INSTRUMENTED_EVENT(event)) {
1206+
_PyEval_StopTheWorld(interp);
1207+
remove_tools(code, offset, event, 1 << tool);
1208+
_PyEval_StartTheWorld(interp);
1209+
}
1210+
else if (PY_MONITORING_IS_LOCAL_EVENT(event)) {
1211+
/* Local but not tied to a bytecode instruction: disable for
1212+
* this code object entirely. */
1213+
_PyEval_StopTheWorld(interp);
1214+
remove_local_tool(code, interp, event, tool);
1215+
_PyEval_StartTheWorld(interp);
1216+
}
1217+
else {
11871218
PyErr_Format(PyExc_ValueError,
11881219
"Cannot disable %s events. Callback removed.",
11891220
event_names[event]);
@@ -1192,12 +1223,6 @@ call_instrumentation_vector(
11921223
err = -1;
11931224
break;
11941225
}
1195-
else {
1196-
PyInterpreterState *interp = tstate->interp;
1197-
_PyEval_StopTheWorld(interp);
1198-
remove_tools(code, offset, event, 1 << tool);
1199-
_PyEval_StartTheWorld(interp);
1200-
}
12011226
}
12021227
}
12031228
Py_DECREF(arg2_obj);

0 commit comments

Comments
 (0)