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.
This commit is contained in:
Nick Coghlan 2017-09-08 10:14:16 +10:00 committed by GitHub
parent 2eb0cb4787
commit 5a8516701f
11 changed files with 126 additions and 17 deletions

View File

@ -1068,7 +1068,7 @@ always available.
Trace functions should have three arguments: *frame*, *event*, and
*arg*. *frame* is the current stack frame. *event* is a string: ``'call'``,
``'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
local scope is entered; it should return a reference to a local trace
@ -1091,6 +1091,8 @@ always available.
``None``; the return value specifies the new local trace function. See
:file:`Objects/lnotab_notes.txt` for a detailed explanation of how this
works.
Per-line events may be disabled for a frame by setting
:attr:`f_trace_lines` to :const:`False` on that frame.
``'return'``
A function (or other code block) is about to return. The local trace
@ -1113,6 +1115,14 @@ always available.
``'c_exception'``
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
``'exception'`` event is generated at each level.
@ -1125,6 +1135,11 @@ always available.
implementation platform, rather than part of the language definition, and
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)
Accepts two optional keyword arguments which are callables that accept an

View File

@ -970,10 +970,20 @@ Internal types
.. index::
single: f_trace (frame attribute)
single: f_trace_lines (frame attribute)
single: f_trace_opcodes (frame attribute)
single: f_lineno (frame attribute)
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
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)

View File

@ -348,6 +348,18 @@ Build and C API Changes
(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
==========

View File

@ -27,6 +27,8 @@ typedef struct _frame {
to the current stack top. */
PyObject **f_stacktop;
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
state inside the generator and the exception state of the calling

View File

@ -92,7 +92,11 @@ typedef struct _is {
/* Py_tracefunc return -1 when raising an exception, or 0 for success. */
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_EXCEPTION 1
#define PyTrace_LINE 2
@ -100,6 +104,7 @@ typedef int (*Py_tracefunc)(PyObject *, struct _frame *, int, PyObject *);
#define PyTrace_C_CALL 4
#define PyTrace_C_EXCEPTION 5
#define PyTrace_C_RETURN 6
#define PyTrace_OPCODE 7
#endif
#ifdef Py_LIMITED_API

View File

@ -971,7 +971,7 @@ class SizeofTest(unittest.TestCase):
nfrees = len(x.f_code.co_freevars)
extras = x.f_code.co_stacksize + x.f_code.co_nlocals +\
ncells + nfrees - 1
check(x, vsize('12P3ic' + CO_MAXBLOCKS*'3i' + 'P' + extras*'P'))
check(x, vsize('8P2c4P3ic' + CO_MAXBLOCKS*'3i' + 'P' + extras*'P'))
# function
def func(): pass
check(func, size('12P'))

View File

@ -234,16 +234,29 @@ generator_example.events = ([(0, 'call'),
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 = []
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):
self._reconfigure_frame(frame)
self.events.append((frame.f_lineno, event))
return self.trace
def traceWithGenexp(self, frame, event, arg):
self._reconfigure_frame(frame)
(o for o in [1])
self.events.append((frame.f_lineno, event))
return self.trace
class TraceTestCase(unittest.TestCase):
# Disable gc collection when tracing, otherwise the
@ -257,6 +270,11 @@ class TraceTestCase(unittest.TestCase):
if self.using_gc:
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):
events = [(l - line_offset, e) for (l, e) in events]
if events != expected_events:
@ -266,7 +284,7 @@ class TraceTestCase(unittest.TestCase):
[str(x) for x in events])))
def run_and_compare(self, func, events):
tracer = Tracer()
tracer = self.make_tracer()
sys.settrace(tracer.trace)
func()
sys.settrace(None)
@ -277,7 +295,7 @@ class TraceTestCase(unittest.TestCase):
self.run_and_compare(func, func.events)
def run_test2(self, func):
tracer = Tracer()
tracer = self.make_tracer()
func(tracer.trace)
sys.settrace(None)
self.compare_events(func.__code__.co_firstlineno,
@ -329,7 +347,7 @@ class TraceTestCase(unittest.TestCase):
# and if the traced function contains another generator
# that is not completely exhausted, the trace stopped.
# Worse: the 'finally' clause was not invoked.
tracer = Tracer()
tracer = self.make_tracer()
sys.settrace(tracer.traceWithGenexp)
generator_example()
sys.settrace(None)
@ -398,6 +416,34 @@ class TraceTestCase(unittest.TestCase):
(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):
def setUp(self):
self.addCleanup(sys.settrace, sys.gettrace())
@ -846,6 +892,8 @@ output.append(4)
def test_main():
support.run_unittest(
TraceTestCase,
SkipLineEventsTraceTestCase,
TraceOpcodesTestCase,
RaisingTraceFuncTestCase,
JumpTestCase
)

View File

@ -0,0 +1,5 @@
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.

View File

@ -15,6 +15,8 @@ static PyMemberDef frame_memberlist[] = {
{"f_builtins", T_OBJECT, OFF(f_builtins), READONLY},
{"f_globals", T_OBJECT, OFF(f_globals), 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 */
};
@ -728,6 +730,8 @@ _PyFrame_New_NoTrack(PyThreadState *tstate, PyCodeObject *code,
f->f_iblock = 0;
f->f_executing = 0;
f->f_gen = NULL;
f->f_trace_opcodes = 0;
f->f_trace_lines = 1;
return f;
}

View File

@ -4458,12 +4458,19 @@ maybe_call_line_trace(Py_tracefunc func, PyObject *obj,
*instr_lb = bounds.ap_lower;
*instr_ub = bounds.ap_upper;
}
/* 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 call the trace function. */
if (frame->f_lasti == *instr_lb || frame->f_lasti < *instr_prev) {
/* Always emit an opcode event if we're tracing all opcodes. */
if (frame->f_trace_opcodes) {
result = call_trace(func, obj, tstate, frame, PyTrace_OPCODE, Py_None);
}
/* 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;
result = call_trace(func, obj, tstate, frame, PyTrace_LINE, Py_None);
if (frame->f_trace_lines) {
result = call_trace(func, obj, tstate, frame, PyTrace_LINE, Py_None);
}
}
*instr_prev = frame->f_lasti;
return result;

View File

@ -349,18 +349,19 @@ same value.");
* Cached interned string objects used for calling the profile and
* 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
trace_init(void)
{
static const char * const whatnames[7] = {
static const char * const whatnames[8] = {
"call", "exception", "line", "return",
"c_call", "c_exception", "c_return"
"c_call", "c_exception", "c_return",
"opcode"
};
PyObject *name;
int i;
for (i = 0; i < 7; ++i) {
for (i = 0; i < 8; ++i) {
if (whatstrings[i] == NULL) {
name = PyUnicode_InternFromString(whatnames[i]);
if (name == NULL)