Repository navigation
profiling.sampling incorrectly handles base_frame and native frame as the first frame #157605
Description
Bug description
profiling.sampling does not correctly handle some frame chains whose first frame is not a Python frame.
There are two cases:
-
The first frame is
base_frameA native thread can retain a Python thread state while waiting in C code, with no Python functions executing. Its frame chain contains only the
base_framesentinel.The sampler skips this sentinel and then raises:
RuntimeError: Failed to parse initial frame in chain -
The first frame is a native frame
When the first frame in the chain is a native frame, the sampler does not correctly handle it as the initial frame and may report the frame chain as invalid instead of adding a
<native>frame.
In both cases, the failure can cause the entire sampling call to fail, so valid Python stacks from other threads are also lost.
Reproduction
Case 1: base_frame as the first frame
Create a pthread that registers and detaches a Python thread state:
PyGILState_STATE state = PyGILState_Ensure();
PyThreadState *saved = PyEval_SaveThread();
/* Wait on a pthread condition variable while the parent samples. */
/* Cleanup after sampling. */
PyEval_RestoreThread(saved);
PyGILState_Release(state);Keep the main thread inside a Python function and sample the child process from its parent:
unwinder = _remote_debugging.RemoteUnwinder(
pid, all_threads=True, cache_frames=False,
)
unwinder.get_stack_trace()tachyon-empty-native-repro.zip
- Reproduces with frame caching both enabled and disabled.
Case 2: Native frame as the first frame
Construct a frame chain whose current frame is a native frame and whose previous frame leads to base_frame.
When native-frame collection is enabled, sampling this thread currently fails to correctly handle the native frame as the first frame.
Expected behavior
The sampler should correctly handle both cases:
- If the first frame is
base_frame, treat it as a valid empty Python stack. - If the first frame is a native frame, add a
<native>frame to the returned stack and continue walking the frame chain.
A special frame state in one thread should not cause the entire sampling operation to fail or prevent valid stacks from other threads from being returned.
Suspected cause
process_frame_chain() does not correctly handle cases where the first frame does not contain a parseable Python frame, including base_frame and native frames.
These cases can be incorrectly treated as a broken initial frame chain.
Environment
Linux, CPython 3.15.0rc2+dev free-threaded build and current main.
Activity
pythongh-157605: Fix regression test include dirs for out-of-tree builds
pythongh-157605: Handle a native first frame like a mid-chain native …
pythongh-157605: Cover native-first frames with a regression test
pythongh-157605: Discriminate code-less frames by owner field
pythongh-157605: Skip dropped frames and keep walking the live caller…
[-]profiling.sampling fails when a native thread has an empty Python stack[/-][+]profiling.sampling incorrectly handles base_frame and native frame as the first frame[/+]liuzhijie-0614 commentedon Sep 19, 2026
I've updated the issue description to clarify the two cases I've identified:
base_frame, which should be treated as a valid empty Python stack;<native>entry when native frame collection is enabled.Since gh-157657 was closed, I'd like to confirm the preferred direction for this issue.
@pablogsal , would it make sense to continue investigating these cases and prepare a fresh PR with focused regression tests, or would you prefer a different approach here? Thanks!
pablogsal commentedon Sep 19, 2026
Was closed as it violates https://devguide.python.org/getting-started/ai-tools/. Since this issue was also AI generated I don't trust right now more PRs for it. I will probably take of it myself.
liuzhijie-0614 commentedon Sep 19, 2026
Alright, this is a real issue discovered in a production environment, and we genuinely want to resolve it. All examples come from actual environments.
pablogsal commentedon Sep 19, 2026
Thanks for letting me know
pablogsal commentedon Sep 19, 2026
Also, what examples are you talking about? There is no "examples|
liuzhijie-0614 commentedon Sep 19, 2026
this case:
I have created a base frame case locally. The native frame case was discovered during the analysis of the stack, but it hasn't been constructed yet. I am currently attempting to do so.
tachyon-empty-native-repro.zip
pythongh-157605: Accept empty native-thread stacks when sampling
liuzhijie-0614 commentedon Sep 20, 2026
@pablogsal Thanks for the fix! This looks like it resolves the issue.
While testing this PR, I noticed that a cleared Python frame could also be mistaken for a native frame. In my test run, about 0.288% of samples contained an unexpected frame.
This seems like a separate issue. With the initial-frame check removed, this case now produces a frame instead of an error. Do you have any suggestions on how we could handle it? I’ve included a small reproducer below.
flamegraph.html
liuzhijie-0614 commentedon Sep 20, 2026
Both a real first frame and a cleared frame incorrectly recognized as seem related to frame cleanup:
tstate->current_framehas already moved to the caller._PyEval_FrameClearAndPop()clears the code pointer, but previous can still be non-NULL, so the frame is mistakenly recognized as a frame.pythongh-157605: Accept empty native-thread stacks when sampling
pythongh-157605: Accept empty native-thread stacks when sampling
pythongh-157605: Distinguish C frames from cleared Python frames
pythongh-157605: Distinguish C frames from cleared Python frames
pythongh-157605: Distinguish C frames from cleared Python frames
gh-157605: Accept empty native-thread stacks when sampling