Skip to content

Commit 5e0abb4

Browse files
gh-116750: Add clear_tool_id function to unregister events and callbacks (#124568)
1 parent b482538 commit 5e0abb4

File tree

8 files changed

+165
-8
lines changed

8 files changed

+165
-8
lines changed

Doc/library/sys.monitoring.rst

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -50,16 +50,14 @@ Registering and using tools
5050
*tool_id* must be in the range 0 to 5 inclusive.
5151
Raises a :exc:`ValueError` if *tool_id* is in use.
5252

53-
.. function:: free_tool_id(tool_id: int, /) -> None
53+
.. function:: clear_tool_id(tool_id: int, /) -> None
5454

55-
Should be called once a tool no longer requires *tool_id*.
55+
Unregister all events and callback functions associated with *tool_id*.
5656

57-
.. note::
57+
.. function:: free_tool_id(tool_id: int, /) -> None
5858

59-
:func:`free_tool_id` will not disable global or local events associated
60-
with *tool_id*, nor will it unregister any callback functions. This
61-
function is only intended to be used to notify the VM that the
62-
particular *tool_id* is no longer in use.
59+
Should be called once a tool no longer requires *tool_id*.
60+
Will call :func:`clear_tool_id` before releasing *tool_id*.
6361

6462
.. function:: get_tool(tool_id: int, /) -> str | None
6563

Include/cpython/code.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
extern "C" {
99
#endif
1010

11+
/* Total tool ids available */
12+
#define _PY_MONITORING_TOOL_IDS 8
1113
/* Count of all local monitoring events */
1214
#define _PY_MONITORING_LOCAL_EVENTS 10
1315
/* Count of all "real" monitoring events (not derived from other events) */
@@ -57,6 +59,8 @@ typedef struct {
5759
_Py_LocalMonitors active_monitors;
5860
/* The tools that are to be notified for events for the matching code unit */
5961
uint8_t *tools;
62+
/* The version of tools when they instrument the code */
63+
uintptr_t tool_versions[_PY_MONITORING_TOOL_IDS];
6064
/* Information to support line events */
6165
_PyCoLineInstrumentationData *lines;
6266
/* The tools that are to be notified for line events for the matching code unit */

Include/internal/pycore_interp.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,7 @@ struct _is {
272272
Py_ssize_t sys_tracing_threads; /* Count of threads with c_tracefunc set */
273273
PyObject *monitoring_callables[PY_MONITORING_TOOL_IDS][_PY_MONITORING_EVENTS];
274274
PyObject *monitoring_tool_names[PY_MONITORING_TOOL_IDS];
275+
uintptr_t monitoring_tool_versions[PY_MONITORING_TOOL_IDS];
275276

276277
struct _Py_interp_cached_objects cached_objects;
277278
struct _Py_interp_static_objects static_objects;

Lib/test/test_monitoring.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,14 @@ def nth_line(func, offset):
4646

4747
class MonitoringBasicTest(unittest.TestCase):
4848

49+
def tearDown(self):
50+
sys.monitoring.free_tool_id(TEST_TOOL)
51+
4952
def test_has_objects(self):
5053
m = sys.monitoring
5154
m.events
5255
m.use_tool_id
56+
m.clear_tool_id
5357
m.free_tool_id
5458
m.get_tool
5559
m.get_events
@@ -77,6 +81,43 @@ def test_tool(self):
7781
with self.assertRaises(ValueError):
7882
sys.monitoring.set_events(TEST_TOOL, sys.monitoring.events.CALL)
7983

84+
def test_clear(self):
85+
events = []
86+
sys.monitoring.use_tool_id(TEST_TOOL, "MonitoringTest.Tool")
87+
sys.monitoring.register_callback(TEST_TOOL, E.PY_START, lambda *args: events.append(args))
88+
sys.monitoring.register_callback(TEST_TOOL, E.LINE, lambda *args: events.append(args))
89+
def f():
90+
a = 1
91+
sys.monitoring.set_local_events(TEST_TOOL, f.__code__, E.LINE)
92+
sys.monitoring.set_events(TEST_TOOL, E.PY_START)
93+
94+
f()
95+
sys.monitoring.clear_tool_id(TEST_TOOL)
96+
f()
97+
98+
# the first f() should trigger a PY_START and a LINE event
99+
# the second f() after clear_tool_id should not trigger any event
100+
# the callback function should be cleared as well
101+
self.assertEqual(len(events), 2)
102+
callback = sys.monitoring.register_callback(TEST_TOOL, E.LINE, None)
103+
self.assertIs(callback, None)
104+
105+
sys.monitoring.free_tool_id(TEST_TOOL)
106+
107+
events = []
108+
sys.monitoring.use_tool_id(TEST_TOOL, "MonitoringTest.Tool")
109+
sys.monitoring.register_callback(TEST_TOOL, E.LINE, lambda *args: events.append(args))
110+
sys.monitoring.set_local_events(TEST_TOOL, f.__code__, E.LINE)
111+
f()
112+
sys.monitoring.free_tool_id(TEST_TOOL)
113+
sys.monitoring.use_tool_id(TEST_TOOL, "MonitoringTest.Tool")
114+
f()
115+
# the first f() should trigger a LINE event, and even if we use the
116+
# tool id immediately after freeing it, the second f() should not
117+
# trigger any event
118+
self.assertEqual(len(events), 1)
119+
sys.monitoring.free_tool_id(TEST_TOOL)
120+
80121

81122
class MonitoringTestBase:
82123

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Provide :func:`sys.monitoring.clear_tool_id` to unregister all events and callbacks set by the tool.

Python/clinic/instrumentation.c.h

Lines changed: 28 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Python/instrumentation.c

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1660,6 +1660,16 @@ update_instrumentation_data(PyCodeObject *code, PyInterpreterState *interp)
16601660
if (allocate_instrumentation_data(code)) {
16611661
return -1;
16621662
}
1663+
// If the local monitors are out of date, clear them up
1664+
_Py_LocalMonitors *local_monitors = &code->_co_monitoring->local_monitors;
1665+
for (int i = 0; i < PY_MONITORING_TOOL_IDS; i++) {
1666+
if (code->_co_monitoring->tool_versions[i] != interp->monitoring_tool_versions[i]) {
1667+
for (int j = 0; j < _PY_MONITORING_LOCAL_EVENTS; j++) {
1668+
local_monitors->tools[j] &= ~(1 << i);
1669+
}
1670+
}
1671+
}
1672+
16631673
_Py_LocalMonitors all_events = local_union(
16641674
interp->monitors,
16651675
code->_co_monitoring->local_monitors);
@@ -2004,6 +2014,8 @@ _PyMonitoring_SetLocalEvents(PyCodeObject *code, int tool_id, _PyMonitoringEvent
20042014
goto done;
20052015
}
20062016

2017+
code->_co_monitoring->tool_versions[tool_id] = interp->monitoring_tool_versions[tool_id];
2018+
20072019
_Py_LocalMonitors *local = &code->_co_monitoring->local_monitors;
20082020
uint32_t existing_events = get_local_events(local, tool_id);
20092021
if (existing_events == events) {
@@ -2036,6 +2048,43 @@ _PyMonitoring_GetLocalEvents(PyCodeObject *code, int tool_id, _PyMonitoringEvent
20362048
return 0;
20372049
}
20382050

2051+
int _PyMonitoring_ClearToolId(int tool_id)
2052+
{
2053+
assert(0 <= tool_id && tool_id < PY_MONITORING_TOOL_IDS);
2054+
PyInterpreterState *interp = _PyInterpreterState_GET();
2055+
2056+
for (int i = 0; i < _PY_MONITORING_EVENTS; i++) {
2057+
PyObject *func = _PyMonitoring_RegisterCallback(tool_id, i, NULL);
2058+
if (func != NULL) {
2059+
Py_DECREF(func);
2060+
}
2061+
}
2062+
2063+
if (_PyMonitoring_SetEvents(tool_id, 0) < 0) {
2064+
return -1;
2065+
}
2066+
2067+
_PyEval_StopTheWorld(interp);
2068+
uint32_t version = global_version(interp) + MONITORING_VERSION_INCREMENT;
2069+
if (version == 0) {
2070+
PyErr_Format(PyExc_OverflowError, "events set too many times");
2071+
_PyEval_StartTheWorld(interp);
2072+
return -1;
2073+
}
2074+
2075+
// monitoring_tool_versions[tool_id] is set to latest global version here to
2076+
// 1. invalidate local events on all existing code objects
2077+
// 2. be ready for the next call to set local events
2078+
interp->monitoring_tool_versions[tool_id] = version;
2079+
2080+
// Set the new global version so all the code objects can refresh the
2081+
// instrumentation.
2082+
set_global_version(_PyThreadState_GET(), version);
2083+
int res = instrument_all_executing_code_objects(interp);
2084+
_PyEval_StartTheWorld(interp);
2085+
return res;
2086+
}
2087+
20392088
/*[clinic input]
20402089
module monitoring
20412090
[clinic start generated code]*/
@@ -2083,6 +2132,33 @@ monitoring_use_tool_id_impl(PyObject *module, int tool_id, PyObject *name)
20832132
Py_RETURN_NONE;
20842133
}
20852134

2135+
/*[clinic input]
2136+
monitoring.clear_tool_id
2137+
2138+
tool_id: int
2139+
/
2140+
2141+
[clinic start generated code]*/
2142+
2143+
static PyObject *
2144+
monitoring_clear_tool_id_impl(PyObject *module, int tool_id)
2145+
/*[clinic end generated code: output=04defc23470b1be7 input=af643d6648a66163]*/
2146+
{
2147+
if (check_valid_tool(tool_id)) {
2148+
return NULL;
2149+
}
2150+
2151+
PyInterpreterState *interp = _PyInterpreterState_GET();
2152+
2153+
if (interp->monitoring_tool_names[tool_id] != NULL) {
2154+
if (_PyMonitoring_ClearToolId(tool_id) < 0) {
2155+
return NULL;
2156+
}
2157+
}
2158+
2159+
Py_RETURN_NONE;
2160+
}
2161+
20862162
/*[clinic input]
20872163
monitoring.free_tool_id
20882164
@@ -2099,6 +2175,13 @@ monitoring_free_tool_id_impl(PyObject *module, int tool_id)
20992175
return NULL;
21002176
}
21012177
PyInterpreterState *interp = _PyInterpreterState_GET();
2178+
2179+
if (interp->monitoring_tool_names[tool_id] != NULL) {
2180+
if (_PyMonitoring_ClearToolId(tool_id) < 0) {
2181+
return NULL;
2182+
}
2183+
}
2184+
21022185
Py_CLEAR(interp->monitoring_tool_names[tool_id]);
21032186
Py_RETURN_NONE;
21042187
}
@@ -2376,6 +2459,7 @@ monitoring__all_events_impl(PyObject *module)
23762459

23772460
static PyMethodDef methods[] = {
23782461
MONITORING_USE_TOOL_ID_METHODDEF
2462+
MONITORING_CLEAR_TOOL_ID_METHODDEF
23792463
MONITORING_FREE_TOOL_ID_METHODDEF
23802464
MONITORING_GET_TOOL_METHODDEF
23812465
MONITORING_REGISTER_CALLBACK_METHODDEF

Python/pystate.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -654,6 +654,7 @@ init_interpreter(PyInterpreterState *interp,
654654
interp->monitoring_callables[t][e] = NULL;
655655

656656
}
657+
interp->monitoring_tool_versions[t] = 0;
657658
}
658659
interp->sys_profile_initialized = false;
659660
interp->sys_trace_initialized = false;

0 commit comments

Comments
 (0)