Skip to content

Commit 8254a8a

Browse files
committed
gh-158121: Preserve instruction events when line monitoring is disabled
1 parent 596d923 commit 8254a8a

4 files changed

Lines changed: 135 additions & 3 deletions

File tree

‎Lib/test/test_free_threading/test_monitoring.py‎

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Tests monitoring, sys.settrace, and sys.setprofile in a multi-threaded
22
environment to verify things are thread-safe in a free-threaded build"""
33

4+
import dis
45
import sys
56
import threading
67
import time
@@ -251,6 +252,59 @@ def append(self, trace):
251252

252253
@threading_helper.requires_working_threading()
253254
class MonitoringMisc(MonitoringTestMixin, TestCase):
255+
def test_disable_line_keeps_instruction_events(self):
256+
def func(x):
257+
a = x + 1
258+
b = a * 2
259+
return b
260+
261+
code = func.__code__
262+
expected = [instr.offset for instr in dis.get_instructions(func)
263+
if instr.opname != "RESUME"]
264+
records = [[[], []] for _ in range(2)]
265+
local = threading.local()
266+
ready = Barrier(len(records) + 1)
267+
start = threading.Event()
268+
269+
def worker(calls):
270+
# Create each thread's bytecode copy before enabling monitoring.
271+
func(1)
272+
ready.wait()
273+
start.wait()
274+
for instructions in calls:
275+
local.instructions = instructions
276+
func(1)
277+
278+
def instruction(code, offset):
279+
local.instructions.append(offset)
280+
281+
def line(code, lineno):
282+
return monitoring.DISABLE
283+
284+
threads = [Thread(target=worker, args=(calls,)) for calls in records]
285+
try:
286+
with threading_helper.start_threads(threads, unlock=start.set):
287+
ready.wait()
288+
monitoring.register_callback(
289+
self.tool_id, monitoring.events.INSTRUCTION, instruction)
290+
monitoring.register_callback(
291+
self.tool_id, monitoring.events.LINE, line)
292+
monitoring.set_local_events(
293+
self.tool_id, code,
294+
monitoring.events.INSTRUCTION | monitoring.events.LINE)
295+
start.set()
296+
finally:
297+
monitoring.set_local_events(self.tool_id, code, 0)
298+
monitoring.register_callback(
299+
self.tool_id, monitoring.events.INSTRUCTION, None)
300+
monitoring.register_callback(
301+
self.tool_id, monitoring.events.LINE, None)
302+
monitoring.restart_events()
303+
304+
for calls in records:
305+
for instructions in calls:
306+
self.assertEqual(instructions, expected)
307+
254308
def register_callback(self, barrier):
255309
barrier.wait()
256310

‎Lib/test/test_monitoring.py‎

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1192,6 +1192,72 @@ def __call__(self, code, offset):
11921192
class TestLineAndInstructionEvents(CheckEvents):
11931193
maxDiff = None
11941194

1195+
def test_disable_line_keeps_instruction_events(self):
1196+
for local in (False, True):
1197+
for line_tool in (TEST_TOOL, TEST_TOOL2):
1198+
with self.subTest(local=local, line_tool=line_tool):
1199+
self.check_disable_line_keeps_instruction_events(
1200+
local, line_tool)
1201+
1202+
def check_disable_line_keeps_instruction_events(self, local, line_tool):
1203+
def func(x):
1204+
a = x + 1
1205+
b = (
1206+
a * 2
1207+
)
1208+
return b
1209+
1210+
code = func.__code__
1211+
instructions = []
1212+
lines = []
1213+
disable = False
1214+
1215+
def instruction(code_arg, offset):
1216+
if code_arg is code:
1217+
instructions.append(offset)
1218+
1219+
def line(code_arg, lineno):
1220+
if code_arg is code:
1221+
lines.append(lineno)
1222+
if disable:
1223+
return sys.monitoring.DISABLE
1224+
1225+
sys.monitoring.register_callback(TEST_TOOL, E.INSTRUCTION, instruction)
1226+
sys.monitoring.register_callback(line_tool, E.LINE, line)
1227+
events = {TEST_TOOL: E.INSTRUCTION}
1228+
events[line_tool] = events.get(line_tool, 0) | E.LINE
1229+
try:
1230+
for tool, mask in events.items():
1231+
if local:
1232+
sys.monitoring.set_local_events(tool, code, mask)
1233+
else:
1234+
sys.monitoring.set_events(tool, mask)
1235+
func(1)
1236+
expected_instructions = instructions[:]
1237+
expected_lines = lines[:]
1238+
self.assertEqual(expected_instructions, [
1239+
inst.offset for inst in dis.get_instructions(func)
1240+
if inst.opname != "RESUME"
1241+
])
1242+
self.assertTrue(expected_lines)
1243+
disable = True
1244+
for restart in (False, True):
1245+
if restart:
1246+
sys.monitoring.restart_events()
1247+
for call in range(2):
1248+
instructions.clear()
1249+
lines.clear()
1250+
func(1)
1251+
self.assertEqual(instructions, expected_instructions)
1252+
self.assertEqual(lines, expected_lines if call == 0 else [])
1253+
finally:
1254+
for tool in events:
1255+
sys.monitoring.set_local_events(tool, code, 0)
1256+
sys.monitoring.set_events(tool, 0)
1257+
sys.monitoring.register_callback(TEST_TOOL, E.INSTRUCTION, None)
1258+
sys.monitoring.register_callback(line_tool, E.LINE, None)
1259+
sys.monitoring.restart_events()
1260+
11951261
def test_simple(self):
11961262

11971263
def func1():
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix missing :mod:`sys.monitoring` ``INSTRUCTION`` events when a ``LINE``
2+
callback returns :data:`sys.monitoring.DISABLE`.

‎Python/instrumentation.c‎

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -715,9 +715,6 @@ de_instrument_line(PyCodeObject *code, _Py_CODEUNIT *bytecode, _PyCoMonitoringDa
715715
}
716716
_PyCoLineInstrumentationData *lines = monitoring->lines;
717717
int original_opcode = _PyCode_GetOriginalOpcode(lines, i);
718-
if (original_opcode == INSTRUMENTED_INSTRUCTION) {
719-
set_original_opcode(lines, i, monitoring->per_instruction_opcodes[i]);
720-
}
721718
CHECK(original_opcode != 0);
722719
CHECK(original_opcode == _PyOpcode_Deopt[original_opcode]);
723720
FT_ATOMIC_STORE_UINT8(instr->op.code, original_opcode);
@@ -883,6 +880,14 @@ remove_line_tools(PyCodeObject * code, int offset, int tools)
883880
}
884881
if (should_de_instrument) {
885882
MODIFY_BYTECODE(code, de_instrument_line, monitoring, offset);
883+
/* Restore all thread-local bytecodes before updating the shared
884+
* original opcode. */
885+
if (_PyCode_GetOriginalOpcode(monitoring->lines, offset) ==
886+
INSTRUMENTED_INSTRUCTION)
887+
{
888+
set_original_opcode(monitoring->lines, offset,
889+
monitoring->per_instruction_opcodes[offset]);
890+
}
886891
}
887892
}
888893

@@ -1424,6 +1429,11 @@ _Py_call_instrumentation_line(PyThreadState *tstate, _PyInterpreterFrame* frame,
14241429
uint8_t original_opcode;
14251430
done:
14261431
original_opcode = _PyCode_GetOriginalOpcode(line_data, i);
1432+
if (instr->op.code == INSTRUMENTED_INSTRUCTION) {
1433+
/* A LINE callback may have disabled the last LINE tool while
1434+
* leaving INSTRUCTION monitoring enabled. */
1435+
original_opcode = INSTRUMENTED_INSTRUCTION;
1436+
}
14271437
assert(original_opcode != 0);
14281438
assert(original_opcode != INSTRUMENTED_LINE);
14291439
assert(_PyOpcode_Deopt[original_opcode] == original_opcode);

0 commit comments

Comments
 (0)