Skip to content

Commit

Permalink
GH-103082: Filter LINE events in VM, to simplify tool implementation. (
Browse files Browse the repository at this point in the history
…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.
  • Loading branch information
markshannon authored May 12, 2023
1 parent 19ee53d commit 45f5aa8
Show file tree
Hide file tree
Showing 16 changed files with 252 additions and 158 deletions.
1 change: 0 additions & 1 deletion Include/internal/pycore_frame.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ struct _frame {
struct _PyInterpreterFrame *f_frame; /* points to the frame data */
PyObject *f_trace; /* Trace function */
int f_lineno; /* Current line number. Only valid if non-zero */
int f_last_traced_line; /* The last line traced for this frame */
char f_trace_lines; /* Emit per-line trace events? */
char f_trace_opcodes; /* Emit per-opcode trace events? */
char f_fast_as_locals; /* Have the fast locals of this frame been converted to a dict? */
Expand Down
5 changes: 3 additions & 2 deletions Include/internal/pycore_instruments.h
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,13 @@ _Py_call_instrumentation(PyThreadState *tstate, int event,

extern int
_Py_call_instrumentation_line(PyThreadState *tstate, _PyInterpreterFrame* frame,
_Py_CODEUNIT *instr);
_Py_CODEUNIT *instr, _Py_CODEUNIT *prev);

extern int
_Py_call_instrumentation_instruction(
PyThreadState *tstate, _PyInterpreterFrame* frame, _Py_CODEUNIT *instr);

int
_Py_CODEUNIT *
_Py_call_instrumentation_jump(
PyThreadState *tstate, int event,
_PyInterpreterFrame *frame, _Py_CODEUNIT *instr, _Py_CODEUNIT *target);
Expand All @@ -100,6 +100,7 @@ extern int
_Py_Instrumentation_GetLine(PyCodeObject *code, int index);

extern PyObject _PyInstrumentation_MISSING;
extern PyObject _PyInstrumentation_DISABLE;

#ifdef __cplusplus
}
Expand Down
63 changes: 61 additions & 2 deletions Lib/test/test_monitoring.py
Original file line number Diff line number Diff line change
Expand Up @@ -524,7 +524,7 @@ def test_lines_loop(self):
sys.monitoring.set_events(TEST_TOOL, 0)
sys.monitoring.register_callback(TEST_TOOL, E.LINE, None)
start = LineMonitoringTest.test_lines_loop.__code__.co_firstlineno
self.assertEqual(events, [start+7, 21, 22, 22, 21, start+8])
self.assertEqual(events, [start+7, 21, 22, 21, 22, 21, start+8])
finally:
sys.monitoring.set_events(TEST_TOOL, 0)
sys.monitoring.register_callback(TEST_TOOL, E.LINE, None)
Expand Down Expand Up @@ -1050,6 +1050,8 @@ def func3():
def line_from_offset(code, offset):
for start, end, line in code.co_lines():
if start <= offset < end:
if line is None:
return f"[offset={offset}]"
return line - code.co_firstlineno
return -1

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

class ReturnRecorder:

event_type = E.PY_RETURN

def __init__(self, events):
self.events = events

def __call__(self, code, offset, val):
self.events.append(("return", val))


JUMP_AND_BRANCH_RECORDERS = JumpRecorder, BranchRecorder
JUMP_BRANCH_AND_LINE_RECORDERS = JumpRecorder, BranchRecorder, LineRecorder
FLOW_AND_LINE_RECORDERS = JumpRecorder, BranchRecorder, LineRecorder, ExceptionRecorder, ReturnRecorder

class TestBranchAndJumpEvents(CheckEvents):
maxDiff = None
Expand All @@ -1098,7 +1111,6 @@ def func():
('jump', 'func', 4, 2),
('branch', 'func', 2, 2)])


self.check_events(func, recorders = JUMP_BRANCH_AND_LINE_RECORDERS, expected = [
('line', 'check_events', 10),
('line', 'func', 1),
Expand All @@ -1108,15 +1120,62 @@ def func():
('branch', 'func', 3, 6),
('line', 'func', 6),
('jump', 'func', 6, 2),
('line', 'func', 2),
('branch', 'func', 2, 2),
('line', 'func', 3),
('branch', 'func', 3, 4),
('line', 'func', 4),
('jump', 'func', 4, 2),
('line', 'func', 2),
('branch', 'func', 2, 2),
('line', 'check_events', 11)])

def test_except_star(self):

class Foo:
def meth(self):
pass

def func():
try:
try:
raise KeyError
except* Exception as e:
f = Foo(); f.meth()
except KeyError:
pass


self.check_events(func, recorders = JUMP_BRANCH_AND_LINE_RECORDERS, expected = [
('line', 'check_events', 10),
('line', 'func', 1),
('line', 'func', 2),
('line', 'func', 3),
('line', 'func', 4),
('branch', 'func', 4, 4),
('line', 'func', 5),
('line', 'meth', 1),
('jump', 'func', 5, 5),
('jump', 'func', 5, '[offset=114]'),
('branch', 'func', '[offset=120]', '[offset=122]'),
('line', 'check_events', 11)])

self.check_events(func, recorders = FLOW_AND_LINE_RECORDERS, expected = [
('line', 'check_events', 10),
('line', 'func', 1),
('line', 'func', 2),
('line', 'func', 3),
('raise', KeyError),
('line', 'func', 4),
('branch', 'func', 4, 4),
('line', 'func', 5),
('line', 'meth', 1),
('return', None),
('jump', 'func', 5, 5),
('jump', 'func', 5, '[offset=114]'),
('branch', 'func', '[offset=120]', '[offset=122]'),
('return', None),
('line', 'check_events', 11)])

class TestSetGetEvents(MonitoringTestBase, unittest.TestCase):

Expand Down
5 changes: 3 additions & 2 deletions Lib/test/test_pdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -1793,8 +1793,9 @@ def test_pdb_issue_gh_101517():
... 'continue'
... ]):
... test_function()
> <doctest test.test_pdb.test_pdb_issue_gh_101517[0]>(5)test_function()
-> import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace()
--Return--
> <doctest test.test_pdb.test_pdb_issue_gh_101517[0]>(None)test_function()->None
-> Warning: lineno is None
(Pdb) continue
"""

Expand Down
2 changes: 1 addition & 1 deletion Lib/test/test_sys.py
Original file line number Diff line number Diff line change
Expand Up @@ -1446,7 +1446,7 @@ class C(object): pass
def func():
return sys._getframe()
x = func()
check(x, size('3Pii3c7P2ic??2P'))
check(x, size('3Pi3c7P2ic??2P'))
# function
def func(): pass
check(func, size('14Pi'))
Expand Down
1 change: 0 additions & 1 deletion Lib/test/test_sys_settrace.py
Original file line number Diff line number Diff line change
Expand Up @@ -2867,6 +2867,5 @@ def func(arg = 1):
sys.settrace(None)



if __name__ == "__main__":
unittest.main()
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Change behavior of ``sys.monitoring.events.LINE`` events in
``sys.monitoring``: Line events now occur when a new line is reached
dynamically, instead of using a static approximation, as before. This makes
the behavior very similar to that of "line" events in ``sys.settrace``. This
should ease porting of tools from 3.11 to 3.12.
3 changes: 0 additions & 3 deletions Objects/frameobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -831,7 +831,6 @@ frame_setlineno(PyFrameObject *f, PyObject* p_new_lineno, void *Py_UNUSED(ignore
start_stack = pop_value(start_stack);
}
/* Finally set the new lasti and return OK. */
f->f_last_traced_line = new_lineno;
f->f_lineno = 0;
f->f_frame->prev_instr = _PyCode_CODE(f->f_frame->f_code) + best_addr;
return 0;
Expand All @@ -854,7 +853,6 @@ frame_settrace(PyFrameObject *f, PyObject* v, void *closure)
}
if (v != f->f_trace) {
Py_XSETREF(f->f_trace, Py_XNewRef(v));
f->f_last_traced_line = -1;
}
return 0;
}
Expand Down Expand Up @@ -1056,7 +1054,6 @@ _PyFrame_New_NoTrack(PyCodeObject *code)
f->f_trace_opcodes = 0;
f->f_fast_as_locals = 0;
f->f_lineno = 0;
f->f_last_traced_line = -1;
return f;
}

Expand Down
22 changes: 0 additions & 22 deletions Python/bytecodes.c
Original file line number Diff line number Diff line change
Expand Up @@ -3288,28 +3288,6 @@ dummy_func(
assert(oparg >= 2);
}

inst(INSTRUMENTED_LINE, ( -- )) {
_Py_CODEUNIT *here = next_instr-1;
_PyFrame_SetStackPointer(frame, stack_pointer);
int original_opcode = _Py_call_instrumentation_line(
tstate, frame, here);
stack_pointer = _PyFrame_GetStackPointer(frame);
if (original_opcode < 0) {
next_instr = here+1;
goto error;
}
next_instr = frame->prev_instr;
if (next_instr != here) {
DISPATCH();
}
if (_PyOpcode_Caches[original_opcode]) {
_PyBinaryOpCache *cache = (_PyBinaryOpCache *)(next_instr+1);
INCREMENT_ADAPTIVE_COUNTER(cache->counter);
}
opcode = original_opcode;
DISPATCH_GOTO();
}

inst(INSTRUMENTED_INSTRUCTION, ( -- )) {
int next_opcode = _Py_call_instrumentation_instruction(
tstate, frame, next_instr-1);
Expand Down
35 changes: 35 additions & 0 deletions Python/ceval.c
Original file line number Diff line number Diff line change
Expand Up @@ -775,6 +775,41 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, _PyInterpreterFrame *frame, int

#include "generated_cases.c.h"

/* INSTRUMENTED_LINE has to be here, rather than in bytecodes.c,
* because it needs to capture frame->prev_instr before it is updated,
* as happens in the standard instruction prologue.
*/
#if USE_COMPUTED_GOTOS
TARGET_INSTRUMENTED_LINE:
#else
case INSTRUMENTED_LINE:
#endif
{
_Py_CODEUNIT *prev = frame->prev_instr;
_Py_CODEUNIT *here = frame->prev_instr = next_instr;
_PyFrame_SetStackPointer(frame, stack_pointer);
int original_opcode = _Py_call_instrumentation_line(
tstate, frame, here, prev);
stack_pointer = _PyFrame_GetStackPointer(frame);
if (original_opcode < 0) {
next_instr = here+1;
goto error;
}
next_instr = frame->prev_instr;
if (next_instr != here) {
DISPATCH();
}
if (_PyOpcode_Caches[original_opcode]) {
_PyBinaryOpCache *cache = (_PyBinaryOpCache *)(next_instr+1);
/* Prevent the underlying instruction from specializing
* and overwriting the instrumentation. */
INCREMENT_ADAPTIVE_COUNTER(cache->counter);
}
opcode = original_opcode;
DISPATCH_GOTO();
}


#if USE_COMPUTED_GOTOS
_unknown_opcode:
#else
Expand Down
5 changes: 2 additions & 3 deletions Python/ceval_macros.h
Original file line number Diff line number Diff line change
Expand Up @@ -334,11 +334,10 @@ do { \
#define INSTRUMENTED_JUMP(src, dest, event) \
do { \
_PyFrame_SetStackPointer(frame, stack_pointer); \
int err = _Py_call_instrumentation_jump(tstate, event, frame, src, dest); \
next_instr = _Py_call_instrumentation_jump(tstate, event, frame, src, dest); \
stack_pointer = _PyFrame_GetStackPointer(frame); \
if (err) { \
if (next_instr == NULL) { \
next_instr = (dest)+1; \
goto error; \
} \
next_instr = frame->prev_instr; \
} while (0);
Loading

0 comments on commit 45f5aa8

Please sign in to comment.