Skip to content

Commit 45f5aa8

Browse files
authored
GH-103082: Filter LINE events in VM, to simplify tool implementation. (GH-104387)
When monitoring LINE events, instrument all instructions that can have a predecessor on a different line. Then check that the a new line has been hit in the instrumentation code. This brings the behavior closer to that of 3.11, simplifying implementation and porting of tools.
1 parent 19ee53d commit 45f5aa8

16 files changed

+252
-158
lines changed

Include/internal/pycore_frame.h

-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ struct _frame {
1919
struct _PyInterpreterFrame *f_frame; /* points to the frame data */
2020
PyObject *f_trace; /* Trace function */
2121
int f_lineno; /* Current line number. Only valid if non-zero */
22-
int f_last_traced_line; /* The last line traced for this frame */
2322
char f_trace_lines; /* Emit per-line trace events? */
2423
char f_trace_opcodes; /* Emit per-opcode trace events? */
2524
char f_fast_as_locals; /* Have the fast locals of this frame been converted to a dict? */

Include/internal/pycore_instruments.h

+3-2
Original file line numberDiff line numberDiff line change
@@ -69,13 +69,13 @@ _Py_call_instrumentation(PyThreadState *tstate, int event,
6969

7070
extern int
7171
_Py_call_instrumentation_line(PyThreadState *tstate, _PyInterpreterFrame* frame,
72-
_Py_CODEUNIT *instr);
72+
_Py_CODEUNIT *instr, _Py_CODEUNIT *prev);
7373

7474
extern int
7575
_Py_call_instrumentation_instruction(
7676
PyThreadState *tstate, _PyInterpreterFrame* frame, _Py_CODEUNIT *instr);
7777

78-
int
78+
_Py_CODEUNIT *
7979
_Py_call_instrumentation_jump(
8080
PyThreadState *tstate, int event,
8181
_PyInterpreterFrame *frame, _Py_CODEUNIT *instr, _Py_CODEUNIT *target);
@@ -100,6 +100,7 @@ extern int
100100
_Py_Instrumentation_GetLine(PyCodeObject *code, int index);
101101

102102
extern PyObject _PyInstrumentation_MISSING;
103+
extern PyObject _PyInstrumentation_DISABLE;
103104

104105
#ifdef __cplusplus
105106
}

Lib/test/test_monitoring.py

+61-2
Original file line numberDiff line numberDiff line change
@@ -524,7 +524,7 @@ def test_lines_loop(self):
524524
sys.monitoring.set_events(TEST_TOOL, 0)
525525
sys.monitoring.register_callback(TEST_TOOL, E.LINE, None)
526526
start = LineMonitoringTest.test_lines_loop.__code__.co_firstlineno
527-
self.assertEqual(events, [start+7, 21, 22, 22, 21, start+8])
527+
self.assertEqual(events, [start+7, 21, 22, 21, 22, 21, start+8])
528528
finally:
529529
sys.monitoring.set_events(TEST_TOOL, 0)
530530
sys.monitoring.register_callback(TEST_TOOL, E.LINE, None)
@@ -1050,6 +1050,8 @@ def func3():
10501050
def line_from_offset(code, offset):
10511051
for start, end, line in code.co_lines():
10521052
if start <= offset < end:
1053+
if line is None:
1054+
return f"[offset={offset}]"
10531055
return line - code.co_firstlineno
10541056
return -1
10551057

@@ -1072,9 +1074,20 @@ class BranchRecorder(JumpRecorder):
10721074
event_type = E.BRANCH
10731075
name = "branch"
10741076

1077+
class ReturnRecorder:
1078+
1079+
event_type = E.PY_RETURN
1080+
1081+
def __init__(self, events):
1082+
self.events = events
1083+
1084+
def __call__(self, code, offset, val):
1085+
self.events.append(("return", val))
1086+
10751087

10761088
JUMP_AND_BRANCH_RECORDERS = JumpRecorder, BranchRecorder
10771089
JUMP_BRANCH_AND_LINE_RECORDERS = JumpRecorder, BranchRecorder, LineRecorder
1090+
FLOW_AND_LINE_RECORDERS = JumpRecorder, BranchRecorder, LineRecorder, ExceptionRecorder, ReturnRecorder
10781091

10791092
class TestBranchAndJumpEvents(CheckEvents):
10801093
maxDiff = None
@@ -1098,7 +1111,6 @@ def func():
10981111
('jump', 'func', 4, 2),
10991112
('branch', 'func', 2, 2)])
11001113

1101-
11021114
self.check_events(func, recorders = JUMP_BRANCH_AND_LINE_RECORDERS, expected = [
11031115
('line', 'check_events', 10),
11041116
('line', 'func', 1),
@@ -1108,15 +1120,62 @@ def func():
11081120
('branch', 'func', 3, 6),
11091121
('line', 'func', 6),
11101122
('jump', 'func', 6, 2),
1123+
('line', 'func', 2),
11111124
('branch', 'func', 2, 2),
11121125
('line', 'func', 3),
11131126
('branch', 'func', 3, 4),
11141127
('line', 'func', 4),
11151128
('jump', 'func', 4, 2),
1129+
('line', 'func', 2),
11161130
('branch', 'func', 2, 2),
1131+
('line', 'check_events', 11)])
1132+
1133+
def test_except_star(self):
1134+
1135+
class Foo:
1136+
def meth(self):
1137+
pass
1138+
1139+
def func():
1140+
try:
1141+
try:
1142+
raise KeyError
1143+
except* Exception as e:
1144+
f = Foo(); f.meth()
1145+
except KeyError:
1146+
pass
1147+
1148+
1149+
self.check_events(func, recorders = JUMP_BRANCH_AND_LINE_RECORDERS, expected = [
1150+
('line', 'check_events', 10),
1151+
('line', 'func', 1),
11171152
('line', 'func', 2),
1153+
('line', 'func', 3),
1154+
('line', 'func', 4),
1155+
('branch', 'func', 4, 4),
1156+
('line', 'func', 5),
1157+
('line', 'meth', 1),
1158+
('jump', 'func', 5, 5),
1159+
('jump', 'func', 5, '[offset=114]'),
1160+
('branch', 'func', '[offset=120]', '[offset=122]'),
11181161
('line', 'check_events', 11)])
11191162

1163+
self.check_events(func, recorders = FLOW_AND_LINE_RECORDERS, expected = [
1164+
('line', 'check_events', 10),
1165+
('line', 'func', 1),
1166+
('line', 'func', 2),
1167+
('line', 'func', 3),
1168+
('raise', KeyError),
1169+
('line', 'func', 4),
1170+
('branch', 'func', 4, 4),
1171+
('line', 'func', 5),
1172+
('line', 'meth', 1),
1173+
('return', None),
1174+
('jump', 'func', 5, 5),
1175+
('jump', 'func', 5, '[offset=114]'),
1176+
('branch', 'func', '[offset=120]', '[offset=122]'),
1177+
('return', None),
1178+
('line', 'check_events', 11)])
11201179

11211180
class TestSetGetEvents(MonitoringTestBase, unittest.TestCase):
11221181

Lib/test/test_pdb.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -1793,8 +1793,9 @@ def test_pdb_issue_gh_101517():
17931793
... 'continue'
17941794
... ]):
17951795
... test_function()
1796-
> <doctest test.test_pdb.test_pdb_issue_gh_101517[0]>(5)test_function()
1797-
-> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace()
1796+
--Return--
1797+
> <doctest test.test_pdb.test_pdb_issue_gh_101517[0]>(None)test_function()->None
1798+
-> Warning: lineno is None
17981799
(Pdb) continue
17991800
"""
18001801

Lib/test/test_sys.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1446,7 +1446,7 @@ class C(object): pass
14461446
def func():
14471447
return sys._getframe()
14481448
x = func()
1449-
check(x, size('3Pii3c7P2ic??2P'))
1449+
check(x, size('3Pi3c7P2ic??2P'))
14501450
# function
14511451
def func(): pass
14521452
check(func, size('14Pi'))

Lib/test/test_sys_settrace.py

-1
Original file line numberDiff line numberDiff line change
@@ -2867,6 +2867,5 @@ def func(arg = 1):
28672867
sys.settrace(None)
28682868

28692869

2870-
28712870
if __name__ == "__main__":
28722871
unittest.main()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Change behavior of ``sys.monitoring.events.LINE`` events in
2+
``sys.monitoring``: Line events now occur when a new line is reached
3+
dynamically, instead of using a static approximation, as before. This makes
4+
the behavior very similar to that of "line" events in ``sys.settrace``. This
5+
should ease porting of tools from 3.11 to 3.12.

Objects/frameobject.c

-3
Original file line numberDiff line numberDiff line change
@@ -831,7 +831,6 @@ frame_setlineno(PyFrameObject *f, PyObject* p_new_lineno, void *Py_UNUSED(ignore
831831
start_stack = pop_value(start_stack);
832832
}
833833
/* Finally set the new lasti and return OK. */
834-
f->f_last_traced_line = new_lineno;
835834
f->f_lineno = 0;
836835
f->f_frame->prev_instr = _PyCode_CODE(f->f_frame->f_code) + best_addr;
837836
return 0;
@@ -854,7 +853,6 @@ frame_settrace(PyFrameObject *f, PyObject* v, void *closure)
854853
}
855854
if (v != f->f_trace) {
856855
Py_XSETREF(f->f_trace, Py_XNewRef(v));
857-
f->f_last_traced_line = -1;
858856
}
859857
return 0;
860858
}
@@ -1056,7 +1054,6 @@ _PyFrame_New_NoTrack(PyCodeObject *code)
10561054
f->f_trace_opcodes = 0;
10571055
f->f_fast_as_locals = 0;
10581056
f->f_lineno = 0;
1059-
f->f_last_traced_line = -1;
10601057
return f;
10611058
}
10621059

Python/bytecodes.c

-22
Original file line numberDiff line numberDiff line change
@@ -3288,28 +3288,6 @@ dummy_func(
32883288
assert(oparg >= 2);
32893289
}
32903290

3291-
inst(INSTRUMENTED_LINE, ( -- )) {
3292-
_Py_CODEUNIT *here = next_instr-1;
3293-
_PyFrame_SetStackPointer(frame, stack_pointer);
3294-
int original_opcode = _Py_call_instrumentation_line(
3295-
tstate, frame, here);
3296-
stack_pointer = _PyFrame_GetStackPointer(frame);
3297-
if (original_opcode < 0) {
3298-
next_instr = here+1;
3299-
goto error;
3300-
}
3301-
next_instr = frame->prev_instr;
3302-
if (next_instr != here) {
3303-
DISPATCH();
3304-
}
3305-
if (_PyOpcode_Caches[original_opcode]) {
3306-
_PyBinaryOpCache *cache = (_PyBinaryOpCache *)(next_instr+1);
3307-
INCREMENT_ADAPTIVE_COUNTER(cache->counter);
3308-
}
3309-
opcode = original_opcode;
3310-
DISPATCH_GOTO();
3311-
}
3312-
33133291
inst(INSTRUMENTED_INSTRUCTION, ( -- )) {
33143292
int next_opcode = _Py_call_instrumentation_instruction(
33153293
tstate, frame, next_instr-1);

Python/ceval.c

+35
Original file line numberDiff line numberDiff line change
@@ -775,6 +775,41 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, _PyInterpreterFrame *frame, int
775775

776776
#include "generated_cases.c.h"
777777

778+
/* INSTRUMENTED_LINE has to be here, rather than in bytecodes.c,
779+
* because it needs to capture frame->prev_instr before it is updated,
780+
* as happens in the standard instruction prologue.
781+
*/
782+
#if USE_COMPUTED_GOTOS
783+
TARGET_INSTRUMENTED_LINE:
784+
#else
785+
case INSTRUMENTED_LINE:
786+
#endif
787+
{
788+
_Py_CODEUNIT *prev = frame->prev_instr;
789+
_Py_CODEUNIT *here = frame->prev_instr = next_instr;
790+
_PyFrame_SetStackPointer(frame, stack_pointer);
791+
int original_opcode = _Py_call_instrumentation_line(
792+
tstate, frame, here, prev);
793+
stack_pointer = _PyFrame_GetStackPointer(frame);
794+
if (original_opcode < 0) {
795+
next_instr = here+1;
796+
goto error;
797+
}
798+
next_instr = frame->prev_instr;
799+
if (next_instr != here) {
800+
DISPATCH();
801+
}
802+
if (_PyOpcode_Caches[original_opcode]) {
803+
_PyBinaryOpCache *cache = (_PyBinaryOpCache *)(next_instr+1);
804+
/* Prevent the underlying instruction from specializing
805+
* and overwriting the instrumentation. */
806+
INCREMENT_ADAPTIVE_COUNTER(cache->counter);
807+
}
808+
opcode = original_opcode;
809+
DISPATCH_GOTO();
810+
}
811+
812+
778813
#if USE_COMPUTED_GOTOS
779814
_unknown_opcode:
780815
#else

Python/ceval_macros.h

+2-3
Original file line numberDiff line numberDiff line change
@@ -334,11 +334,10 @@ do { \
334334
#define INSTRUMENTED_JUMP(src, dest, event) \
335335
do { \
336336
_PyFrame_SetStackPointer(frame, stack_pointer); \
337-
int err = _Py_call_instrumentation_jump(tstate, event, frame, src, dest); \
337+
next_instr = _Py_call_instrumentation_jump(tstate, event, frame, src, dest); \
338338
stack_pointer = _PyFrame_GetStackPointer(frame); \
339-
if (err) { \
339+
if (next_instr == NULL) { \
340340
next_instr = (dest)+1; \
341341
goto error; \
342342
} \
343-
next_instr = frame->prev_instr; \
344343
} while (0);

0 commit comments

Comments
 (0)