Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gh-91462: Make lltrace output human-readable. #91463

Merged
merged 14 commits into from
Apr 16, 2022
97 changes: 89 additions & 8 deletions Lib/test/test_lltrace.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,112 @@
import os
import dis
import sys
import textwrap
import unittest

from test.support import os_helper
from test.support import os_helper, verbose
from test.support.script_helper import assert_python_ok

def example():
x = []
for i in range(1):
x.append(i)
x = "this is"
y = "an example"
print(x, y)

Py_DEBUG = hasattr(sys, 'gettotalrefcount')

@unittest.skipUnless(Py_DEBUG, "lltrace requires Py_DEBUG")
class TestLLTrace(unittest.TestCase):

def run_code(self, code):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Surprised there isn't already a utility for this.)

code = textwrap.dedent(code).strip()
with open(os_helper.TESTFN, 'w', encoding='utf-8') as fd:
self.addCleanup(os_helper.unlink, os_helper.TESTFN)
fd.write(code)
status, stdout, stderr = assert_python_ok(os_helper.TESTFN)
self.assertEqual(stderr, b"")
self.assertEqual(status, 0)
result = stdout.decode('utf-8')
if verbose:
print("\n\n--- code ---")
print(code)
print("\n--- stdout ---")
print(result)
print()
return result

def test_lltrace(self):
stdout = self.run_code("""
def dont_trace_1():
a = "a"
a = 10 * a
def trace_me():
for i in range(3):
+i
def dont_trace_2():
x = 42
y = -x
dont_trace_1()
__ltrace__ = 1
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This tripped me over! :-) It almost seems like a bug that the feature is called LLTRACE but the dunder is __ltrace__...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems to be a decade-old typo introduced here: 3c1e481

We could maybe accept both __ltrace__ and __lltrace__ at this point?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Eh, just change to __lltrace__. This feature is too niche to require backward compatibility (nobody complained when it was broken 10 years ago :-).

trace_me()
del __ltrace__
dont_trace_2()
""")
self.assertIn("GET_ITER", stdout)
self.assertIn("FOR_ITER", stdout)
self.assertIn("UNARY_POSITIVE", stdout)
self.assertIn("POP_TOP", stdout)
self.assertNotIn("BINARY_OP", stdout)
self.assertNotIn("UNARY_NEGATIVE", stdout)
gvanrossum marked this conversation as resolved.
Show resolved Hide resolved

self.assertIn("'trace_me' in module '__main__'", stdout)
self.assertNotIn("dont_trace_1", stdout)
self.assertNotIn("'dont_trace_2' in module", stdout)
gvanrossum marked this conversation as resolved.
Show resolved Hide resolved

def test_lltrace_different_module(self):
stdout = self.run_code("""
from test import test_lltrace
test_lltrace.__ltrace__ = 1
test_lltrace.example()
""")
self.assertIn("'example' in module 'test.test_lltrace'", stdout)
self.assertIn('LOAD_CONST', stdout)
self.assertIn('FOR_ITER', stdout)
self.assertIn('this is an example', stdout)

# check that offsets match the output of dis.dis()
instr_map = {i.offset: i for i in dis.get_instructions(example)}
for line in stdout.splitlines():
offset, colon, opname_oparg = line.partition(":")
if not colon:
continue
offset = int(offset)
opname_oparg = opname_oparg.split()
if len(opname_oparg) == 2:
opname, oparg = opname_oparg
oparg = int(oparg)
else:
(opname,) = opname_oparg
oparg = None
self.assertEqual(instr_map[offset].opname, opname)
self.assertEqual(instr_map[offset].arg, oparg)

def test_lltrace_does_not_crash_on_subscript_operator(self):
# If this test fails, it will reproduce a crash reported as
# bpo-34113. The crash happened at the command line console of
# debug Python builds with __ltrace__ enabled (only possible in console),
# when the internal Python stack was negatively adjusted
with open(os_helper.TESTFN, 'w', encoding='utf-8') as fd:
self.addCleanup(os_helper.unlink, os_helper.TESTFN)
fd.write(textwrap.dedent("""\
stdout = self.run_code("""
import code

console = code.InteractiveConsole()
console.push('__ltrace__ = 1')
console.push('a = [1, 2, 3]')
console.push('a[0] = 1')
print('unreachable if bug exists')
"""))

assert_python_ok(os_helper.TESTFN)
""")
self.assertIn("unreachable if bug exists", stdout)

if __name__ == "__main__":
unittest.main()
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Make the interpreter's low-level tracing (lltrace) feature output more readable by displaying opcode names (rather than just numbers), and by displaying stack contents before each opcode.
118 changes: 83 additions & 35 deletions Python/ceval.c
Original file line number Diff line number Diff line change
Expand Up @@ -54,15 +54,77 @@ static PyObject * do_call_core(

#ifdef LLTRACE
static int lltrace;
static int prtrace(PyThreadState *, PyObject *, const char *);
static void lltrace_instruction(_PyInterpreterFrame *frame, int opcode, int oparg)
static void
dump_stack(_PyInterpreterFrame *frame, PyObject **stack_pointer)
{
PyObject **stack_base = _PyFrame_Stackbase(frame);
PyObject *type, *value, *traceback;
PyErr_Fetch(&type, &value, &traceback);
printf(" stack=[");
for (PyObject **ptr = stack_base; ptr < stack_pointer; ptr++) {
if (ptr != stack_base) {
printf(", ");
}
if (PyObject_Print(*ptr, stdout, 0) != 0) {
PyErr_Clear();
printf("<%s object at %p>",
Py_TYPE(*ptr)->tp_name, (void *)(*ptr));
}
}
printf("]\n");
fflush(stdout);
PyErr_Restore(type, value, traceback);
}

static void
lltrace_instruction(_PyInterpreterFrame *frame,
PyObject **stack_pointer,
_Py_CODEUNIT *next_instr)
{
dump_stack(frame, stack_pointer);
int oparg = _Py_OPARG(*next_instr);
int opcode = _Py_OPCODE(*next_instr);
const char *opname = _PyOpcode_OpName[opcode];
assert(opname != NULL);
int offset = (int)(next_instr - _PyCode_CODE(frame->f_code));
if (HAS_ARG(opcode)) {
printf("%d: %d, %d\n", _PyInterpreterFrame_LASTI(frame), opcode, oparg);
printf("%d: %s %d\n", offset * 2, opname, oparg);
gvanrossum marked this conversation as resolved.
Show resolved Hide resolved
}
else {
printf("%d: %d\n", _PyInterpreterFrame_LASTI(frame), opcode);
printf("%d: %s\n", offset * 2, opname);
}
fflush(stdout);
}
static void
lltrace_resume_frame(_PyInterpreterFrame *frame)
{
PyFunctionObject *f = frame->f_func;
if (f == NULL) {
printf("\nResuming frame.");
return;
}
PyObject *type, *value, *traceback;
PyErr_Fetch(&type, &value, &traceback);
PyObject *name = f->func_qualname;
if (name == NULL) {
name = f->func_name;
}
printf("\nResuming frame");
if (name) {
printf(" for ");
if (PyObject_Print(name, stdout, 0) < 0) {
PyErr_Clear();
}
}
if (f->func_module) {
printf(" in module ");
if (PyObject_Print(f->func_module, stdout, 0) < 0) {
PyErr_Clear();
}
}
printf("\n");
fflush(stdout);
PyErr_Restore(type, value, traceback);
}
#endif
static int call_trace(Py_tracefunc, PyObject *,
Expand Down Expand Up @@ -1266,7 +1328,8 @@ eval_frame_handle_pending(PyThreadState *tstate)

/* PRE_DISPATCH_GOTO() does lltrace if enabled. Normally a no-op */
#ifdef LLTRACE
#define PRE_DISPATCH_GOTO() if (lltrace) { lltrace_instruction(frame, opcode, oparg); }
#define PRE_DISPATCH_GOTO() if (lltrace) { \
lltrace_instruction(frame, stack_pointer, next_instr); }
#else
#define PRE_DISPATCH_GOTO() ((void)0)
#endif
Expand Down Expand Up @@ -1375,6 +1438,7 @@ eval_frame_handle_pending(PyThreadState *tstate)
/* The stack can grow at most MAXINT deep, as co_nlocals and
co_stacksize are ints. */
#define STACK_LEVEL() ((int)(stack_pointer - _PyFrame_Stackbase(frame)))
#define STACK_SIZE() (frame->f_code->co_stacksize)
#define EMPTY() (STACK_LEVEL() == 0)
#define TOP() (stack_pointer[-1])
#define SECOND() (stack_pointer[-2])
Expand All @@ -1387,23 +1451,21 @@ eval_frame_handle_pending(PyThreadState *tstate)
#define BASIC_PUSH(v) (*stack_pointer++ = (v))
#define BASIC_POP() (*--stack_pointer)

#ifdef LLTRACE
#define PUSH(v) { (void)(BASIC_PUSH(v), \
lltrace && prtrace(tstate, TOP(), "push")); \
assert(STACK_LEVEL() <= frame->f_code->co_stacksize); }
#define POP() ((void)(lltrace && prtrace(tstate, TOP(), "pop")), \
BASIC_POP())
#ifdef Py_DEBUG
#define PUSH(v) do { \
BASIC_PUSH(v); \
assert(STACK_LEVEL() <= STACK_SIZE()); \
} while (0)
#define POP() (assert(STACK_LEVEL() > 0), BASIC_POP())
#define STACK_GROW(n) do { \
assert(n >= 0); \
(void)(BASIC_STACKADJ(n), \
lltrace && prtrace(tstate, TOP(), "stackadj")); \
assert(STACK_LEVEL() <= frame->f_code->co_stacksize); \
assert(n >= 0); \
BASIC_STACKADJ(n); \
assert(STACK_LEVEL() <= STACK_SIZE()); \
} while (0)
#define STACK_SHRINK(n) do { \
assert(n >= 0); \
(void)(lltrace && prtrace(tstate, TOP(), "stackadj")); \
(void)(BASIC_STACKADJ(-(n))); \
assert(STACK_LEVEL() <= frame->f_code->co_stacksize); \
assert(STACK_LEVEL() >= n); \
BASIC_STACKADJ(-(n)); \
} while (0)
#else
#define PUSH(v) BASIC_PUSH(v)
Expand Down Expand Up @@ -1673,6 +1735,9 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, _PyInterpreterFrame *frame, int
}
lltrace = r;
}
if (lltrace) {
lltrace_resume_frame(frame);
}
#endif

#ifdef Py_DEBUG
Expand Down Expand Up @@ -6663,23 +6728,6 @@ unpack_iterable(PyThreadState *tstate, PyObject *v,
return 0;
}

#ifdef LLTRACE
static int
prtrace(PyThreadState *tstate, PyObject *v, const char *str)
{
printf("%s ", str);
PyObject *type, *value, *traceback;
PyErr_Fetch(&type, &value, &traceback);
if (PyObject_Print(v, stdout, 0) != 0) {
/* Don't know what else to do */
_PyErr_Clear(tstate);
}
printf("\n");
PyErr_Restore(type, value, traceback);
return 1;
}
#endif

static void
call_exc_trace(Py_tracefunc func, PyObject *self,
PyThreadState *tstate,
Expand Down