Commit 5a851670 authored by Nick Coghlan's avatar Nick Coghlan Committed by GitHub

bpo-31344: Per-frame control of trace events (GH-3417)

f_trace_lines: enable/disable line trace events
f_trace_opcodes: enable/disable opcode trace events

These are intended primarily for testing of the interpreter
itself, as they make it much easier to emulate signals
arriving at unfortunate times.
parent 2eb0cb47
...@@ -1068,7 +1068,7 @@ always available. ...@@ -1068,7 +1068,7 @@ always available.
Trace functions should have three arguments: *frame*, *event*, and Trace functions should have three arguments: *frame*, *event*, and
*arg*. *frame* is the current stack frame. *event* is a string: ``'call'``, *arg*. *frame* is the current stack frame. *event* is a string: ``'call'``,
``'line'``, ``'return'``, ``'exception'``, ``'c_call'``, ``'c_return'``, or ``'line'``, ``'return'``, ``'exception'``, ``'c_call'``, ``'c_return'``, or
``'c_exception'``. *arg* depends on the event type. ``'c_exception'``, ``'opcode'``. *arg* depends on the event type.
The trace function is invoked (with *event* set to ``'call'``) whenever a new The trace function is invoked (with *event* set to ``'call'``) whenever a new
local scope is entered; it should return a reference to a local trace local scope is entered; it should return a reference to a local trace
...@@ -1091,6 +1091,8 @@ always available. ...@@ -1091,6 +1091,8 @@ always available.
``None``; the return value specifies the new local trace function. See ``None``; the return value specifies the new local trace function. See
:file:`Objects/lnotab_notes.txt` for a detailed explanation of how this :file:`Objects/lnotab_notes.txt` for a detailed explanation of how this
works. works.
Per-line events may be disabled for a frame by setting
:attr:`f_trace_lines` to :const:`False` on that frame.
``'return'`` ``'return'``
A function (or other code block) is about to return. The local trace A function (or other code block) is about to return. The local trace
...@@ -1113,6 +1115,14 @@ always available. ...@@ -1113,6 +1115,14 @@ always available.
``'c_exception'`` ``'c_exception'``
A C function has raised an exception. *arg* is the C function object. A C function has raised an exception. *arg* is the C function object.
``'opcode'``
The interpreter is about to execute a new opcode (see :mod:`dis` for
opcode details). The local trace function is called; *arg* is
``None``; the return value specifies the new local trace function.
Per-opcode events are not emitted by default: they must be explicitly
requested by setting :attr:`f_trace_opcodes` to :const:`True` on the
frame.
Note that as an exception is propagated down the chain of callers, an Note that as an exception is propagated down the chain of callers, an
``'exception'`` event is generated at each level. ``'exception'`` event is generated at each level.
...@@ -1125,6 +1135,11 @@ always available. ...@@ -1125,6 +1135,11 @@ always available.
implementation platform, rather than part of the language definition, and implementation platform, rather than part of the language definition, and
thus may not be available in all Python implementations. thus may not be available in all Python implementations.
.. versionchanged:: 3.7
``'opcode'`` event type added; :attr:`f_trace_lines` and
:attr:`f_trace_opcodes` attributes added to frames
.. function:: set_asyncgen_hooks(firstiter, finalizer) .. function:: set_asyncgen_hooks(firstiter, finalizer)
Accepts two optional keyword arguments which are callables that accept an Accepts two optional keyword arguments which are callables that accept an
......
...@@ -970,10 +970,20 @@ Internal types ...@@ -970,10 +970,20 @@ Internal types
.. index:: .. index::
single: f_trace (frame attribute) single: f_trace (frame attribute)
single: f_trace_lines (frame attribute)
single: f_trace_opcodes (frame attribute)
single: f_lineno (frame attribute) single: f_lineno (frame attribute)
Special writable attributes: :attr:`f_trace`, if not ``None``, is a function Special writable attributes: :attr:`f_trace`, if not ``None``, is a function
called at the start of each source code line (this is used by the debugger); called for various events during code execution (this is used by the debugger).
Normally an event is triggered for each new source line - this can be
disabled by setting :attr:`f_trace_lines` to :const:`False`.
Implementations *may* allow per-opcode events to be requested by setting
:attr:`f_trace_opcodes` to :const:`True`. Note that this may lead to
undefined interpreter behaviour if exceptions raised by the trace
function escape to the function being traced.
:attr:`f_lineno` is the current line number of the frame --- writing to this :attr:`f_lineno` is the current line number of the frame --- writing to this
from within a trace function jumps to the given line (only for the bottom-most from within a trace function jumps to the given line (only for the bottom-most
frame). A debugger can implement a Jump command (aka Set Next Statement) frame). A debugger can implement a Jump command (aka Set Next Statement)
......
...@@ -348,6 +348,18 @@ Build and C API Changes ...@@ -348,6 +348,18 @@ Build and C API Changes
(Contributed by Antoine Pitrou in :issue:`31370`.). (Contributed by Antoine Pitrou in :issue:`31370`.).
Other CPython Implementation Changes
====================================
* Trace hooks may now opt out of receiving ``line`` events from the interpreter
by setting the new ``f_trace_lines`` attribute to :const:`False` on the frame
being traced. (Contributed by Nick Coghlan in :issue:`31344`.)
* Trace hooks may now opt in to receiving ``opcode`` events from the interpreter
by setting the new ``f_trace_opcodes`` attribute to :const:`True` on the frame
being traced. (Contributed by Nick Coghlan in :issue:`31344`.)
Deprecated Deprecated
========== ==========
......
...@@ -27,6 +27,8 @@ typedef struct _frame { ...@@ -27,6 +27,8 @@ typedef struct _frame {
to the current stack top. */ to the current stack top. */
PyObject **f_stacktop; PyObject **f_stacktop;
PyObject *f_trace; /* Trace function */ PyObject *f_trace; /* Trace function */
char f_trace_lines; /* Emit per-line trace events? */
char f_trace_opcodes; /* Emit per-opcode trace events? */
/* In a generator, we need to be able to swap between the exception /* In a generator, we need to be able to swap between the exception
state inside the generator and the exception state of the calling state inside the generator and the exception state of the calling
......
...@@ -92,7 +92,11 @@ typedef struct _is { ...@@ -92,7 +92,11 @@ typedef struct _is {
/* Py_tracefunc return -1 when raising an exception, or 0 for success. */ /* Py_tracefunc return -1 when raising an exception, or 0 for success. */
typedef int (*Py_tracefunc)(PyObject *, struct _frame *, int, PyObject *); typedef int (*Py_tracefunc)(PyObject *, struct _frame *, int, PyObject *);
/* The following values are used for 'what' for tracefunc functions: */ /* The following values are used for 'what' for tracefunc functions
*
* To add a new kind of trace event, also update "trace_init" in
* Python/sysmodule.c to define the Python level event name
*/
#define PyTrace_CALL 0 #define PyTrace_CALL 0
#define PyTrace_EXCEPTION 1 #define PyTrace_EXCEPTION 1
#define PyTrace_LINE 2 #define PyTrace_LINE 2
...@@ -100,6 +104,7 @@ typedef int (*Py_tracefunc)(PyObject *, struct _frame *, int, PyObject *); ...@@ -100,6 +104,7 @@ typedef int (*Py_tracefunc)(PyObject *, struct _frame *, int, PyObject *);
#define PyTrace_C_CALL 4 #define PyTrace_C_CALL 4
#define PyTrace_C_EXCEPTION 5 #define PyTrace_C_EXCEPTION 5
#define PyTrace_C_RETURN 6 #define PyTrace_C_RETURN 6
#define PyTrace_OPCODE 7
#endif #endif
#ifdef Py_LIMITED_API #ifdef Py_LIMITED_API
......
...@@ -971,7 +971,7 @@ class SizeofTest(unittest.TestCase): ...@@ -971,7 +971,7 @@ class SizeofTest(unittest.TestCase):
nfrees = len(x.f_code.co_freevars) nfrees = len(x.f_code.co_freevars)
extras = x.f_code.co_stacksize + x.f_code.co_nlocals +\ extras = x.f_code.co_stacksize + x.f_code.co_nlocals +\
ncells + nfrees - 1 ncells + nfrees - 1
check(x, vsize('12P3ic' + CO_MAXBLOCKS*'3i' + 'P' + extras*'P')) check(x, vsize('8P2c4P3ic' + CO_MAXBLOCKS*'3i' + 'P' + extras*'P'))
# function # function
def func(): pass def func(): pass
check(func, size('12P')) check(func, size('12P'))
......
...@@ -234,16 +234,29 @@ generator_example.events = ([(0, 'call'), ...@@ -234,16 +234,29 @@ generator_example.events = ([(0, 'call'),
class Tracer: class Tracer:
def __init__(self): def __init__(self, trace_line_events=None, trace_opcode_events=None):
self.trace_line_events = trace_line_events
self.trace_opcode_events = trace_opcode_events
self.events = [] self.events = []
def _reconfigure_frame(self, frame):
if self.trace_line_events is not None:
frame.f_trace_lines = self.trace_line_events
if self.trace_opcode_events is not None:
frame.f_trace_opcodes = self.trace_opcode_events
def trace(self, frame, event, arg): def trace(self, frame, event, arg):
self._reconfigure_frame(frame)
self.events.append((frame.f_lineno, event)) self.events.append((frame.f_lineno, event))
return self.trace return self.trace
def traceWithGenexp(self, frame, event, arg): def traceWithGenexp(self, frame, event, arg):
self._reconfigure_frame(frame)
(o for o in [1]) (o for o in [1])
self.events.append((frame.f_lineno, event)) self.events.append((frame.f_lineno, event))
return self.trace return self.trace
class TraceTestCase(unittest.TestCase): class TraceTestCase(unittest.TestCase):
# Disable gc collection when tracing, otherwise the # Disable gc collection when tracing, otherwise the
...@@ -257,6 +270,11 @@ class TraceTestCase(unittest.TestCase): ...@@ -257,6 +270,11 @@ class TraceTestCase(unittest.TestCase):
if self.using_gc: if self.using_gc:
gc.enable() gc.enable()
@staticmethod
def make_tracer():
"""Helper to allow test subclasses to configure tracers differently"""
return Tracer()
def compare_events(self, line_offset, events, expected_events): def compare_events(self, line_offset, events, expected_events):
events = [(l - line_offset, e) for (l, e) in events] events = [(l - line_offset, e) for (l, e) in events]
if events != expected_events: if events != expected_events:
...@@ -266,7 +284,7 @@ class TraceTestCase(unittest.TestCase): ...@@ -266,7 +284,7 @@ class TraceTestCase(unittest.TestCase):
[str(x) for x in events]))) [str(x) for x in events])))
def run_and_compare(self, func, events): def run_and_compare(self, func, events):
tracer = Tracer() tracer = self.make_tracer()
sys.settrace(tracer.trace) sys.settrace(tracer.trace)
func() func()
sys.settrace(None) sys.settrace(None)
...@@ -277,7 +295,7 @@ class TraceTestCase(unittest.TestCase): ...@@ -277,7 +295,7 @@ class TraceTestCase(unittest.TestCase):
self.run_and_compare(func, func.events) self.run_and_compare(func, func.events)
def run_test2(self, func): def run_test2(self, func):
tracer = Tracer() tracer = self.make_tracer()
func(tracer.trace) func(tracer.trace)
sys.settrace(None) sys.settrace(None)
self.compare_events(func.__code__.co_firstlineno, self.compare_events(func.__code__.co_firstlineno,
...@@ -329,7 +347,7 @@ class TraceTestCase(unittest.TestCase): ...@@ -329,7 +347,7 @@ class TraceTestCase(unittest.TestCase):
# and if the traced function contains another generator # and if the traced function contains another generator
# that is not completely exhausted, the trace stopped. # that is not completely exhausted, the trace stopped.
# Worse: the 'finally' clause was not invoked. # Worse: the 'finally' clause was not invoked.
tracer = Tracer() tracer = self.make_tracer()
sys.settrace(tracer.traceWithGenexp) sys.settrace(tracer.traceWithGenexp)
generator_example() generator_example()
sys.settrace(None) sys.settrace(None)
...@@ -398,6 +416,34 @@ class TraceTestCase(unittest.TestCase): ...@@ -398,6 +416,34 @@ class TraceTestCase(unittest.TestCase):
(1, 'line')]) (1, 'line')])
class SkipLineEventsTraceTestCase(TraceTestCase):
"""Repeat the trace tests, but with per-line events skipped"""
def compare_events(self, line_offset, events, expected_events):
skip_line_events = [e for e in expected_events if e[1] != 'line']
super().compare_events(line_offset, events, skip_line_events)
@staticmethod
def make_tracer():
return Tracer(trace_line_events=False)
@support.cpython_only
class TraceOpcodesTestCase(TraceTestCase):
"""Repeat the trace tests, but with per-opcodes events enabled"""
def compare_events(self, line_offset, events, expected_events):
skip_opcode_events = [e for e in events if e[1] != 'opcode']
if len(events) > 1:
self.assertLess(len(skip_opcode_events), len(events),
msg="No 'opcode' events received by the tracer")
super().compare_events(line_offset, skip_opcode_events, expected_events)
@staticmethod
def make_tracer():
return Tracer(trace_opcode_events=True)
class RaisingTraceFuncTestCase(unittest.TestCase): class RaisingTraceFuncTestCase(unittest.TestCase):
def setUp(self): def setUp(self):
self.addCleanup(sys.settrace, sys.gettrace()) self.addCleanup(sys.settrace, sys.gettrace())
...@@ -846,6 +892,8 @@ output.append(4) ...@@ -846,6 +892,8 @@ output.append(4)
def test_main(): def test_main():
support.run_unittest( support.run_unittest(
TraceTestCase, TraceTestCase,
SkipLineEventsTraceTestCase,
TraceOpcodesTestCase,
RaisingTraceFuncTestCase, RaisingTraceFuncTestCase,
JumpTestCase JumpTestCase
) )
......
For finer control of tracing behaviour when testing the interpreter, two new
frame attributes have been added to control the emission of particular trace
events: ``f_trace_lines`` (``True`` by default) to turn off per-line trace
events; and ``f_trace_opcodes`` (``False`` by default) to turn on per-opcode
trace events.
...@@ -15,6 +15,8 @@ static PyMemberDef frame_memberlist[] = { ...@@ -15,6 +15,8 @@ static PyMemberDef frame_memberlist[] = {
{"f_builtins", T_OBJECT, OFF(f_builtins), READONLY}, {"f_builtins", T_OBJECT, OFF(f_builtins), READONLY},
{"f_globals", T_OBJECT, OFF(f_globals), READONLY}, {"f_globals", T_OBJECT, OFF(f_globals), READONLY},
{"f_lasti", T_INT, OFF(f_lasti), READONLY}, {"f_lasti", T_INT, OFF(f_lasti), READONLY},
{"f_trace_lines", T_BOOL, OFF(f_trace_lines), 0},
{"f_trace_opcodes", T_BOOL, OFF(f_trace_opcodes), 0},
{NULL} /* Sentinel */ {NULL} /* Sentinel */
}; };
...@@ -728,6 +730,8 @@ _PyFrame_New_NoTrack(PyThreadState *tstate, PyCodeObject *code, ...@@ -728,6 +730,8 @@ _PyFrame_New_NoTrack(PyThreadState *tstate, PyCodeObject *code,
f->f_iblock = 0; f->f_iblock = 0;
f->f_executing = 0; f->f_executing = 0;
f->f_gen = NULL; f->f_gen = NULL;
f->f_trace_opcodes = 0;
f->f_trace_lines = 1;
return f; return f;
} }
......
...@@ -4458,13 +4458,20 @@ maybe_call_line_trace(Py_tracefunc func, PyObject *obj, ...@@ -4458,13 +4458,20 @@ maybe_call_line_trace(Py_tracefunc func, PyObject *obj,
*instr_lb = bounds.ap_lower; *instr_lb = bounds.ap_lower;
*instr_ub = bounds.ap_upper; *instr_ub = bounds.ap_upper;
} }
/* If the last instruction falls at the start of a line or if /* Always emit an opcode event if we're tracing all opcodes. */
it represents a jump backwards, update the frame's line if (frame->f_trace_opcodes) {
number and call the trace function. */ result = call_trace(func, obj, tstate, frame, PyTrace_OPCODE, Py_None);
if (frame->f_lasti == *instr_lb || frame->f_lasti < *instr_prev) { }
/* If the last instruction falls at the start of a line or if it
represents a jump backwards, update the frame's line number and
then call the trace function if we're tracing source lines.
*/
if ((frame->f_lasti == *instr_lb || frame->f_lasti < *instr_prev)) {
frame->f_lineno = line; frame->f_lineno = line;
if (frame->f_trace_lines) {
result = call_trace(func, obj, tstate, frame, PyTrace_LINE, Py_None); result = call_trace(func, obj, tstate, frame, PyTrace_LINE, Py_None);
} }
}
*instr_prev = frame->f_lasti; *instr_prev = frame->f_lasti;
return result; return result;
} }
......
...@@ -349,18 +349,19 @@ same value."); ...@@ -349,18 +349,19 @@ same value.");
* Cached interned string objects used for calling the profile and * Cached interned string objects used for calling the profile and
* trace functions. Initialized by trace_init(). * trace functions. Initialized by trace_init().
*/ */
static PyObject *whatstrings[7] = {NULL, NULL, NULL, NULL, NULL, NULL, NULL}; static PyObject *whatstrings[8] = {NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL};
static int static int
trace_init(void) trace_init(void)
{ {
static const char * const whatnames[7] = { static const char * const whatnames[8] = {
"call", "exception", "line", "return", "call", "exception", "line", "return",
"c_call", "c_exception", "c_return" "c_call", "c_exception", "c_return",
"opcode"
}; };
PyObject *name; PyObject *name;
int i; int i;
for (i = 0; i < 7; ++i) { for (i = 0; i < 8; ++i) {
if (whatstrings[i] == NULL) { if (whatstrings[i] == NULL) {
name = PyUnicode_InternFromString(whatnames[i]); name = PyUnicode_InternFromString(whatnames[i]);
if (name == NULL) if (name == NULL)
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment