From 4eab5a0bf7b98409d5544cfbf78f6e1fae82d601 Mon Sep 17 00:00:00 2001 From: maurycy <5383+maurycy@users.noreply.github.com> Date: Sat, 13 Jun 2026 11:42:54 +0200 Subject: [PATCH 1/3] up last_profiled_frame --- Modules/_testinternalcapi/test_cases.c.h | 9 +++++++++ Objects/genobject.c | 6 ++++++ Python/bytecodes.c | 6 ++++++ Python/ceval.c | 3 +++ Python/executor_cases.c.h | 6 ++++++ Python/generated_cases.c.h | 9 +++++++++ 6 files changed, 39 insertions(+) diff --git a/Modules/_testinternalcapi/test_cases.c.h b/Modules/_testinternalcapi/test_cases.c.h index 503f566c9ae86a5..d3ac8e6f96b4193 100644 --- a/Modules/_testinternalcapi/test_cases.c.h +++ b/Modules/_testinternalcapi/test_cases.c.h @@ -7939,6 +7939,9 @@ gen->gi_exc_state.previous_item = NULL; _Py_LeaveRecursiveCallPy(tstate); _PyInterpreterFrame *gen_frame = frame; + if (tstate->last_profiled_frame == gen_frame) { + tstate->last_profiled_frame = gen_frame->previous; + } frame = tstate->current_frame = frame->previous; gen_frame->previous = NULL; ((_PyThreadStateImpl *)tstate)->generator_return_kind = GENERATOR_YIELD; @@ -11000,6 +11003,9 @@ gen_frame->owner = FRAME_OWNED_BY_GENERATOR; _Py_LeaveRecursiveCallPy(tstate); _PyInterpreterFrame *prev = frame->previous; + if (tstate->last_profiled_frame == frame) { + tstate->last_profiled_frame = prev; + } _PyThreadState_PopFrame(tstate, frame); frame = tstate->current_frame = prev; LOAD_IP(frame->return_offset); @@ -13029,6 +13035,9 @@ gen->gi_exc_state.previous_item = NULL; _Py_LeaveRecursiveCallPy(tstate); _PyInterpreterFrame *gen_frame = frame; + if (tstate->last_profiled_frame == gen_frame) { + tstate->last_profiled_frame = gen_frame->previous; + } frame = tstate->current_frame = frame->previous; gen_frame->previous = NULL; ((_PyThreadStateImpl *)tstate)->generator_return_kind = GENERATOR_YIELD; diff --git a/Objects/genobject.c b/Objects/genobject.c index 38d493343454fce..68249fc9a6a77de 100644 --- a/Objects/genobject.c +++ b/Objects/genobject.c @@ -681,6 +681,9 @@ _gen_throw(PyGenObject *gen, int close_on_genexit, 'yield from' or awaiting on with 'await'. */ ret = _gen_throw((PyGenObject *)yf, close_on_genexit, typ, val, tb); + if (tstate->last_profiled_frame == frame) { + tstate->last_profiled_frame = prev; + } tstate->current_frame = prev; frame->previous = NULL; } @@ -701,6 +704,9 @@ _gen_throw(PyGenObject *gen, int close_on_genexit, frame->previous = prev; tstate->current_frame = frame; ret = PyObject_CallFunctionObjArgs(meth, typ, val, tb, NULL); + if (tstate->last_profiled_frame == frame) { + tstate->last_profiled_frame = prev; + } tstate->current_frame = prev; frame->previous = NULL; Py_DECREF(meth); diff --git a/Python/bytecodes.c b/Python/bytecodes.c index e368092b300f864..08c09edd0e7a0a5 100644 --- a/Python/bytecodes.c +++ b/Python/bytecodes.c @@ -1860,6 +1860,9 @@ dummy_func( gen->gi_exc_state.previous_item = NULL; _Py_LeaveRecursiveCallPy(tstate); _PyInterpreterFrame *gen_frame = frame; + if (tstate->last_profiled_frame == gen_frame) { + tstate->last_profiled_frame = gen_frame->previous; + } frame = tstate->current_frame = frame->previous; gen_frame->previous = NULL; ((_PyThreadStateImpl *)tstate)->generator_return_kind = GENERATOR_YIELD; @@ -5874,6 +5877,9 @@ dummy_func( gen_frame->owner = FRAME_OWNED_BY_GENERATOR; _Py_LeaveRecursiveCallPy(tstate); _PyInterpreterFrame *prev = frame->previous; + if (tstate->last_profiled_frame == frame) { + tstate->last_profiled_frame = prev; + } _PyThreadState_PopFrame(tstate, frame); frame = tstate->current_frame = prev; LOAD_IP(frame->return_offset); diff --git a/Python/ceval.c b/Python/ceval.c index a9b31affca9890a..b7d8afa1b122f22 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -2006,6 +2006,9 @@ _PyEvalFramePushAndInit(PyThreadState *tstate, _PyStackRef func, _PyFrame_Initialize(tstate, frame, func, locals, code, 0, previous); if (initialize_locals(tstate, func_obj, frame->localsplus, args, argcount, kwnames)) { assert(frame->owner == FRAME_OWNED_BY_THREAD); + if (tstate->last_profiled_frame == frame) { + tstate->last_profiled_frame = tstate->current_frame; + } clear_thread_frame(tstate, frame); return NULL; } diff --git a/Python/executor_cases.c.h b/Python/executor_cases.c.h index 7973c75e1a60ad2..0589b860716c2e7 100644 --- a/Python/executor_cases.c.h +++ b/Python/executor_cases.c.h @@ -9340,6 +9340,9 @@ gen->gi_exc_state.previous_item = NULL; _Py_LeaveRecursiveCallPy(tstate); _PyInterpreterFrame *gen_frame = frame; + if (tstate->last_profiled_frame == gen_frame) { + tstate->last_profiled_frame = gen_frame->previous; + } frame = tstate->current_frame = frame->previous; gen_frame->previous = NULL; ((_PyThreadStateImpl *)tstate)->generator_return_kind = GENERATOR_YIELD; @@ -20346,6 +20349,9 @@ gen_frame->owner = FRAME_OWNED_BY_GENERATOR; _Py_LeaveRecursiveCallPy(tstate); _PyInterpreterFrame *prev = frame->previous; + if (tstate->last_profiled_frame == frame) { + tstate->last_profiled_frame = prev; + } _PyThreadState_PopFrame(tstate, frame); frame = tstate->current_frame = prev; LOAD_IP(frame->return_offset); diff --git a/Python/generated_cases.c.h b/Python/generated_cases.c.h index 5adcdcb4521baf5..3a7265ecbe31f41 100644 --- a/Python/generated_cases.c.h +++ b/Python/generated_cases.c.h @@ -7938,6 +7938,9 @@ gen->gi_exc_state.previous_item = NULL; _Py_LeaveRecursiveCallPy(tstate); _PyInterpreterFrame *gen_frame = frame; + if (tstate->last_profiled_frame == gen_frame) { + tstate->last_profiled_frame = gen_frame->previous; + } frame = tstate->current_frame = frame->previous; gen_frame->previous = NULL; ((_PyThreadStateImpl *)tstate)->generator_return_kind = GENERATOR_YIELD; @@ -10997,6 +11000,9 @@ gen_frame->owner = FRAME_OWNED_BY_GENERATOR; _Py_LeaveRecursiveCallPy(tstate); _PyInterpreterFrame *prev = frame->previous; + if (tstate->last_profiled_frame == frame) { + tstate->last_profiled_frame = prev; + } _PyThreadState_PopFrame(tstate, frame); frame = tstate->current_frame = prev; LOAD_IP(frame->return_offset); @@ -13026,6 +13032,9 @@ gen->gi_exc_state.previous_item = NULL; _Py_LeaveRecursiveCallPy(tstate); _PyInterpreterFrame *gen_frame = frame; + if (tstate->last_profiled_frame == gen_frame) { + tstate->last_profiled_frame = gen_frame->previous; + } frame = tstate->current_frame = frame->previous; gen_frame->previous = NULL; ((_PyThreadStateImpl *)tstate)->generator_return_kind = GENERATOR_YIELD; From 4c946536e9ba30767d36410d0527b0dc055071d9 Mon Sep 17 00:00:00 2001 From: maurycy <5383+maurycy@users.noreply.github.com> Date: Sat, 13 Jun 2026 11:58:17 +0200 Subject: [PATCH 2/3] news --- .../Library/2026-06-13-11-57-48.gh-issue-151436.UEDowO.rst | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-06-13-11-57-48.gh-issue-151436.UEDowO.rst diff --git a/Misc/NEWS.d/next/Library/2026-06-13-11-57-48.gh-issue-151436.UEDowO.rst b/Misc/NEWS.d/next/Library/2026-06-13-11-57-48.gh-issue-151436.UEDowO.rst new file mode 100644 index 000000000000000..1d1aadbf57be485 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-06-13-11-57-48.gh-issue-151436.UEDowO.rst @@ -0,0 +1,4 @@ +Fix skewed stack trackes in the Tachyon profiler when caching is enabled and +when generators and coroutines are profiled, by updating +``tstate->last_profiled_frame`` at every frame-removal site. The issue resulted +in total erasure of some callers. Patch by Maurycy Pawłowski-Wieroński. From 1348756c07ac0501fab5b46700da002ec804d5b7 Mon Sep 17 00:00:00 2001 From: Pablo Galindo Salgado Date: Wed, 17 Jun 2026 19:18:10 +0100 Subject: [PATCH 3/3] gh-151436: Add macro for last_profiled_frame updates --- Include/internal/pycore_interpframe.h | 14 ++++++++++++++ Modules/_testinternalcapi/test_cases.c.h | 12 +++--------- Objects/genobject.c | 9 +++------ Python/bytecodes.c | 8 ++------ Python/ceval.c | 13 ++----------- Python/executor_cases.c.h | 8 ++------ Python/generated_cases.c.h | 12 +++--------- 7 files changed, 29 insertions(+), 47 deletions(-) diff --git a/Include/internal/pycore_interpframe.h b/Include/internal/pycore_interpframe.h index ee90ca3a9b59002..0f4bf7d8a2f2f0a 100644 --- a/Include/internal/pycore_interpframe.h +++ b/Include/internal/pycore_interpframe.h @@ -287,6 +287,20 @@ _PyThreadState_GetFrame(PyThreadState *tstate) return _PyFrame_GetFirstComplete(tstate->current_frame); } +// Update last_profiled_frame for remote profiler frame caching. +// Only update if we're removing the exact frame that was last profiled. +// This avoids corrupting the cache when transient frames (called and returned +// between profiler samples) update last_profiled_frame to addresses the +// profiler never saw. +#define _PyThreadState_UpdateLastProfiledFrame(tstate, frame, previous) \ + do { \ + PyThreadState *tstate_ = (tstate); \ + _PyInterpreterFrame *frame_ = (frame); \ + if (tstate_->last_profiled_frame == frame_) { \ + tstate_->last_profiled_frame = (previous); \ + } \ + } while (0) + /* For use by _PyFrame_GetFrameObject Do not call directly. */ PyAPI_FUNC(PyFrameObject *) diff --git a/Modules/_testinternalcapi/test_cases.c.h b/Modules/_testinternalcapi/test_cases.c.h index d3ac8e6f96b4193..fbe32cd5f4210b2 100644 --- a/Modules/_testinternalcapi/test_cases.c.h +++ b/Modules/_testinternalcapi/test_cases.c.h @@ -7939,9 +7939,7 @@ gen->gi_exc_state.previous_item = NULL; _Py_LeaveRecursiveCallPy(tstate); _PyInterpreterFrame *gen_frame = frame; - if (tstate->last_profiled_frame == gen_frame) { - tstate->last_profiled_frame = gen_frame->previous; - } + _PyThreadState_UpdateLastProfiledFrame(tstate, gen_frame, gen_frame->previous); frame = tstate->current_frame = frame->previous; gen_frame->previous = NULL; ((_PyThreadStateImpl *)tstate)->generator_return_kind = GENERATOR_YIELD; @@ -11003,9 +11001,7 @@ gen_frame->owner = FRAME_OWNED_BY_GENERATOR; _Py_LeaveRecursiveCallPy(tstate); _PyInterpreterFrame *prev = frame->previous; - if (tstate->last_profiled_frame == frame) { - tstate->last_profiled_frame = prev; - } + _PyThreadState_UpdateLastProfiledFrame(tstate, frame, prev); _PyThreadState_PopFrame(tstate, frame); frame = tstate->current_frame = prev; LOAD_IP(frame->return_offset); @@ -13035,9 +13031,7 @@ gen->gi_exc_state.previous_item = NULL; _Py_LeaveRecursiveCallPy(tstate); _PyInterpreterFrame *gen_frame = frame; - if (tstate->last_profiled_frame == gen_frame) { - tstate->last_profiled_frame = gen_frame->previous; - } + _PyThreadState_UpdateLastProfiledFrame(tstate, gen_frame, gen_frame->previous); frame = tstate->current_frame = frame->previous; gen_frame->previous = NULL; ((_PyThreadStateImpl *)tstate)->generator_return_kind = GENERATOR_YIELD; diff --git a/Objects/genobject.c b/Objects/genobject.c index 68249fc9a6a77de..3cdc06733363d3e 100644 --- a/Objects/genobject.c +++ b/Objects/genobject.c @@ -168,6 +168,7 @@ gen_clear_frame(PyGenObject *gen) { assert(FT_ATOMIC_LOAD_INT8_RELAXED(gen->gi_frame_state) == FRAME_CLEARED); _PyInterpreterFrame *frame = &gen->gi_iframe; + _PyThreadState_UpdateLastProfiledFrame(_PyThreadState_GET(), frame, frame->previous); frame->previous = NULL; _PyFrame_ClearExceptCode(frame); _PyErr_ClearExcState(&gen->gi_exc_state); @@ -681,9 +682,7 @@ _gen_throw(PyGenObject *gen, int close_on_genexit, 'yield from' or awaiting on with 'await'. */ ret = _gen_throw((PyGenObject *)yf, close_on_genexit, typ, val, tb); - if (tstate->last_profiled_frame == frame) { - tstate->last_profiled_frame = prev; - } + _PyThreadState_UpdateLastProfiledFrame(tstate, frame, prev); tstate->current_frame = prev; frame->previous = NULL; } @@ -704,9 +703,7 @@ _gen_throw(PyGenObject *gen, int close_on_genexit, frame->previous = prev; tstate->current_frame = frame; ret = PyObject_CallFunctionObjArgs(meth, typ, val, tb, NULL); - if (tstate->last_profiled_frame == frame) { - tstate->last_profiled_frame = prev; - } + _PyThreadState_UpdateLastProfiledFrame(tstate, frame, prev); tstate->current_frame = prev; frame->previous = NULL; Py_DECREF(meth); diff --git a/Python/bytecodes.c b/Python/bytecodes.c index 08c09edd0e7a0a5..f4d10532a8039ba 100644 --- a/Python/bytecodes.c +++ b/Python/bytecodes.c @@ -1860,9 +1860,7 @@ dummy_func( gen->gi_exc_state.previous_item = NULL; _Py_LeaveRecursiveCallPy(tstate); _PyInterpreterFrame *gen_frame = frame; - if (tstate->last_profiled_frame == gen_frame) { - tstate->last_profiled_frame = gen_frame->previous; - } + _PyThreadState_UpdateLastProfiledFrame(tstate, gen_frame, gen_frame->previous); frame = tstate->current_frame = frame->previous; gen_frame->previous = NULL; ((_PyThreadStateImpl *)tstate)->generator_return_kind = GENERATOR_YIELD; @@ -5877,9 +5875,7 @@ dummy_func( gen_frame->owner = FRAME_OWNED_BY_GENERATOR; _Py_LeaveRecursiveCallPy(tstate); _PyInterpreterFrame *prev = frame->previous; - if (tstate->last_profiled_frame == frame) { - tstate->last_profiled_frame = prev; - } + _PyThreadState_UpdateLastProfiledFrame(tstate, frame, prev); _PyThreadState_PopFrame(tstate, frame); frame = tstate->current_frame = prev; LOAD_IP(frame->return_offset); diff --git a/Python/ceval.c b/Python/ceval.c index b7d8afa1b122f22..4cb6f60b5eaf9b1 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -1972,15 +1972,8 @@ clear_gen_frame(PyThreadState *tstate, _PyInterpreterFrame * frame) void _PyEval_FrameClearAndPop(PyThreadState *tstate, _PyInterpreterFrame * frame) { - // Update last_profiled_frame for remote profiler frame caching. // By this point, tstate->current_frame is already set to the parent frame. - // Only update if we're popping the exact frame that was last profiled. - // This avoids corrupting the cache when transient frames (called and returned - // between profiler samples) update last_profiled_frame to addresses the - // profiler never saw. - if (tstate->last_profiled_frame != NULL && tstate->last_profiled_frame == frame) { - tstate->last_profiled_frame = tstate->current_frame; - } + _PyThreadState_UpdateLastProfiledFrame(tstate, frame, tstate->current_frame); if (frame->owner == FRAME_OWNED_BY_THREAD) { clear_thread_frame(tstate, frame); @@ -2006,9 +1999,7 @@ _PyEvalFramePushAndInit(PyThreadState *tstate, _PyStackRef func, _PyFrame_Initialize(tstate, frame, func, locals, code, 0, previous); if (initialize_locals(tstate, func_obj, frame->localsplus, args, argcount, kwnames)) { assert(frame->owner == FRAME_OWNED_BY_THREAD); - if (tstate->last_profiled_frame == frame) { - tstate->last_profiled_frame = tstate->current_frame; - } + _PyThreadState_UpdateLastProfiledFrame(tstate, frame, tstate->current_frame); clear_thread_frame(tstate, frame); return NULL; } diff --git a/Python/executor_cases.c.h b/Python/executor_cases.c.h index 0589b860716c2e7..05c20c243719eb3 100644 --- a/Python/executor_cases.c.h +++ b/Python/executor_cases.c.h @@ -9340,9 +9340,7 @@ gen->gi_exc_state.previous_item = NULL; _Py_LeaveRecursiveCallPy(tstate); _PyInterpreterFrame *gen_frame = frame; - if (tstate->last_profiled_frame == gen_frame) { - tstate->last_profiled_frame = gen_frame->previous; - } + _PyThreadState_UpdateLastProfiledFrame(tstate, gen_frame, gen_frame->previous); frame = tstate->current_frame = frame->previous; gen_frame->previous = NULL; ((_PyThreadStateImpl *)tstate)->generator_return_kind = GENERATOR_YIELD; @@ -20349,9 +20347,7 @@ gen_frame->owner = FRAME_OWNED_BY_GENERATOR; _Py_LeaveRecursiveCallPy(tstate); _PyInterpreterFrame *prev = frame->previous; - if (tstate->last_profiled_frame == frame) { - tstate->last_profiled_frame = prev; - } + _PyThreadState_UpdateLastProfiledFrame(tstate, frame, prev); _PyThreadState_PopFrame(tstate, frame); frame = tstate->current_frame = prev; LOAD_IP(frame->return_offset); diff --git a/Python/generated_cases.c.h b/Python/generated_cases.c.h index 3a7265ecbe31f41..273a46eb94e86a9 100644 --- a/Python/generated_cases.c.h +++ b/Python/generated_cases.c.h @@ -7938,9 +7938,7 @@ gen->gi_exc_state.previous_item = NULL; _Py_LeaveRecursiveCallPy(tstate); _PyInterpreterFrame *gen_frame = frame; - if (tstate->last_profiled_frame == gen_frame) { - tstate->last_profiled_frame = gen_frame->previous; - } + _PyThreadState_UpdateLastProfiledFrame(tstate, gen_frame, gen_frame->previous); frame = tstate->current_frame = frame->previous; gen_frame->previous = NULL; ((_PyThreadStateImpl *)tstate)->generator_return_kind = GENERATOR_YIELD; @@ -11000,9 +10998,7 @@ gen_frame->owner = FRAME_OWNED_BY_GENERATOR; _Py_LeaveRecursiveCallPy(tstate); _PyInterpreterFrame *prev = frame->previous; - if (tstate->last_profiled_frame == frame) { - tstate->last_profiled_frame = prev; - } + _PyThreadState_UpdateLastProfiledFrame(tstate, frame, prev); _PyThreadState_PopFrame(tstate, frame); frame = tstate->current_frame = prev; LOAD_IP(frame->return_offset); @@ -13032,9 +13028,7 @@ gen->gi_exc_state.previous_item = NULL; _Py_LeaveRecursiveCallPy(tstate); _PyInterpreterFrame *gen_frame = frame; - if (tstate->last_profiled_frame == gen_frame) { - tstate->last_profiled_frame = gen_frame->previous; - } + _PyThreadState_UpdateLastProfiledFrame(tstate, gen_frame, gen_frame->previous); frame = tstate->current_frame = frame->previous; gen_frame->previous = NULL; ((_PyThreadStateImpl *)tstate)->generator_return_kind = GENERATOR_YIELD;