Skip to content

Commit 5a85167

Browse files
authored
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.
1 parent 2eb0cb4 commit 5a85167

File tree

11 files changed

+126
-17
lines changed

11 files changed

+126
-17
lines changed

Doc/library/sys.rst

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1068,7 +1068,7 @@ always available.
10681068
Trace functions should have three arguments: *frame*, *event*, and
10691069
*arg*. *frame* is the current stack frame. *event* is a string: ``'call'``,
10701070
``'line'``, ``'return'``, ``'exception'``, ``'c_call'``, ``'c_return'``, or
1071-
``'c_exception'``. *arg* depends on the event type.
1071+
``'c_exception'``, ``'opcode'``. *arg* depends on the event type.
10721072

10731073
The trace function is invoked (with *event* set to ``'call'``) whenever a new
10741074
local scope is entered; it should return a reference to a local trace
@@ -1091,6 +1091,8 @@ always available.
10911091
``None``; the return value specifies the new local trace function. See
10921092
:file:`Objects/lnotab_notes.txt` for a detailed explanation of how this
10931093
works.
1094+
Per-line events may be disabled for a frame by setting
1095+
:attr:`f_trace_lines` to :const:`False` on that frame.
10941096

10951097
``'return'``
10961098
A function (or other code block) is about to return. The local trace
@@ -1113,6 +1115,14 @@ always available.
11131115
``'c_exception'``
11141116
A C function has raised an exception. *arg* is the C function object.
11151117

1118+
``'opcode'``
1119+
The interpreter is about to execute a new opcode (see :mod:`dis` for
1120+
opcode details). The local trace function is called; *arg* is
1121+
``None``; the return value specifies the new local trace function.
1122+
Per-opcode events are not emitted by default: they must be explicitly
1123+
requested by setting :attr:`f_trace_opcodes` to :const:`True` on the
1124+
frame.
1125+
11161126
Note that as an exception is propagated down the chain of callers, an
11171127
``'exception'`` event is generated at each level.
11181128

@@ -1125,6 +1135,11 @@ always available.
11251135
implementation platform, rather than part of the language definition, and
11261136
thus may not be available in all Python implementations.
11271137

1138+
.. versionchanged:: 3.7
1139+
1140+
``'opcode'`` event type added; :attr:`f_trace_lines` and
1141+
:attr:`f_trace_opcodes` attributes added to frames
1142+
11281143
.. function:: set_asyncgen_hooks(firstiter, finalizer)
11291144

11301145
Accepts two optional keyword arguments which are callables that accept an

Doc/reference/datamodel.rst

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -970,10 +970,20 @@ Internal types
970970

971971
.. index::
972972
single: f_trace (frame attribute)
973+
single: f_trace_lines (frame attribute)
974+
single: f_trace_opcodes (frame attribute)
973975
single: f_lineno (frame attribute)
974976

975977
Special writable attributes: :attr:`f_trace`, if not ``None``, is a function
976-
called at the start of each source code line (this is used by the debugger);
978+
called for various events during code execution (this is used by the debugger).
979+
Normally an event is triggered for each new source line - this can be
980+
disabled by setting :attr:`f_trace_lines` to :const:`False`.
981+
982+
Implementations *may* allow per-opcode events to be requested by setting
983+
:attr:`f_trace_opcodes` to :const:`True`. Note that this may lead to
984+
undefined interpreter behaviour if exceptions raised by the trace
985+
function escape to the function being traced.
986+
977987
:attr:`f_lineno` is the current line number of the frame --- writing to this
978988
from within a trace function jumps to the given line (only for the bottom-most
979989
frame). A debugger can implement a Jump command (aka Set Next Statement)

Doc/whatsnew/3.7.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,18 @@ Build and C API Changes
348348
(Contributed by Antoine Pitrou in :issue:`31370`.).
349349

350350

351+
Other CPython Implementation Changes
352+
====================================
353+
354+
* Trace hooks may now opt out of receiving ``line`` events from the interpreter
355+
by setting the new ``f_trace_lines`` attribute to :const:`False` on the frame
356+
being traced. (Contributed by Nick Coghlan in :issue:`31344`.)
357+
358+
* Trace hooks may now opt in to receiving ``opcode`` events from the interpreter
359+
by setting the new ``f_trace_opcodes`` attribute to :const:`True` on the frame
360+
being traced. (Contributed by Nick Coghlan in :issue:`31344`.)
361+
362+
351363
Deprecated
352364
==========
353365

Include/frameobject.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ typedef struct _frame {
2727
to the current stack top. */
2828
PyObject **f_stacktop;
2929
PyObject *f_trace; /* Trace function */
30+
char f_trace_lines; /* Emit per-line trace events? */
31+
char f_trace_opcodes; /* Emit per-opcode trace events? */
3032

3133
/* In a generator, we need to be able to swap between the exception
3234
state inside the generator and the exception state of the calling

Include/pystate.h

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,14 +92,19 @@ typedef struct _is {
9292
/* Py_tracefunc return -1 when raising an exception, or 0 for success. */
9393
typedef int (*Py_tracefunc)(PyObject *, struct _frame *, int, PyObject *);
9494

95-
/* The following values are used for 'what' for tracefunc functions: */
95+
/* The following values are used for 'what' for tracefunc functions
96+
*
97+
* To add a new kind of trace event, also update "trace_init" in
98+
* Python/sysmodule.c to define the Python level event name
99+
*/
96100
#define PyTrace_CALL 0
97101
#define PyTrace_EXCEPTION 1
98102
#define PyTrace_LINE 2
99103
#define PyTrace_RETURN 3
100104
#define PyTrace_C_CALL 4
101105
#define PyTrace_C_EXCEPTION 5
102106
#define PyTrace_C_RETURN 6
107+
#define PyTrace_OPCODE 7
103108
#endif
104109

105110
#ifdef Py_LIMITED_API

Lib/test/test_sys.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -971,7 +971,7 @@ class C(object): pass
971971
nfrees = len(x.f_code.co_freevars)
972972
extras = x.f_code.co_stacksize + x.f_code.co_nlocals +\
973973
ncells + nfrees - 1
974-
check(x, vsize('12P3ic' + CO_MAXBLOCKS*'3i' + 'P' + extras*'P'))
974+
check(x, vsize('8P2c4P3ic' + CO_MAXBLOCKS*'3i' + 'P' + extras*'P'))
975975
# function
976976
def func(): pass
977977
check(func, size('12P'))

Lib/test/test_sys_settrace.py

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -234,16 +234,29 @@ def generator_example():
234234

235235

236236
class Tracer:
237-
def __init__(self):
237+
def __init__(self, trace_line_events=None, trace_opcode_events=None):
238+
self.trace_line_events = trace_line_events
239+
self.trace_opcode_events = trace_opcode_events
238240
self.events = []
241+
242+
def _reconfigure_frame(self, frame):
243+
if self.trace_line_events is not None:
244+
frame.f_trace_lines = self.trace_line_events
245+
if self.trace_opcode_events is not None:
246+
frame.f_trace_opcodes = self.trace_opcode_events
247+
239248
def trace(self, frame, event, arg):
249+
self._reconfigure_frame(frame)
240250
self.events.append((frame.f_lineno, event))
241251
return self.trace
252+
242253
def traceWithGenexp(self, frame, event, arg):
254+
self._reconfigure_frame(frame)
243255
(o for o in [1])
244256
self.events.append((frame.f_lineno, event))
245257
return self.trace
246258

259+
247260
class TraceTestCase(unittest.TestCase):
248261

249262
# Disable gc collection when tracing, otherwise the
@@ -257,6 +270,11 @@ def tearDown(self):
257270
if self.using_gc:
258271
gc.enable()
259272

273+
@staticmethod
274+
def make_tracer():
275+
"""Helper to allow test subclasses to configure tracers differently"""
276+
return Tracer()
277+
260278
def compare_events(self, line_offset, events, expected_events):
261279
events = [(l - line_offset, e) for (l, e) in events]
262280
if events != expected_events:
@@ -266,7 +284,7 @@ def compare_events(self, line_offset, events, expected_events):
266284
[str(x) for x in events])))
267285

268286
def run_and_compare(self, func, events):
269-
tracer = Tracer()
287+
tracer = self.make_tracer()
270288
sys.settrace(tracer.trace)
271289
func()
272290
sys.settrace(None)
@@ -277,7 +295,7 @@ def run_test(self, func):
277295
self.run_and_compare(func, func.events)
278296

279297
def run_test2(self, func):
280-
tracer = Tracer()
298+
tracer = self.make_tracer()
281299
func(tracer.trace)
282300
sys.settrace(None)
283301
self.compare_events(func.__code__.co_firstlineno,
@@ -329,7 +347,7 @@ def test_13_genexp(self):
329347
# and if the traced function contains another generator
330348
# that is not completely exhausted, the trace stopped.
331349
# Worse: the 'finally' clause was not invoked.
332-
tracer = Tracer()
350+
tracer = self.make_tracer()
333351
sys.settrace(tracer.traceWithGenexp)
334352
generator_example()
335353
sys.settrace(None)
@@ -398,6 +416,34 @@ def func():
398416
(1, 'line')])
399417

400418

419+
class SkipLineEventsTraceTestCase(TraceTestCase):
420+
"""Repeat the trace tests, but with per-line events skipped"""
421+
422+
def compare_events(self, line_offset, events, expected_events):
423+
skip_line_events = [e for e in expected_events if e[1] != 'line']
424+
super().compare_events(line_offset, events, skip_line_events)
425+
426+
@staticmethod
427+
def make_tracer():
428+
return Tracer(trace_line_events=False)
429+
430+
431+
@support.cpython_only
432+
class TraceOpcodesTestCase(TraceTestCase):
433+
"""Repeat the trace tests, but with per-opcodes events enabled"""
434+
435+
def compare_events(self, line_offset, events, expected_events):
436+
skip_opcode_events = [e for e in events if e[1] != 'opcode']
437+
if len(events) > 1:
438+
self.assertLess(len(skip_opcode_events), len(events),
439+
msg="No 'opcode' events received by the tracer")
440+
super().compare_events(line_offset, skip_opcode_events, expected_events)
441+
442+
@staticmethod
443+
def make_tracer():
444+
return Tracer(trace_opcode_events=True)
445+
446+
401447
class RaisingTraceFuncTestCase(unittest.TestCase):
402448
def setUp(self):
403449
self.addCleanup(sys.settrace, sys.gettrace())
@@ -846,6 +892,8 @@ class fake_function:
846892
def test_main():
847893
support.run_unittest(
848894
TraceTestCase,
895+
SkipLineEventsTraceTestCase,
896+
TraceOpcodesTestCase,
849897
RaisingTraceFuncTestCase,
850898
JumpTestCase
851899
)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
For finer control of tracing behaviour when testing the interpreter, two new
2+
frame attributes have been added to control the emission of particular trace
3+
events: ``f_trace_lines`` (``True`` by default) to turn off per-line trace
4+
events; and ``f_trace_opcodes`` (``False`` by default) to turn on per-opcode
5+
trace events.

Objects/frameobject.c

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ static PyMemberDef frame_memberlist[] = {
1515
{"f_builtins", T_OBJECT, OFF(f_builtins), READONLY},
1616
{"f_globals", T_OBJECT, OFF(f_globals), READONLY},
1717
{"f_lasti", T_INT, OFF(f_lasti), READONLY},
18+
{"f_trace_lines", T_BOOL, OFF(f_trace_lines), 0},
19+
{"f_trace_opcodes", T_BOOL, OFF(f_trace_opcodes), 0},
1820
{NULL} /* Sentinel */
1921
};
2022

@@ -728,6 +730,8 @@ _PyFrame_New_NoTrack(PyThreadState *tstate, PyCodeObject *code,
728730
f->f_iblock = 0;
729731
f->f_executing = 0;
730732
f->f_gen = NULL;
733+
f->f_trace_opcodes = 0;
734+
f->f_trace_lines = 1;
731735

732736
return f;
733737
}

Python/ceval.c

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4458,12 +4458,19 @@ maybe_call_line_trace(Py_tracefunc func, PyObject *obj,
44584458
*instr_lb = bounds.ap_lower;
44594459
*instr_ub = bounds.ap_upper;
44604460
}
4461-
/* If the last instruction falls at the start of a line or if
4462-
it represents a jump backwards, update the frame's line
4463-
number and call the trace function. */
4464-
if (frame->f_lasti == *instr_lb || frame->f_lasti < *instr_prev) {
4461+
/* Always emit an opcode event if we're tracing all opcodes. */
4462+
if (frame->f_trace_opcodes) {
4463+
result = call_trace(func, obj, tstate, frame, PyTrace_OPCODE, Py_None);
4464+
}
4465+
/* If the last instruction falls at the start of a line or if it
4466+
represents a jump backwards, update the frame's line number and
4467+
then call the trace function if we're tracing source lines.
4468+
*/
4469+
if ((frame->f_lasti == *instr_lb || frame->f_lasti < *instr_prev)) {
44654470
frame->f_lineno = line;
4466-
result = call_trace(func, obj, tstate, frame, PyTrace_LINE, Py_None);
4471+
if (frame->f_trace_lines) {
4472+
result = call_trace(func, obj, tstate, frame, PyTrace_LINE, Py_None);
4473+
}
44674474
}
44684475
*instr_prev = frame->f_lasti;
44694476
return result;

Python/sysmodule.c

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -349,18 +349,19 @@ same value.");
349349
* Cached interned string objects used for calling the profile and
350350
* trace functions. Initialized by trace_init().
351351
*/
352-
static PyObject *whatstrings[7] = {NULL, NULL, NULL, NULL, NULL, NULL, NULL};
352+
static PyObject *whatstrings[8] = {NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL};
353353

354354
static int
355355
trace_init(void)
356356
{
357-
static const char * const whatnames[7] = {
357+
static const char * const whatnames[8] = {
358358
"call", "exception", "line", "return",
359-
"c_call", "c_exception", "c_return"
359+
"c_call", "c_exception", "c_return",
360+
"opcode"
360361
};
361362
PyObject *name;
362363
int i;
363-
for (i = 0; i < 7; ++i) {
364+
for (i = 0; i < 8; ++i) {
364365
if (whatstrings[i] == NULL) {
365366
name = PyUnicode_InternFromString(whatnames[i]);
366367
if (name == NULL)

0 commit comments

Comments
 (0)