]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-91462: Make lltrace output human-readable. (GH-91463)
authorDennis Sweeney <36520290+sweeneyde@users.noreply.github.com>
Sat, 16 Apr 2022 21:36:29 +0000 (17:36 -0400)
committerGitHub <noreply@github.com>
Sat, 16 Apr 2022 21:36:29 +0000 (17:36 -0400)
* Transform opcodes into opnames
* Print the whole stack at each opcode, and eliminate prtrace output at each (push/pop/stackadj)
* Display info about the function at each resume_frame

Lib/test/test_lltrace.py
Misc/NEWS.d/next/Core and Builtins/2022-04-12-00-44-14.gh-issue-91462.t8oxyd.rst [new file with mode: 0644]
Python/ceval.c

index 06e33f4c4c2f3859cddc810c24432ff125f8ba53..75b377b637b9aa02f80a9c3c4e7d95db0f5ea652 100644 (file)
-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):
+        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
+            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)
+
+        self.assertIn("'trace_me' in module '__main__'", stdout)
+        self.assertNotIn("dont_trace_1", stdout)
+        self.assertNotIn("'dont_trace_2' in module", stdout)
+
+    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()
@@ -23,9 +105,8 @@ class TestLLTrace(unittest.TestCase):
             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()
diff --git a/Misc/NEWS.d/next/Core and Builtins/2022-04-12-00-44-14.gh-issue-91462.t8oxyd.rst b/Misc/NEWS.d/next/Core and Builtins/2022-04-12-00-44-14.gh-issue-91462.t8oxyd.rst
new file mode 100644 (file)
index 0000000..0656b30
--- /dev/null
@@ -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.
index 66856e5177607f6c6f265f8f54d0a9ebc0b4ec92..d6f11d8f4364ae8f767fa4c7b948bdc92dce0112 100644 (file)
@@ -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);
     }
     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 *,
@@ -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
@@ -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])
@@ -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)
@@ -1673,6 +1735,9 @@ resume_frame:
         }
         lltrace = r;
     }
+    if (lltrace) {
+        lltrace_resume_frame(frame);
+    }
 #endif
 
 #ifdef Py_DEBUG
@@ -6663,23 +6728,6 @@ Error:
     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,