https://github.com/python/cpython/commit/a8d74c062fe3c5cb2962dde8bee83704fcfa1bc9
commit: a8d74c062fe3c5cb2962dde8bee83704fcfa1bc9
branch: main
author: Maurycy Pawłowski-Wieroński <[email protected]>
committer: pablogsal <[email protected]>
date: 2026-06-17T16:49:23-04:00
summary:
gh-151436: Fix missing `tstate->last_profiled_frame` updates (#151437)
files:
A Misc/NEWS.d/next/Library/2026-06-13-11-57-48.gh-issue-151436.UEDowO.rst
M Include/internal/pycore_interpframe.h
M Modules/_testinternalcapi/test_cases.c.h
M Objects/genobject.c
M Python/bytecodes.c
M Python/ceval.c
M Python/executor_cases.c.h
M Python/generated_cases.c.h
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/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.
diff --git a/Modules/_testinternalcapi/test_cases.c.h
b/Modules/_testinternalcapi/test_cases.c.h
index 62d08826a2faea4..f2935310ffbdb9d 100644
--- a/Modules/_testinternalcapi/test_cases.c.h
+++ b/Modules/_testinternalcapi/test_cases.c.h
@@ -7939,6 +7939,7 @@
gen->gi_exc_state.previous_item = NULL;
_Py_LeaveRecursiveCallPy(tstate);
_PyInterpreterFrame *gen_frame = frame;
+ _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,6 +11001,7 @@
gen_frame->owner = FRAME_OWNED_BY_GENERATOR;
_Py_LeaveRecursiveCallPy(tstate);
_PyInterpreterFrame *prev = frame->previous;
+ _PyThreadState_UpdateLastProfiledFrame(tstate, frame, prev);
_PyThreadState_PopFrame(tstate, frame);
frame = tstate->current_frame = prev;
LOAD_IP(frame->return_offset);
@@ -13029,6 +13031,7 @@
gen->gi_exc_state.previous_item = NULL;
_Py_LeaveRecursiveCallPy(tstate);
_PyInterpreterFrame *gen_frame = frame;
+ _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 38d493343454fce..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,6 +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);
+ _PyThreadState_UpdateLastProfiledFrame(tstate, frame, prev);
tstate->current_frame = prev;
frame->previous = NULL;
}
@@ -701,6 +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);
+ _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 beaf6752b87ea2d..51a188a6120fa7e 100644
--- a/Python/bytecodes.c
+++ b/Python/bytecodes.c
@@ -1860,6 +1860,7 @@ dummy_func(
gen->gi_exc_state.previous_item = NULL;
_Py_LeaveRecursiveCallPy(tstate);
_PyInterpreterFrame *gen_frame = frame;
+ _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;
@@ -5874,6 +5875,7 @@ dummy_func(
gen_frame->owner = FRAME_OWNED_BY_GENERATOR;
_Py_LeaveRecursiveCallPy(tstate);
_PyInterpreterFrame *prev = frame->previous;
+ _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 d6dd7f9a82c4314..32d31bee660a504 100644
--- a/Python/ceval.c
+++ b/Python/ceval.c
@@ -1974,15 +1974,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);
@@ -2008,6 +2001,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);
+ _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 d5bfe60cd234737..e3d13f9f9c61c3f 100644
--- a/Python/executor_cases.c.h
+++ b/Python/executor_cases.c.h
@@ -9340,6 +9340,7 @@
gen->gi_exc_state.previous_item = NULL;
_Py_LeaveRecursiveCallPy(tstate);
_PyInterpreterFrame *gen_frame = frame;
+ _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;
@@ -20346,6 +20347,7 @@
gen_frame->owner = FRAME_OWNED_BY_GENERATOR;
_Py_LeaveRecursiveCallPy(tstate);
_PyInterpreterFrame *prev = frame->previous;
+ _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 a6e0f90d8c1ce2a..a5590d992b39cfd 100644
--- a/Python/generated_cases.c.h
+++ b/Python/generated_cases.c.h
@@ -7938,6 +7938,7 @@
gen->gi_exc_state.previous_item = NULL;
_Py_LeaveRecursiveCallPy(tstate);
_PyInterpreterFrame *gen_frame = frame;
+ _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;
@@ -10997,6 +10998,7 @@
gen_frame->owner = FRAME_OWNED_BY_GENERATOR;
_Py_LeaveRecursiveCallPy(tstate);
_PyInterpreterFrame *prev = frame->previous;
+ _PyThreadState_UpdateLastProfiledFrame(tstate, frame, prev);
_PyThreadState_PopFrame(tstate, frame);
frame = tstate->current_frame = prev;
LOAD_IP(frame->return_offset);
@@ -13026,6 +13028,7 @@
gen->gi_exc_state.previous_item = NULL;
_Py_LeaveRecursiveCallPy(tstate);
_PyInterpreterFrame *gen_frame = frame;
+ _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;
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]