https://github.com/python/cpython/commit/f0f76de0c8521e51c2e5c1da6de23373ec39231c
commit: f0f76de0c8521e51c2e5c1da6de23373ec39231c
branch: main
author: limadog9 <[email protected]>
committer: gaogaotiantian <[email protected]>
date: 2026-10-19T12:17:16-07:00
summary:
gh-158121: Preserve instruction events when line monitoring is disabled
(#158456)
files:
A
Misc/NEWS.d/next/Core_and_Builtins/2026-09-29-14-35-00.gh-issue-158121.Kj4nQ8.rst
M Lib/test/test_free_threading/test_monitoring.py
M Lib/test/test_monitoring.py
M Python/instrumentation.c
diff --git a/Lib/test/test_free_threading/test_monitoring.py
b/Lib/test/test_free_threading/test_monitoring.py
index 2cd6e7b035ecb41..1ce84ce38d8273b 100644
--- a/Lib/test/test_free_threading/test_monitoring.py
+++ b/Lib/test/test_free_threading/test_monitoring.py
@@ -1,6 +1,7 @@
"""Tests monitoring, sys.settrace, and sys.setprofile in a multi-threaded
environment to verify things are thread-safe in a free-threaded build"""
+import dis
import sys
import threading
import time
@@ -251,6 +252,62 @@ def append(self, trace):
@threading_helper.requires_working_threading()
class MonitoringMisc(MonitoringTestMixin, TestCase):
+ def test_disable_line_keeps_instruction_events(self):
+ def func(x):
+ a = x + 1
+ b = a * 2
+ return b
+
+ code = func.__code__
+ expected = [instr.offset for instr in dis.get_instructions(func)
+ if instr.opname != "RESUME"]
+ # Two threads, each with two lists of instruction offsets (one per
call).
+ # Check that disabling LINE preserves INSTRUCTION events in every
+ # thread-local bytecode copy, including on subsequent calls.
+ records = [[[], []] for _ in range(2)]
+ local = threading.local()
+ ready = Barrier(len(records) + 1)
+ start = threading.Event()
+
+ def worker(calls):
+ # Create each thread's bytecode copy before enabling monitoring.
+ func(1)
+ ready.wait()
+ start.wait()
+ for instructions in calls:
+ local.instructions = instructions
+ func(1)
+
+ def instruction(code, offset):
+ local.instructions.append(offset)
+
+ def line(code, lineno):
+ return monitoring.DISABLE
+
+ threads = [Thread(target=worker, args=(calls,)) for calls in records]
+ try:
+ with threading_helper.start_threads(threads, unlock=start.set):
+ ready.wait()
+ monitoring.register_callback(
+ self.tool_id, monitoring.events.INSTRUCTION, instruction)
+ monitoring.register_callback(
+ self.tool_id, monitoring.events.LINE, line)
+ monitoring.set_local_events(
+ self.tool_id, code,
+ monitoring.events.INSTRUCTION | monitoring.events.LINE)
+ start.set()
+ finally:
+ monitoring.set_local_events(self.tool_id, code, 0)
+ monitoring.register_callback(
+ self.tool_id, monitoring.events.INSTRUCTION, None)
+ monitoring.register_callback(
+ self.tool_id, monitoring.events.LINE, None)
+ monitoring.restart_events()
+
+ for calls in records:
+ for instructions in calls:
+ self.assertEqual(instructions, expected)
+
def register_callback(self, barrier):
barrier.wait()
diff --git a/Lib/test/test_monitoring.py b/Lib/test/test_monitoring.py
index d1b2a4380be806d..24cc51f4b4053ab 100644
--- a/Lib/test/test_monitoring.py
+++ b/Lib/test/test_monitoring.py
@@ -1192,6 +1192,80 @@ def __call__(self, code, offset):
class TestLineAndInstructionEvents(CheckEvents):
maxDiff = None
+ def test_disable_line_keeps_instruction_events(self):
+ for local in (False, True):
+ # Use the same tool or different tools for LINE and INSTRUCTION.
+ for line_tool, instr_tool in (
+ (TEST_TOOL, TEST_TOOL),
+ (TEST_TOOL2, TEST_TOOL),
+ ):
+ with self.subTest(local=local, line_tool=line_tool,
+ instr_tool=instr_tool):
+ self.check_disable_line_keeps_instruction_events(
+ local, line_tool, instr_tool)
+
+ def check_disable_line_keeps_instruction_events(self, local, line_tool,
+ instr_tool):
+ def func(x):
+ a = x + 1
+ # Split the assignment so line events also move backwards from
+ # the expression's line to the assignment's line.
+ b = (
+ a * 2
+ )
+ return b
+
+ code = func.__code__
+ instructions = []
+ lines = []
+ disable = False
+
+ def instruction(code_arg, offset):
+ if code_arg is code:
+ instructions.append(offset)
+
+ def line(code_arg, lineno):
+ if code_arg is code:
+ lines.append(lineno)
+ if disable:
+ return sys.monitoring.DISABLE
+
+ sys.monitoring.register_callback(instr_tool, E.INSTRUCTION,
instruction)
+ sys.monitoring.register_callback(line_tool, E.LINE, line)
+ events = {instr_tool: E.INSTRUCTION}
+ events[line_tool] = events.get(line_tool, 0) | E.LINE
+ try:
+ for tool, mask in events.items():
+ if local:
+ sys.monitoring.set_local_events(tool, code, mask)
+ else:
+ sys.monitoring.set_events(tool, mask)
+ func(1)
+ expected_instructions = instructions[:]
+ expected_lines = lines[:]
+ self.assertEqual(expected_instructions, [
+ inst.offset for inst in dis.get_instructions(func)
+ if inst.opname != "RESUME"
+ ])
+ self.assertTrue(expected_lines)
+ disable = True
+ for restart in (False, True):
+ if restart:
+ sys.monitoring.restart_events()
+ for call in range(2):
+ instructions.clear()
+ lines.clear()
+ func(1)
+ self.assertEqual(instructions, expected_instructions)
+ self.assertEqual(lines, expected_lines if call == 0 else
[])
+ finally:
+ for tool in events:
+ sys.monitoring.set_local_events(tool, code, 0)
+ sys.monitoring.set_events(tool, 0)
+ sys.monitoring.register_callback(instr_tool, E.INSTRUCTION, None)
+ sys.monitoring.register_callback(line_tool, E.LINE, None)
+ sys.monitoring.restart_events()
+
def test_simple(self):
def func1():
diff --git
a/Misc/NEWS.d/next/Core_and_Builtins/2026-09-29-14-35-00.gh-issue-158121.Kj4nQ8.rst
b/Misc/NEWS.d/next/Core_and_Builtins/2026-09-29-14-35-00.gh-issue-158121.Kj4nQ8.rst
new file mode 100644
index 000000000000000..3256ce99a84952b
--- /dev/null
+++
b/Misc/NEWS.d/next/Core_and_Builtins/2026-09-29-14-35-00.gh-issue-158121.Kj4nQ8.rst
@@ -0,0 +1,2 @@
+Fix missing :mod:`sys.monitoring` ``INSTRUCTION`` events when a ``LINE``
+callback returns :data:`sys.monitoring.DISABLE`.
diff --git a/Python/instrumentation.c b/Python/instrumentation.c
index 25663ce5430d2ee..3c9b2afcde100f3 100644
--- a/Python/instrumentation.c
+++ b/Python/instrumentation.c
@@ -715,9 +715,6 @@ de_instrument_line(PyCodeObject *code, _Py_CODEUNIT
*bytecode, _PyCoMonitoringDa
}
_PyCoLineInstrumentationData *lines = monitoring->lines;
int original_opcode = _PyCode_GetOriginalOpcode(lines, i);
- if (original_opcode == INSTRUMENTED_INSTRUCTION) {
- set_original_opcode(lines, i, monitoring->per_instruction_opcodes[i]);
- }
CHECK(original_opcode != 0);
CHECK(original_opcode == _PyOpcode_Deopt[original_opcode]);
FT_ATOMIC_STORE_UINT8(instr->op.code, original_opcode);
@@ -882,7 +879,15 @@ remove_line_tools(PyCodeObject * code, int offset, int
tools)
should_de_instrument = ((single_tool & tools) == single_tool);
}
if (should_de_instrument) {
+ /* Restore all thread-local bytecodes before updating the shared
+ * original opcode. */
MODIFY_BYTECODE(code, de_instrument_line, monitoring, offset);
+ if (_PyCode_GetOriginalOpcode(monitoring->lines, offset) ==
+ INSTRUMENTED_INSTRUCTION)
+ {
+ set_original_opcode(monitoring->lines, offset,
+ monitoring->per_instruction_opcodes[offset]);
+ }
}
}
@@ -1424,6 +1429,11 @@ _Py_call_instrumentation_line(PyThreadState *tstate,
_PyInterpreterFrame* frame,
uint8_t original_opcode;
done:
original_opcode = _PyCode_GetOriginalOpcode(line_data, i);
+ if (instr->op.code == INSTRUMENTED_INSTRUCTION) {
+ /* A LINE callback may have disabled the last LINE tool while
+ * leaving INSTRUCTION monitoring enabled. */
+ original_opcode = INSTRUMENTED_INSTRUCTION;
+ }
assert(original_opcode != 0);
assert(original_opcode != INSTRUMENTED_LINE);
assert(_PyOpcode_Deopt[original_opcode] == original_opcode);
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]