From d7163bb35d1ed46bde9affcd4eb267dfd0b703dd Mon Sep 17 00:00:00 2001 From: Mark Shannon Date: Fri, 25 Mar 2022 12:57:50 +0000 Subject: bpo-42197: Don't create `f_locals` dictionary unless we actually need it. (GH-32055) * `PyFrame_FastToLocalsWithError` and `PyFrame_LocalsToFast` are no longer called during profile and tracing. (Contributed by Fabio Zadrozny) * Make accesses to a frame's `f_locals` safe from C code, not relying on calls to `PyFrame_FastToLocals` or `PyFrame_LocalsToFast`. * Document new `PyFrame_GetLocals` C-API function. --- Doc/c-api/frame.rst | 11 +++++++++++ Doc/whatsnew/3.11.rst | 8 +++++++- Include/cpython/frameobject.h | 1 + Include/internal/pycore_frame.h | 1 + .../2022-03-22-15-12-28.bpo-42197.SwrrFO.rst | 2 ++ Objects/frameobject.c | 23 ++++++++++++++++------ Python/sysmodule.c | 10 +++++++--- 7 files changed, 46 insertions(+), 10 deletions(-) create mode 100644 Misc/NEWS.d/next/Core and Builtins/2022-03-22-15-12-28.bpo-42197.SwrrFO.rst diff --git a/Doc/c-api/frame.rst b/Doc/c-api/frame.rst index 0e36e6e..0c11bc1 100644 --- a/Doc/c-api/frame.rst +++ b/Doc/c-api/frame.rst @@ -41,6 +41,17 @@ See also :ref:`Reflection `. .. versionadded:: 3.9 +.. c:function:: PyObject* PyFrame_GetLocals(PyFrameObject *frame) + + Get the *frame*'s ``f_locals`` attribute (:class:`dict`). + + Return a :term:`strong reference`. + + *frame* must not be ``NULL``. + + .. versionadded:: 3.11 + + .. c:function:: int PyFrame_GetLineNumber(PyFrameObject *frame) Return the line number that *frame* is currently executing. diff --git a/Doc/whatsnew/3.11.rst b/Doc/whatsnew/3.11.rst index b2fdb48..8c120ec 100644 --- a/Doc/whatsnew/3.11.rst +++ b/Doc/whatsnew/3.11.rst @@ -969,7 +969,7 @@ Porting to Python 3.11 Code using ``f_lasti`` with ``PyCode_Addr2Line()`` must use :c:func:`PyFrame_GetLineNumber` instead. * ``f_lineno``: use :c:func:`PyFrame_GetLineNumber` - * ``f_locals``: use ``PyObject_GetAttrString((PyObject*)frame, "f_locals")``. + * ``f_locals``: use :c:func:`PyFrame_GetLocals`. * ``f_stackdepth``: removed. * ``f_state``: no public API (renamed to ``f_frame.f_state``). * ``f_trace``: no public API. @@ -983,6 +983,12 @@ Porting to Python 3.11 computed lazily. The :c:func:`PyFrame_GetBack` function must be called instead. + Debuggers that accessed the ``f_locals`` directly *must* call + `:c:func:`PyFrame_GetLocals` instead. They no longer need to call + `:c:func:`PyFrame_FastToLocalsWithError` or :c:func:`PyFrame_LocalsToFast`, + in fact they should not call those functions. The necessary updating of the + frame is now managed by the virtual machine. + Code defining ``PyFrame_GetCode()`` on Python 3.8 and older:: #if PY_VERSION_HEX < 0x030900B1 diff --git a/Include/cpython/frameobject.h b/Include/cpython/frameobject.h index 9b697fb..d54d365 100644 --- a/Include/cpython/frameobject.h +++ b/Include/cpython/frameobject.h @@ -23,3 +23,4 @@ PyAPI_FUNC(int) PyFrame_FastToLocalsWithError(PyFrameObject *f); PyAPI_FUNC(void) PyFrame_FastToLocals(PyFrameObject *); PyAPI_FUNC(PyFrameObject *) PyFrame_GetBack(PyFrameObject *frame); +PyAPI_FUNC(PyObject *) PyFrame_GetLocals(PyFrameObject *frame); diff --git a/Include/internal/pycore_frame.h b/Include/internal/pycore_frame.h index 14fba8c..211831a 100644 --- a/Include/internal/pycore_frame.h +++ b/Include/internal/pycore_frame.h @@ -15,6 +15,7 @@ struct _frame { int f_lineno; /* Current line number. Only valid if non-zero */ char f_trace_lines; /* Emit per-line trace events? */ char f_trace_opcodes; /* Emit per-opcode trace events? */ + char f_fast_as_locals; /* Have the fast locals of this frame been converted to a dict? */ /* The frame data, if this frame object owns the frame */ PyObject *_f_frame_data[1]; }; diff --git a/Misc/NEWS.d/next/Core and Builtins/2022-03-22-15-12-28.bpo-42197.SwrrFO.rst b/Misc/NEWS.d/next/Core and Builtins/2022-03-22-15-12-28.bpo-42197.SwrrFO.rst new file mode 100644 index 0000000..d54002a --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2022-03-22-15-12-28.bpo-42197.SwrrFO.rst @@ -0,0 +1,2 @@ +:c:func:`PyFrame_FastToLocalsWithError` and :c:func:`PyFrame_LocalsToFast` are no longer +called during profiling nor tracing. C code can access the ``f_locals`` attribute of :c:type:`PyFrameObject` by calling :c:func:`PyFrame_GetLocals`. diff --git a/Objects/frameobject.c b/Objects/frameobject.c index 5c6a8bc..13dfbf6 100644 --- a/Objects/frameobject.c +++ b/Objects/frameobject.c @@ -840,6 +840,7 @@ _PyFrame_New_NoTrack(PyCodeObject *code) f->f_trace = NULL; f->f_trace_lines = 1; f->f_trace_opcodes = 0; + f->f_fast_as_locals = 0; f->f_lineno = 0; return f; } @@ -1004,7 +1005,11 @@ PyFrame_FastToLocalsWithError(PyFrameObject *f) PyErr_BadInternalCall(); return -1; } - return _PyFrame_FastToLocalsWithError(f->f_frame); + int err = _PyFrame_FastToLocalsWithError(f->f_frame); + if (err == 0) { + f->f_fast_as_locals = 1; + } + return err; } void @@ -1028,8 +1033,9 @@ _PyFrame_LocalsToFast(_PyInterpreterFrame *frame, int clear) PyObject *error_type, *error_value, *error_traceback; PyCodeObject *co; locals = frame->f_locals; - if (locals == NULL) + if (locals == NULL) { return; + } fast = _PyFrame_GetLocalsArray(frame); co = frame->f_code; @@ -1088,13 +1094,12 @@ _PyFrame_LocalsToFast(_PyInterpreterFrame *frame, int clear) void PyFrame_LocalsToFast(PyFrameObject *f, int clear) { - if (f == NULL || _PyFrame_GetState(f) == FRAME_CLEARED) { - return; + if (f && f->f_fast_as_locals && _PyFrame_GetState(f) != FRAME_CLEARED) { + _PyFrame_LocalsToFast(f->f_frame, clear); + f->f_fast_as_locals = 0; } - _PyFrame_LocalsToFast(f->f_frame, clear); } - PyCodeObject * PyFrame_GetCode(PyFrameObject *frame) { @@ -1119,6 +1124,12 @@ PyFrame_GetBack(PyFrameObject *frame) } PyObject* +PyFrame_GetLocals(PyFrameObject *frame) +{ + return frame_getlocals(frame, NULL); +} + +PyObject* _PyEval_BuiltinsFromGlobals(PyThreadState *tstate, PyObject *globals) { PyObject *builtins = PyDict_GetItemWithError(globals, &_Py_ID(__builtins__)); diff --git a/Python/sysmodule.c b/Python/sysmodule.c index c89f81f..6322af5 100644 --- a/Python/sysmodule.c +++ b/Python/sysmodule.c @@ -924,15 +924,19 @@ static PyObject * call_trampoline(PyThreadState *tstate, PyObject* callback, PyFrameObject *frame, int what, PyObject *arg) { - if (PyFrame_FastToLocalsWithError(frame) < 0) { - return NULL; - } PyObject *stack[3]; stack[0] = (PyObject *)frame; stack[1] = whatstrings[what]; stack[2] = (arg != NULL) ? arg : Py_None; + /* Discard any previous modifications the frame's fast locals */ + if (frame->f_fast_as_locals) { + if (PyFrame_FastToLocalsWithError(frame) < 0) { + return NULL; + } + } + /* call the Python-level function */ PyObject *result = _PyObject_FastCallTstate(tstate, callback, stack, 3); -- cgit v0.12