]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-142186: Allow all PEP-669 events to be per-code object and disableable (GH-146182)
authorGabriele N. Tornetta <P403n1x87@users.noreply.github.com>
Wed, 22 Apr 2026 08:08:23 +0000 (09:08 +0100)
committerGitHub <noreply@github.com>
Wed, 22 Apr 2026 08:08:23 +0000 (09:08 +0100)
* Make the `PY_UNWIND` monitoring event available as a code-local
event to allow trapping on function exit events when an exception
bubbles up. This complements the PY_RETURN event by allowing to
catch any function exit event.

* Allow `PY_UNWIND`  to be `DISABLE`d; disabling it disables the event for the whole code object.

* Do the above for `PY_THROW`, `RAISE`, `EXCEPTION_HANDLED`, and `RERAISE` events.

Doc/library/sys.monitoring.rst
Doc/whatsnew/3.15.rst
Include/cpython/monitoring.h
Include/internal/pycore_ceval.h
Include/internal/pycore_instruments.h
Lib/test/test_monitoring.py
Misc/NEWS.d/next/Core_and_Builtins/2026-03-23-11-34-37.gh-issue-142186.v8Yp3W.rst [new file with mode: 0644]
Objects/genobject.c
Python/ceval.c
Python/ceval.h
Python/instrumentation.c

index 16e6b1d6dc786b08500ee81485d9d2e8473c5858..7cca6f2bcdae912b0277b7a27cd47b9db3ba64d5 100644 (file)
@@ -180,8 +180,8 @@ Local events
 ''''''''''''
 
 Local events are associated with normal execution of the program and happen
-at clearly defined locations. All local events can be disabled.
-The local events are:
+at clearly defined locations. All local events can be disabled
+per location. The local events are:
 
 * :monitoring-event:`PY_START`
 * :monitoring-event:`PY_RESUME`
@@ -205,6 +205,8 @@ Using :monitoring-event:`BRANCH_LEFT` and :monitoring-event:`BRANCH_RIGHT`
 events will give much better performance as they can be disabled
 independently.
 
+.. _monitoring-ancillary-events:
+
 Ancillary events
 ''''''''''''''''
 
@@ -226,7 +228,7 @@ Other events
 ''''''''''''
 
 Other events are not necessarily tied to a specific location in the
-program and cannot be individually disabled via :data:`DISABLE`.
+program and cannot be individually disabled per location.
 
 The other events that can be monitored are:
 
@@ -234,6 +236,12 @@ The other events that can be monitored are:
 * :monitoring-event:`PY_UNWIND`
 * :monitoring-event:`RAISE`
 * :monitoring-event:`EXCEPTION_HANDLED`
+* :monitoring-event:`RERAISE`
+
+.. versionchanged:: 3.15
+   Other events can now be turned on and disabled on a per code object
+   basis. Returning :data:`DISABLE` from a callback disables the event
+   for the entire code object (for the current tool).
 
 
 The STOP_ITERATION event
@@ -247,8 +255,7 @@ raise an exception unless it would be visible to other code.
 
 To allow tools to monitor for real exceptions without slowing down generators
 and coroutines, the :monitoring-event:`STOP_ITERATION` event is provided.
-:monitoring-event:`STOP_ITERATION` can be locally disabled, unlike
-:monitoring-event:`RAISE`.
+:monitoring-event:`STOP_ITERATION` can be locally disabled.
 
 Note that the :monitoring-event:`STOP_ITERATION` event and the
 :monitoring-event:`RAISE` event for a :exc:`StopIteration` exception are
@@ -314,15 +321,14 @@ location by returning :data:`sys.monitoring.DISABLE` from a callback function.
 This does not change which events are set, or any other code locations for the
 same event.
 
-Disabling events for specific locations is very important for high
-performance monitoring. For example, a program can be run under a
-debugger with no overhead if the debugger disables all monitoring
-except for a few breakpoints.
+:ref:`Other events <monitoring-event-global>` can be disabled on a per code
+object basis by returning :data:`sys.monitoring.DISABLE` from a callback
+function. This disables the event for the entire code object (for the current
+tool).
 
-If :data:`DISABLE` is returned by a callback for a
-:ref:`global event <monitoring-event-global>`, :exc:`ValueError` will be raised
-by the interpreter in a non-specific location (that is, no traceback will be
-provided).
+Disabling events for specific locations is very important for high performance
+monitoring. For example, a program can be run under a debugger with no overhead
+if the debugger disables all monitoring except for a few breakpoints.
 
 .. function:: restart_events() -> None
 
index c4dac339be66afcc3baa3a0f63e44dfe98b6a8c5..9630df9aad3021c3354e0fd872a27bdb0c77d54a 100644 (file)
@@ -1119,6 +1119,19 @@ sys
   (Contributed by Klaus Zimmermann in :gh:`137476`.)
 
 
+sys.monitoring
+--------------
+
+* The :ref:`other events <monitoring-event-global>`
+  (:monitoring-event:`PY_THROW`, :monitoring-event:`PY_UNWIND`,
+  :monitoring-event:`RAISE`, :monitoring-event:`EXCEPTION_HANDLED`, and
+  :monitoring-event:`RERAISE`) can now be turned on and disabled on a per code
+  object basis. Returning :data:`~sys.monitoring.DISABLE` from a callback for
+  one of these events disables the event for the entire code object (for the
+  current tool), rather than raising :exc:`ValueError` as in prior versions.
+  (Contributed by Gabriele N. Tornetta in :gh:`146182`.)
+
+
 tarfile
 -------
 
index 5094c8c23ae32bb628965105ba77ab2c7df92f05..fa6168d95cd21012f6077fc3ed868c125eaf1509 100644 (file)
@@ -24,16 +24,20 @@ extern "C" {
 #define PY_MONITORING_EVENT_STOP_ITERATION 10
 
 #define PY_MONITORING_IS_INSTRUMENTED_EVENT(ev) \
-    ((ev) < _PY_MONITORING_LOCAL_EVENTS)
+((ev) <= PY_MONITORING_EVENT_STOP_ITERATION)
 
-/* Other events, mainly exceptions */
+/* Other events, mainly exceptions.
+ * These can now be turned on and disabled on a per code object basis. */
 
-#define PY_MONITORING_EVENT_RAISE 11
+#define PY_MONITORING_EVENT_PY_UNWIND 11
 #define PY_MONITORING_EVENT_EXCEPTION_HANDLED 12
-#define PY_MONITORING_EVENT_PY_UNWIND 13
+#define PY_MONITORING_EVENT_RAISE 13
 #define PY_MONITORING_EVENT_PY_THROW 14
 #define PY_MONITORING_EVENT_RERAISE 15
 
+#define _PY_MONITORING_IS_UNGROUPED_EVENT(ev) \
+((ev) < _PY_MONITORING_UNGROUPED_EVENTS)
+
 
 /* Ancillary events */
 
index 94a1f687b7b150677254b03f43f15db140323422..ee8eb1095fe541ef3974a82f65460b2603b88a98 100644 (file)
@@ -320,7 +320,7 @@ PyObject * _PyEval_ImportNameWithImport(
 PyAPI_FUNC(PyObject *)_PyEval_MatchClass(PyThreadState *tstate, PyObject *subject, PyObject *type, Py_ssize_t nargs, PyObject *kwargs);
 PyAPI_FUNC(PyObject *)_PyEval_MatchKeys(PyThreadState *tstate, PyObject *map, PyObject *keys);
 PyAPI_FUNC(void) _PyEval_MonitorRaise(PyThreadState *tstate, _PyInterpreterFrame *frame, _Py_CODEUNIT *instr);
-PyAPI_FUNC(bool) _PyEval_NoToolsForUnwind(PyThreadState *tstate);
+PyAPI_FUNC(bool) _PyEval_NoToolsForUnwind(PyThreadState *tstate, _PyInterpreterFrame *frame);
 PyAPI_FUNC(int) _PyEval_UnpackIterableStackRef(PyThreadState *tstate, PyObject *v, int argcnt, int argcntafter, _PyStackRef *sp);
 PyAPI_FUNC(void) _PyEval_FrameClearAndPop(PyThreadState *tstate, _PyInterpreterFrame *frame);
 PyAPI_FUNC(PyObject **) _PyObjectArray_FromStackRefArray(_PyStackRef *input, Py_ssize_t nargs, PyObject **scratch);
index cb1f50e441c2655f6d13c8ceebd18933ad18778a..56b55e93a014cb5aa85b9a98f205f8f4669a416e 100644 (file)
@@ -70,16 +70,15 @@ PyAPI_DATA(PyObject) _PyInstrumentation_DISABLE;
 
 /* Total tool ids available */
 #define  PY_MONITORING_TOOL_IDS 8
-/* Count of all local monitoring events */
-#define  _PY_MONITORING_LOCAL_EVENTS 11
-/* Count of all "real" monitoring events (not derived from other events) */
+/* Count of all "real" monitoring events (not derived from other events).
+ * "Other" events can now be turned on/disabled per code object. */
 #define _PY_MONITORING_UNGROUPED_EVENTS 16
 /* Count of all  monitoring events */
 #define _PY_MONITORING_EVENTS 19
 
 /* Tables of which tools are active for each monitored event. */
 typedef struct _Py_LocalMonitors {
-    uint8_t tools[_PY_MONITORING_LOCAL_EVENTS];
+    uint8_t tools[_PY_MONITORING_UNGROUPED_EVENTS];
 } _Py_LocalMonitors;
 
 typedef struct _Py_GlobalMonitors {
index bc7af6e15380a440d32ea17fdd1869402ad6f899..b8861d09e1564b6dfaa09cc946f5a15f50dcba6c 100644 (file)
@@ -196,13 +196,10 @@ INSTRUMENTED_EVENTS = [
     (E.BRANCH, "branch"),
 ]
 
-EXCEPT_EVENTS = [
+SIMPLE_EVENTS = INSTRUMENTED_EVENTS + [
     (E.RAISE, "raise"),
-    (E.PY_UNWIND, "unwind"),
     (E.EXCEPTION_HANDLED, "exception_handled"),
-]
-
-SIMPLE_EVENTS = INSTRUMENTED_EVENTS + EXCEPT_EVENTS + [
+    (E.PY_UNWIND, "unwind"),
     (E.C_RAISE, "c_raise"),
     (E.C_RETURN, "c_return"),
 ]
@@ -738,18 +735,6 @@ class TestDisable(MonitoringTestBase, unittest.TestCase):
                 sys.monitoring.register_callback(TEST_TOOL, event, None)
 
 
-    def test_disable_illegal_events(self):
-        for event, name in EXCEPT_EVENTS:
-            try:
-                counter = CounterWithDisable()
-                counter.disable = True
-                sys.monitoring.register_callback(TEST_TOOL, event, counter)
-                sys.monitoring.set_events(TEST_TOOL, event)
-                with self.assertRaises(ValueError):
-                    self.raise_handle_reraise()
-            finally:
-                sys.monitoring.set_events(TEST_TOOL, 0)
-                sys.monitoring.register_callback(TEST_TOOL, event, None)
 
 
 class ExceptionRecorder:
@@ -1481,8 +1466,334 @@ class TestLocalEvents(MonitoringTestBase, unittest.TestCase):
             ('line', 'func3', 6)])
 
     def test_set_non_local_event(self):
+        # C_RETURN/C_RAISE are ancillary (derived) events — not settable as local
         with self.assertRaises(ValueError):
-            sys.monitoring.set_local_events(TEST_TOOL, just_call.__code__, E.RAISE)
+            sys.monitoring.set_local_events(TEST_TOOL, just_call.__code__, E.C_RETURN)
+
+    def test_local_reraise(self):
+        """RERAISE fires as a local event only for the instrumented code object."""
+
+        def foo():
+            try:
+                raise RuntimeError("test")
+            except RuntimeError:
+                raise
+
+        def bar():
+            try:
+                raise RuntimeError("test")
+            except RuntimeError:
+                raise
+
+        events = set()
+
+        def callback(code, offset, exc):
+            events.add(code.co_name)
+
+        try:
+            sys.monitoring.register_callback(TEST_TOOL, E.RERAISE, callback)
+            sys.monitoring.set_local_events(TEST_TOOL, foo.__code__, E.RERAISE)
+            try:
+                foo()
+            except RuntimeError:
+                pass
+            try:
+                bar()  # should NOT trigger the callback
+            except RuntimeError:
+                pass
+            self.assertEqual(events, {'foo'})
+        finally:
+            sys.monitoring.set_local_events(TEST_TOOL, foo.__code__, 0)
+            sys.monitoring.register_callback(TEST_TOOL, E.RERAISE, None)
+
+    def test_local_reraise_disable(self):
+        """Returning DISABLE from a RERAISE callback disables it for that code object."""
+
+        call_count = 0
+
+        def foo():
+            try:
+                raise RuntimeError("test")
+            except RuntimeError:
+                raise
+
+        def callback(code, offset, exc):
+            nonlocal call_count
+            call_count += 1
+            return sys.monitoring.DISABLE
+
+        try:
+            sys.monitoring.register_callback(TEST_TOOL, E.RERAISE, callback)
+            sys.monitoring.set_local_events(TEST_TOOL, foo.__code__, E.RERAISE)
+            try:
+                foo()
+            except RuntimeError:
+                pass
+            self.assertEqual(call_count, 1)
+            try:
+                foo()
+            except RuntimeError:
+                pass
+            self.assertEqual(call_count, 1)  # not fired again — disabled
+        finally:
+            sys.monitoring.set_local_events(TEST_TOOL, foo.__code__, 0)
+            sys.monitoring.register_callback(TEST_TOOL, E.RERAISE, None)
+
+    def test_local_py_throw(self):
+        """PY_THROW fires as a local event only for the instrumented code object."""
+
+        def gen_foo():
+            yield 1
+            yield 2
+
+        def gen_bar():
+            yield 1
+            yield 2
+
+        events = []
+
+        def callback(code, offset, exc):
+            events.append(code.co_name)
+
+        try:
+            sys.monitoring.register_callback(TEST_TOOL, E.PY_THROW, callback)
+            sys.monitoring.set_local_events(TEST_TOOL, gen_foo.__code__, E.PY_THROW)
+
+            g = gen_foo()
+            next(g)
+            try:
+                g.throw(RuntimeError("test"))
+            except RuntimeError:
+                pass
+
+            h = gen_bar()
+            next(h)
+            try:
+                h.throw(RuntimeError("test"))  # should NOT trigger the callback
+            except RuntimeError:
+                pass
+
+            self.assertEqual(events, ['gen_foo'])
+        finally:
+            sys.monitoring.set_local_events(TEST_TOOL, gen_foo.__code__, 0)
+            sys.monitoring.register_callback(TEST_TOOL, E.PY_THROW, None)
+
+    def test_local_py_throw_disable(self):
+        """Returning DISABLE from a PY_THROW callback disables it for that code object."""
+
+        call_count = 0
+
+        def gen_foo():
+            yield 1
+            yield 2
+
+        def callback(code, offset, exc):
+            nonlocal call_count
+            call_count += 1
+            return sys.monitoring.DISABLE
+
+        try:
+            sys.monitoring.register_callback(TEST_TOOL, E.PY_THROW, callback)
+            sys.monitoring.set_local_events(TEST_TOOL, gen_foo.__code__, E.PY_THROW)
+
+            g = gen_foo()
+            next(g)
+            try:
+                g.throw(RuntimeError("test"))
+            except RuntimeError:
+                pass
+            self.assertEqual(call_count, 1)
+
+            g2 = gen_foo()
+            next(g2)
+            try:
+                g2.throw(RuntimeError("test"))
+            except RuntimeError:
+                pass
+            self.assertEqual(call_count, 1)  # not fired again — disabled
+        finally:
+            sys.monitoring.set_local_events(TEST_TOOL, gen_foo.__code__, 0)
+            sys.monitoring.register_callback(TEST_TOOL, E.PY_THROW, None)
+
+    def test_local_raise(self):
+        """RAISE fires as a local event only for the instrumented code object."""
+
+        def foo():
+            try:
+                raise RuntimeError("test")
+            except RuntimeError:
+                pass
+
+        def bar():
+            try:
+                raise RuntimeError("test")
+            except RuntimeError:
+                pass
+
+        events = []
+
+        def callback(code, offset, exc):
+            events.append(code.co_name)
+
+        try:
+            sys.monitoring.register_callback(TEST_TOOL, E.RAISE, callback)
+            sys.monitoring.set_local_events(TEST_TOOL, foo.__code__, E.RAISE)
+            foo()
+            bar()  # should NOT trigger the callback
+            self.assertEqual(events, ['foo'])
+        finally:
+            sys.monitoring.set_local_events(TEST_TOOL, foo.__code__, 0)
+            sys.monitoring.register_callback(TEST_TOOL, E.RAISE, None)
+
+    def test_local_raise_disable(self):
+        """Returning DISABLE from a RAISE callback disables it for that code object."""
+
+        call_count = 0
+
+        def foo():
+            try:
+                raise RuntimeError("test")
+            except RuntimeError:
+                pass
+
+        def callback(code, offset, exc):
+            nonlocal call_count
+            call_count += 1
+            return sys.monitoring.DISABLE
+
+        try:
+            sys.monitoring.register_callback(TEST_TOOL, E.RAISE, callback)
+            sys.monitoring.set_local_events(TEST_TOOL, foo.__code__, E.RAISE)
+            foo()
+            self.assertEqual(call_count, 1)
+            foo()
+            self.assertEqual(call_count, 1)  # not fired again — disabled
+        finally:
+            sys.monitoring.set_local_events(TEST_TOOL, foo.__code__, 0)
+            sys.monitoring.register_callback(TEST_TOOL, E.RAISE, None)
+
+    def test_local_exception_handled(self):
+        """EXCEPTION_HANDLED fires as a local event only for the instrumented code object."""
+
+        def foo():
+            try:
+                raise RuntimeError("test")
+            except RuntimeError:
+                pass
+
+        def bar():
+            try:
+                raise RuntimeError("test")
+            except RuntimeError:
+                pass
+
+        events = []
+
+        def callback(code, offset, exc):
+            events.append(code.co_name)
+
+        try:
+            sys.monitoring.register_callback(TEST_TOOL, E.EXCEPTION_HANDLED, callback)
+            sys.monitoring.set_local_events(TEST_TOOL, foo.__code__, E.EXCEPTION_HANDLED)
+            foo()
+            bar()  # should NOT trigger the callback
+            self.assertEqual(events, ['foo'])
+        finally:
+            sys.monitoring.set_local_events(TEST_TOOL, foo.__code__, 0)
+            sys.monitoring.register_callback(TEST_TOOL, E.EXCEPTION_HANDLED, None)
+
+    def test_local_exception_handled_disable(self):
+        """Returning DISABLE from an EXCEPTION_HANDLED callback disables it for that code object."""
+
+        call_count = 0
+
+        def foo():
+            try:
+                raise RuntimeError("test")
+            except RuntimeError:
+                pass
+
+        def callback(code, offset, exc):
+            nonlocal call_count
+            call_count += 1
+            return sys.monitoring.DISABLE
+
+        try:
+            sys.monitoring.register_callback(TEST_TOOL, E.EXCEPTION_HANDLED, callback)
+            sys.monitoring.set_local_events(TEST_TOOL, foo.__code__, E.EXCEPTION_HANDLED)
+            foo()
+            self.assertEqual(call_count, 1)
+            foo()
+            self.assertEqual(call_count, 1)  # not fired again — disabled
+        finally:
+            sys.monitoring.set_local_events(TEST_TOOL, foo.__code__, 0)
+            sys.monitoring.register_callback(TEST_TOOL, E.EXCEPTION_HANDLED, None)
+
+    def test_local_py_unwind(self):
+        """PY_UNWIND fires as a local event only for the instrumented code object."""
+
+        def foo():
+            raise RuntimeError("test")
+
+        def bar():
+            raise RuntimeError("test")
+
+        events = []
+
+        def callback(code, offset, exc):
+            events.append(code.co_name)
+
+        try:
+            sys.monitoring.register_callback(TEST_TOOL, E.PY_UNWIND, callback)
+            sys.monitoring.set_local_events(TEST_TOOL, foo.__code__, E.PY_UNWIND)
+
+            try:
+                foo()
+            except RuntimeError:
+                pass
+
+            try:
+                bar()  # should NOT trigger the callback
+            except RuntimeError:
+                pass
+
+            self.assertEqual(events, ['foo'])
+        finally:
+            sys.monitoring.set_local_events(TEST_TOOL, foo.__code__, 0)
+            sys.monitoring.register_callback(TEST_TOOL, E.PY_UNWIND, None)
+
+    def test_local_py_unwind_disable(self):
+        """Returning DISABLE from a PY_UNWIND callback disables it for that code object."""
+
+        call_count = 0
+
+        def foo():
+            raise RuntimeError("test")
+
+        def callback(code, offset, exc):
+            nonlocal call_count
+            call_count += 1
+            return sys.monitoring.DISABLE
+
+        try:
+            sys.monitoring.register_callback(TEST_TOOL, E.PY_UNWIND, callback)
+            sys.monitoring.set_local_events(TEST_TOOL, foo.__code__, E.PY_UNWIND)
+
+            try:
+                foo()
+            except RuntimeError:
+                pass
+            self.assertEqual(call_count, 1)  # fired once
+
+            try:
+                foo()
+            except RuntimeError:
+                pass
+            self.assertEqual(call_count, 1)  # not fired again — disabled by DISABLE return
+
+        finally:
+            sys.monitoring.set_local_events(TEST_TOOL, foo.__code__, 0)
+            sys.monitoring.register_callback(TEST_TOOL, E.PY_UNWIND, None)
 
 def line_from_offset(code, offset):
     for start, end, line in code.co_lines():
diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-03-23-11-34-37.gh-issue-142186.v8Yp3W.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-03-23-11-34-37.gh-issue-142186.v8Yp3W.rst
new file mode 100644 (file)
index 0000000..4a04658
--- /dev/null
@@ -0,0 +1,3 @@
+Global :mod:`sys.monitoring` events can now be turned on and disabled on a
+per code object basis. Returning ``DISABLE`` from a callback disables the
+event for the entire code object (for the current tool).
index 2bbe79c253d3e641863797c9e1668db2e3ea7379..d628889afc6dc8f169d68832aa286b9ffc12a0c0 100644 (file)
@@ -496,7 +496,7 @@ gen_close(PyObject *self, PyObject *args)
     }
 
     if (is_resume(frame->instr_ptr)) {
-        bool no_unwind_tools = _PyEval_NoToolsForUnwind(_PyThreadState_GET());
+        bool no_unwind_tools = _PyEval_NoToolsForUnwind(_PyThreadState_GET(), frame);
         /* We can safely ignore the outermost try block
          * as it is automatically generated to handle
          * StopIteration. */
index 03bc5229565966b58ff8612e1e45334f49a6a11a..967d92f4ea68557b8e2411ab19d3453ddc12aed3 100644 (file)
@@ -2406,15 +2406,16 @@ void
 _PyEval_MonitorRaise(PyThreadState *tstate, _PyInterpreterFrame *frame,
               _Py_CODEUNIT *instr)
 {
-    if (no_tools_for_global_event(tstate, PY_MONITORING_EVENT_RAISE)) {
+    if (no_tools_for_local_event(tstate, frame, PY_MONITORING_EVENT_RAISE)) {
         return;
     }
     do_monitor_exc(tstate, frame, instr, PY_MONITORING_EVENT_RAISE);
 }
 
 bool
-_PyEval_NoToolsForUnwind(PyThreadState *tstate) {
-    return no_tools_for_global_event(tstate, PY_MONITORING_EVENT_PY_UNWIND);
+_PyEval_NoToolsForUnwind(PyThreadState *tstate, _PyInterpreterFrame *frame)
+{
+    return no_tools_for_local_event(tstate, frame, PY_MONITORING_EVENT_PY_UNWIND);
 }
 
 
index bb5f7ddb85724674a8772b260ae09450fcbce571..0437ab85c5a668232c9709665aa848e1ee581914 100644 (file)
@@ -367,7 +367,7 @@ no_tools_for_global_event(PyThreadState *tstate, int event)
 static inline bool
 no_tools_for_local_event(PyThreadState *tstate, _PyInterpreterFrame *frame, int event)
 {
-    assert(event < _PY_MONITORING_LOCAL_EVENTS);
+    assert(event < _PY_MONITORING_UNGROUPED_EVENTS);
     _PyCoMonitoringData *data = _PyFrame_GetCode(frame)->_co_monitoring;
     if (data) {
         return data->active_monitors.tools[event] == 0;
@@ -382,7 +382,7 @@ monitor_handled(PyThreadState *tstate,
                 _PyInterpreterFrame *frame,
                 _Py_CODEUNIT *instr, PyObject *exc)
 {
-    if (no_tools_for_global_event(tstate, PY_MONITORING_EVENT_EXCEPTION_HANDLED)) {
+    if (no_tools_for_local_event(tstate, frame, PY_MONITORING_EVENT_EXCEPTION_HANDLED)) {
         return 0;
     }
     return _Py_call_instrumentation_arg(tstate, PY_MONITORING_EVENT_EXCEPTION_HANDLED, frame, instr, exc);
@@ -393,7 +393,7 @@ monitor_throw(PyThreadState *tstate,
               _PyInterpreterFrame *frame,
               _Py_CODEUNIT *instr)
 {
-    if (no_tools_for_global_event(tstate, PY_MONITORING_EVENT_PY_THROW)) {
+    if (no_tools_for_local_event(tstate, frame, PY_MONITORING_EVENT_PY_THROW)) {
         return;
     }
     do_monitor_exc(tstate, frame, instr, PY_MONITORING_EVENT_PY_THROW);
@@ -403,7 +403,7 @@ static void
 monitor_reraise(PyThreadState *tstate, _PyInterpreterFrame *frame,
               _Py_CODEUNIT *instr)
 {
-    if (no_tools_for_global_event(tstate, PY_MONITORING_EVENT_RERAISE)) {
+    if (no_tools_for_local_event(tstate, frame, PY_MONITORING_EVENT_RERAISE)) {
         return;
     }
     do_monitor_exc(tstate, frame, instr, PY_MONITORING_EVENT_RERAISE);
@@ -431,7 +431,7 @@ monitor_unwind(PyThreadState *tstate,
                _PyInterpreterFrame *frame,
                _Py_CODEUNIT *instr)
 {
-    if (no_tools_for_global_event(tstate, PY_MONITORING_EVENT_PY_UNWIND)) {
+    if (no_tools_for_local_event(tstate, frame, PY_MONITORING_EVENT_PY_UNWIND)) {
         return;
     }
     do_monitor_exc(tstate, frame, instr, PY_MONITORING_EVENT_PY_UNWIND);
index 4041aa0d8aeaaf4473d1c8878674e53f9bb1f081..51bcbfdb3b6c55b5fdc06cd1ecd0d8a80aa58697 100644 (file)
@@ -203,7 +203,7 @@ is_instrumented(int opcode)
 static inline bool
 monitors_equals(_Py_LocalMonitors a, _Py_LocalMonitors b)
 {
-    for (int i = 0; i < _PY_MONITORING_LOCAL_EVENTS; i++) {
+    for (int i = 0; i < _PY_MONITORING_UNGROUPED_EVENTS; i++) {
         if (a.tools[i] != b.tools[i]) {
             return false;
         }
@@ -216,7 +216,7 @@ static inline _Py_LocalMonitors
 monitors_sub(_Py_LocalMonitors a, _Py_LocalMonitors b)
 {
     _Py_LocalMonitors res;
-    for (int i = 0; i < _PY_MONITORING_LOCAL_EVENTS; i++) {
+    for (int i = 0; i < _PY_MONITORING_UNGROUPED_EVENTS; i++) {
         res.tools[i] = a.tools[i] & ~b.tools[i];
     }
     return res;
@@ -227,7 +227,7 @@ static inline _Py_LocalMonitors
 monitors_and(_Py_LocalMonitors a, _Py_LocalMonitors b)
 {
     _Py_LocalMonitors res;
-    for (int i = 0; i < _PY_MONITORING_LOCAL_EVENTS; i++) {
+    for (int i = 0; i < _PY_MONITORING_UNGROUPED_EVENTS; i++) {
         res.tools[i] = a.tools[i] & b.tools[i];
     }
     return res;
@@ -243,7 +243,7 @@ static inline _Py_LocalMonitors
 local_union(_Py_GlobalMonitors a, _Py_LocalMonitors b)
 {
     _Py_LocalMonitors res;
-    for (int i = 0; i < _PY_MONITORING_LOCAL_EVENTS; i++) {
+    for (int i = 0; i < _PY_MONITORING_UNGROUPED_EVENTS; i++) {
         res.tools[i] = a.tools[i] | b.tools[i];
     }
     return res;
@@ -252,7 +252,7 @@ local_union(_Py_GlobalMonitors a, _Py_LocalMonitors b)
 static inline bool
 monitors_are_empty(_Py_LocalMonitors m)
 {
-    for (int i = 0; i < _PY_MONITORING_LOCAL_EVENTS; i++) {
+    for (int i = 0; i < _PY_MONITORING_UNGROUPED_EVENTS; i++) {
         if (m.tools[i]) {
             return false;
         }
@@ -263,7 +263,7 @@ monitors_are_empty(_Py_LocalMonitors m)
 static inline bool
 multiple_tools(_Py_LocalMonitors *m)
 {
-    for (int i = 0; i < _PY_MONITORING_LOCAL_EVENTS; i++) {
+    for (int i = 0; i < _PY_MONITORING_UNGROUPED_EVENTS; i++) {
         if (_Py_popcount32(m->tools[i]) > 1) {
             return true;
         }
@@ -275,7 +275,7 @@ static inline _PyMonitoringEventSet
 get_local_events(_Py_LocalMonitors *m, int tool_id)
 {
     _PyMonitoringEventSet result = 0;
-    for (int e = 0; e < _PY_MONITORING_LOCAL_EVENTS; e++) {
+    for (int e = 0; e < _PY_MONITORING_UNGROUPED_EVENTS; e++) {
         if ((m->tools[e] >> tool_id) & 1) {
             result |= (1 << e);
         }
@@ -453,7 +453,7 @@ static void
 dump_local_monitors(const char *prefix, _Py_LocalMonitors monitors, FILE*out)
 {
     fprintf(out, "%s monitors:\n", prefix);
-    for (int event = 0; event < _PY_MONITORING_LOCAL_EVENTS; event++) {
+    for (int event = 0; event < _PY_MONITORING_UNGROUPED_EVENTS; event++) {
         fprintf(out, "    Event %d: Tools %x\n", event, monitors.tools[event]);
     }
 }
@@ -1102,8 +1102,10 @@ get_tools_for_instruction(PyCodeObject *code, PyInterpreterState *interp, int i,
                 event == PY_MONITORING_EVENT_C_RETURN);
         event = PY_MONITORING_EVENT_CALL;
     }
+    assert(_PY_MONITORING_IS_UNGROUPED_EVENT(event));
+    CHECK(debug_check_sanity(interp, code));
     if (PY_MONITORING_IS_INSTRUMENTED_EVENT(event)) {
-        CHECK(debug_check_sanity(interp, code));
+        /* Instrumented events use per-instruction tool bitmaps. */
         if (code->_co_monitoring->tools) {
             tools = code->_co_monitoring->tools[i];
         }
@@ -1112,7 +1114,9 @@ get_tools_for_instruction(PyCodeObject *code, PyInterpreterState *interp, int i,
         }
     }
     else {
-        tools = interp->monitors.tools[event];
+        /* Other (non-instrumented) events are not tied to specific instructions;
+         * use the code-object-level active_monitors bitmap instead. */
+        tools = code->_co_monitoring->active_monitors.tools[event];
     }
     return tools;
 }
@@ -1139,6 +1143,25 @@ static const char *const event_names [] = {
     [PY_MONITORING_EVENT_STOP_ITERATION] = "STOP_ITERATION",
 };
 
+/* Disable an "other" (non-instrumented) event (e.g. PY_UNWIND) for a single
+ * tool on this code object.  Must be called with the world stopped or the
+ * code lock held. */
+static void
+remove_local_tool(PyCodeObject *code, PyInterpreterState *interp,
+                  int event, int tool)
+{
+    ASSERT_WORLD_STOPPED_OR_LOCKED(code);
+    assert(_PY_MONITORING_IS_UNGROUPED_EVENT(event));
+    assert(!PY_MONITORING_IS_INSTRUMENTED_EVENT(event));
+    assert(code->_co_monitoring);
+    code->_co_monitoring->local_monitors.tools[event] &= ~(1 << tool);
+    /* Recompute active_monitors for this event as the union of global and
+     * (now updated) local monitors. */
+    code->_co_monitoring->active_monitors.tools[event] =
+        interp->monitors.tools[event] |
+        code->_co_monitoring->local_monitors.tools[event];
+}
+
 static int
 call_instrumentation_vector(
     _Py_CODEUNIT *instr, PyThreadState *tstate, int event,
@@ -1183,7 +1206,18 @@ call_instrumentation_vector(
         }
         else {
             /* DISABLE */
-            if (!PY_MONITORING_IS_INSTRUMENTED_EVENT(event)) {
+            if (PY_MONITORING_IS_INSTRUMENTED_EVENT(event)) {
+                _PyEval_StopTheWorld(interp);
+                remove_tools(code, offset, event, 1 << tool);
+                _PyEval_StartTheWorld(interp);
+            }
+            else if (_PY_MONITORING_IS_UNGROUPED_EVENT(event)) {
+                /* Other (non-instrumented) event: disable for this code object. */
+                _PyEval_StopTheWorld(interp);
+                remove_local_tool(code, interp, event, tool);
+                _PyEval_StartTheWorld(interp);
+            }
+            else {
                 PyErr_Format(PyExc_ValueError,
                               "Cannot disable %s events. Callback removed.",
                              event_names[event]);
@@ -1192,12 +1226,6 @@ call_instrumentation_vector(
                 err = -1;
                 break;
             }
-            else {
-                PyInterpreterState *interp = tstate->interp;
-                _PyEval_StopTheWorld(interp);
-                remove_tools(code, offset, event, 1 << tool);
-                _PyEval_StartTheWorld(interp);
-            }
         }
     }
     Py_DECREF(arg2_obj);
@@ -1681,7 +1709,7 @@ update_instrumentation_data(PyCodeObject *code, PyInterpreterState *interp)
     _Py_LocalMonitors *local_monitors = &code->_co_monitoring->local_monitors;
     for (int i = 0; i < PY_MONITORING_TOOL_IDS; i++) {
         if (code->_co_monitoring->tool_versions[i] != interp->monitoring_tool_versions[i]) {
-            for (int j = 0; j < _PY_MONITORING_LOCAL_EVENTS; j++) {
+            for (int j = 0; j < _PY_MONITORING_UNGROUPED_EVENTS; j++) {
                 local_monitors->tools[j] &= ~(1 << i);
             }
         }
@@ -1977,7 +2005,7 @@ static void
 set_local_events(_Py_LocalMonitors *m, int tool_id, _PyMonitoringEventSet events)
 {
     assert(0 <= tool_id && tool_id < PY_MONITORING_TOOL_IDS);
-    for (int e = 0; e < _PY_MONITORING_LOCAL_EVENTS; e++) {
+    for (int e = 0; e < _PY_MONITORING_UNGROUPED_EVENTS; e++) {
         uint8_t *tools = &m->tools[e];
         int val = (events >> e) & 1;
         *tools &= ~(1 << tool_id);
@@ -2037,7 +2065,7 @@ _PyMonitoring_SetLocalEvents(PyCodeObject *code, int tool_id, _PyMonitoringEvent
 
     assert(0 <= tool_id && tool_id < PY_MONITORING_TOOL_IDS);
     PyInterpreterState *interp = _PyInterpreterState_GET();
-    assert(events < (1 << _PY_MONITORING_LOCAL_EVENTS));
+    assert(events < (1 << _PY_MONITORING_UNGROUPED_EVENTS));
     if (code->_co_firsttraceable >= Py_SIZE(code)) {
         PyErr_Format(PyExc_SystemError, "cannot instrument shim code object '%U'", code->co_name);
         return -1;
@@ -2373,7 +2401,7 @@ monitoring_get_local_events_impl(PyObject *module, int tool_id,
     _PyMonitoringEventSet event_set = 0;
     _PyCoMonitoringData *data = ((PyCodeObject *)code)->_co_monitoring;
     if (data != NULL) {
-        for (int e = 0; e < _PY_MONITORING_LOCAL_EVENTS; e++) {
+        for (int e = 0; e < _PY_MONITORING_UNGROUPED_EVENTS; e++) {
             if ((data->local_monitors.tools[e] >> tool_id) & 1) {
                 event_set |= (1 << e);
             }
@@ -2416,7 +2444,7 @@ monitoring_set_local_events_impl(PyObject *module, int tool_id,
         event_set &= ~(1 << PY_MONITORING_EVENT_BRANCH);
         event_set |= (1 << PY_MONITORING_EVENT_BRANCH_RIGHT) | (1 << PY_MONITORING_EVENT_BRANCH_LEFT);
     }
-    if (event_set < 0 || event_set >= (1 << _PY_MONITORING_LOCAL_EVENTS)) {
+    if (event_set < 0 || event_set >= (1 << _PY_MONITORING_UNGROUPED_EVENTS)) {
         PyErr_Format(PyExc_ValueError, "invalid local event set 0x%x", event_set);
         return NULL;
     }