Skip to content

gh-157605: Accept empty Python frame stack in sampling frame chain parser - #157657

Open
aidaodedjl wants to merge 2 commits into
python:mainfrom
aidaodedjl:gh-157605-sampling-empty-native-stack
Open

aidaodedjl wants to merge 2 commits into
python:mainfrom
aidaodedjl:gh-157605-sampling-empty-native-stack

Conversation

@aidaodedjl

@aidaodedjl aidaodedjl commented Sep 17, 2026

Copy link
Copy Markdown

Description

A native thread can retain a Python thread state while waiting in C code with no Python functions executing (e.g. after PyGILState_Ensure() + PyEval_SaveThread()). The frame chain of that thread state holds only the base_frame sentinel. process_frame_chain() treated the resulting NULL first frame as a broken chain, raised RuntimeError: Failed to parse initial frame in chain, and failed the entire sampling call — discarding valid stacks from all other threads.

With this change, the unparseable-first-frame error is raised only when the first frame is not the base_frame sentinel. When it is the sentinel, the walk stops and the empty Python stack is accepted, so sampling returns results for the remaining threads.

The walk stops at the sentinel, so last_frame_visited still equals base_frame_addr and the complete-stack validation in the frame cache keeps working for the empty-stack case.

Reproduction / evidence

  • Pre-fix (Linux, cross-process, issue reproducer): pin=1 (native thread retaining an empty-stack thread state) fails 40/40 samples with RuntimeError: Failed to parse initial frame in chain, with and without frame caching; pin=0 baseline succeeds 20/20.
  • Post-fix (same environment): all four pin x cache_frames combinations succeed 20/20 with the target thread's Python frames visible.

Test

test_external_inspection.py gains a self-contained regression test (TestEmptyPythonStackSampling):

  • compiles a small C helper at runtime that spawns a native thread retaining a Python thread state with an empty frame stack (loaded via ctypes);
  • asserts same-process sampling succeeds and the target thread's Python frames stay visible, across pin x cache_frames combinations;
  • skips gracefully when no C compiler is configured (including Windows, where CC is not set) and fails loudly if the helper itself fails to compile.

On this machine (macOS, 3.16 debug build) the test reproduces the failure before the fix ({'failure': 5}) and passes after (1.1 s).

…ain parser

A native thread can retain a Python thread state while waiting in C code
with no Python functions executing. The frame chain of that thread state
holds only the base_frame sentinel, and process_frame_chain() treated the
resulting NULL first frame as a broken chain, failing the entire sampling
call and discarding valid stacks from all other threads.

Only raise when the unparseable first frame is not the base_frame
sentinel; otherwise stop the walk and accept the empty stack. Walking
stops at the sentinel, so last_frame_visited still equals base_frame_addr
and complete-stack validation in the frame cache is preserved.

Tests: a self-contained regression test in test_external_inspection.py
compiles a small C helper at runtime (skipping when no compiler is
available), starts a native thread that retains a Python thread state
with an empty frame stack via ctypes, and asserts that same-process
sampling succeeds while other Python threads remain visible, with and
without frame caching.
sysconfig's install-style include paths do not necessarily exist for
build-tree interpreters: in an out-of-tree build the test's compile step
could not find Python.h (it lives in the source tree's Include directory
while pyconfig.h lives in the build directory), so the helper failed to
compile and the CI test run failed.

Gather candidate include directories from INCLUDEPY, sysconfig paths, the
executable directory and srcdir, and keep the ones that actually contain
Python.h or pyconfig.h. Skip cleanly when either header cannot be found.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant