Skip to content

Commit 17efadc

Browse files
committed
gh-159312: Resolve CPU attribution from collected stacks
1 parent 35b3947 commit 17efadc

4 files changed

Lines changed: 83 additions & 65 deletions

File tree

‎Lib/test/test_external_inspection.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
THREAD_STATUS_ON_CPU = 1 << 1
3737
THREAD_STATUS_UNKNOWN = 1 << 2
3838
THREAD_STATUS_HAS_EXCEPTION = 1 << 4
39+
THREAD_STATUS_MAIN_THREAD = 1 << 5
3940

4041
# Maximum number of retry attempts for operations that may fail transiently
4142
MAX_TRIES = 10
@@ -2627,7 +2628,7 @@ def suspended():
26272628
suspended = self._find_thread_with_frame(trace, lambda f: f.funcname == "suspended")
26282629
self.assertIsNotNone(suspended)
26292630
self.assertEqual(suspended.thread_id, active.thread_id)
2630-
self.assertFalse(suspended.status & THREAD_STATUS_ON_CPU)
2631+
self.assertEqual(suspended.status, THREAD_STATUS_MAIN_THREAD)
26312632
break
26322633

26332634
def _run_thread_status_test(self, mode, check_condition):

‎Modules/_remote_debugging/_remote_debugging.h‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,9 @@ typedef enum _WIN32_THREADSTATE {
179179
#define THREAD_STATUS_HAS_EXCEPTION (1 << 4)
180180
#define THREAD_STATUS_MAIN_THREAD (1 << 5)
181181

182+
// Internal marker, removed before returning the sample.
183+
#define THREAD_STATUS_UNBOUND (1 << 6)
184+
182185
/* Exception cause macro */
183186
#define set_exception_cause(unwinder, exc_type, message) \
184187
do { \
@@ -686,7 +689,7 @@ extern int get_thread_status(RemoteUnwinderObject *unwinder, uint64_t tid, uint6
686689
extern PyObject* unwind_stack_for_thread(
687690
RemoteUnwinderObject *unwinder,
688691
uintptr_t *current_tstate,
689-
uintptr_t gil_holder_tstate,
692+
int *has_unbound,
690693
uintptr_t gc_frame,
691694
uintptr_t main_thread_tstate,
692695
const RemoteReadPrefetch *prefetch

‎Modules/_remote_debugging/module.c‎

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -650,6 +650,66 @@ read_interp_state_and_maybe_thread_frame(
650650
interp_state_buffer);
651651
}
652652

653+
static int
654+
filter_unbound_cpu_samples(PyObject *result, int skip)
655+
{
656+
PyObject *bound = PySet_New(NULL);
657+
if (bound == NULL) {
658+
return -1;
659+
}
660+
// An unbound stack may still be doing native work after a callback.
661+
// Exclude it only when a bound stack shares its native thread ID.
662+
for (Py_ssize_t i = 0; i < PyList_GET_SIZE(result); i++) {
663+
PyObject *threads = PyStructSequence_GET_ITEM(PyList_GET_ITEM(result, i), 1);
664+
for (Py_ssize_t j = 0; j < PyList_GET_SIZE(threads); j++) {
665+
PyObject *thread = PyList_GET_ITEM(threads, j);
666+
long status = PyLong_AsLong(PyStructSequence_GET_ITEM(thread, 1));
667+
if (!(status & THREAD_STATUS_UNBOUND) &&
668+
PyList_GET_SIZE(PyStructSequence_GET_ITEM(thread, 2)) != 0 &&
669+
PySet_Add(bound, PyStructSequence_GET_ITEM(thread, 0)) < 0) {
670+
goto error;
671+
}
672+
}
673+
}
674+
for (Py_ssize_t i = 0; i < PyList_GET_SIZE(result); i++) {
675+
PyObject *threads = PyStructSequence_GET_ITEM(PyList_GET_ITEM(result, i), 1);
676+
for (Py_ssize_t j = PyList_GET_SIZE(threads); j-- > 0;) {
677+
PyObject *thread = PyList_GET_ITEM(threads, j);
678+
long status = PyLong_AsLong(PyStructSequence_GET_ITEM(thread, 1));
679+
if (!(status & THREAD_STATUS_UNBOUND)) {
680+
continue;
681+
}
682+
status &= ~THREAD_STATUS_UNBOUND;
683+
int found = PySet_Contains(bound, PyStructSequence_GET_ITEM(thread, 0));
684+
if (found < 0) {
685+
goto error;
686+
}
687+
if (found) {
688+
status &= ~THREAD_STATUS_ON_CPU;
689+
}
690+
if (skip && !(status & THREAD_STATUS_ON_CPU)) {
691+
if (PySequence_DelItem(threads, j) < 0) {
692+
goto error;
693+
}
694+
}
695+
else {
696+
PyObject *py_status = PyLong_FromLong(status);
697+
if (py_status == NULL) {
698+
goto error;
699+
}
700+
Py_DECREF(PyStructSequence_GET_ITEM(thread, 1));
701+
PyStructSequence_SET_ITEM(thread, 1, py_status);
702+
}
703+
}
704+
}
705+
Py_DECREF(bound);
706+
return 0;
707+
708+
error:
709+
Py_DECREF(bound);
710+
return -1;
711+
}
712+
653713
/*[clinic input]
654714
@critical_section
655715
_remote_debugging.RemoteUnwinder.get_stack_trace
@@ -715,6 +775,8 @@ _remote_debugging_RemoteUnwinder_get_stack_trace_impl(RemoteUnwinderObject *self
715775
return NULL;
716776
}
717777

778+
int has_unbound = 0;
779+
718780
// Iterate over all interpreters
719781
uintptr_t current_interpreter = self->interpreter_addr;
720782
while (current_interpreter != 0) {
@@ -803,7 +865,7 @@ _remote_debugging_RemoteUnwinder_get_stack_trace_impl(RemoteUnwinderObject *self
803865
while (current_tstate != 0) {
804866
uintptr_t prev_tstate = current_tstate;
805867
PyObject* frame_info = unwind_stack_for_thread(self, &current_tstate,
806-
gil_holder_tstate,
868+
&has_unbound,
807869
gc_frame,
808870
main_thread_tstate,
809871
&prefetch);
@@ -890,6 +952,11 @@ _remote_debugging_RemoteUnwinder_get_stack_trace_impl(RemoteUnwinderObject *self
890952
}
891953
}
892954

955+
if (has_unbound && filter_unbound_cpu_samples(result,
956+
self->mode == PROFILING_MODE_CPU && self->skip_non_matching_threads) < 0) {
957+
Py_CLEAR(result);
958+
}
959+
893960
exit:
894961
// Invalidate cache entries for threads not seen in this sample.
895962
// Only do this every 1024 iterations to avoid performance overhead.

‎Modules/_remote_debugging/threads.c‎

Lines changed: 9 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -293,62 +293,6 @@ typedef struct {
293293
unsigned int :24;
294294
} _thread_status;
295295

296-
static int
297-
has_bound_stack(RemoteUnwinderObject *unwinder, long tid)
298-
{
299-
uintptr_t interp, tstate;
300-
int remaining = 8192;
301-
if (read_ptr(unwinder, unwinder->runtime_start_address +
302-
unwinder->debug_offsets.runtime_state.interpreters_head, &interp) < 0) {
303-
goto unavailable;
304-
}
305-
while (interp && remaining-- > 0) {
306-
if (read_ptr(unwinder, interp +
307-
unwinder->debug_offsets.interpreter_state.threads_head, &tstate) < 0) {
308-
goto unavailable;
309-
}
310-
while (tstate && remaining-- > 0) {
311-
char ts[SIZEOF_THREAD_STATE];
312-
if (_Py_RemoteDebug_ReadRemoteMemory(&unwinder->handle, tstate,
313-
unwinder->debug_offsets.thread_state.size, ts) < 0) {
314-
goto unavailable;
315-
}
316-
if (GET_MEMBER(long, ts, unwinder->debug_offsets.thread_state.native_thread_id) == tid &&
317-
GET_MEMBER(_thread_status, ts, unwinder->debug_offsets.thread_state.status).bound_gilstate) {
318-
uintptr_t frame = GET_MEMBER(uintptr_t, ts, unwinder->debug_offsets.thread_state.current_frame);
319-
uintptr_t base = GET_MEMBER(uintptr_t, ts, unwinder->debug_offsets.thread_state.base_frame);
320-
while (frame && frame != base && remaining-- > 0) {
321-
char data[SIZEOF_INTERP_FRAME];
322-
if (_Py_RemoteDebug_ReadRemoteMemory(&unwinder->handle, frame,
323-
sizeof(data), data) < 0) {
324-
goto unavailable;
325-
}
326-
int valid = is_frame_valid(unwinder, (uintptr_t)data,
327-
GET_MEMBER_NO_TAG(uintptr_t, data, unwinder->debug_offsets.interpreter_frame.executable));
328-
if (valid < 0) {
329-
goto unavailable;
330-
}
331-
if (valid == 1) {
332-
return 1;
333-
}
334-
frame = GET_MEMBER(uintptr_t, data, unwinder->debug_offsets.interpreter_frame.previous);
335-
}
336-
}
337-
tstate = GET_MEMBER(uintptr_t, ts, unwinder->debug_offsets.thread_state.next);
338-
}
339-
if (read_ptr(unwinder, interp +
340-
unwinder->debug_offsets.interpreter_state.next, &interp) < 0) {
341-
goto unavailable;
342-
}
343-
}
344-
return 0;
345-
346-
unavailable:
347-
// Keep the sample when remote state changes during the lookup.
348-
PyErr_Clear();
349-
return 0;
350-
}
351-
352296
static int
353297
read_thread_state_and_maybe_frame(
354298
RemoteUnwinderObject *unwinder,
@@ -388,7 +332,7 @@ PyObject*
388332
unwind_stack_for_thread(
389333
RemoteUnwinderObject *unwinder,
390334
uintptr_t *current_tstate,
391-
uintptr_t gil_holder_tstate,
335+
int *has_unbound,
392336
uintptr_t gc_frame,
393337
uintptr_t main_thread_tstate,
394338
const RemoteReadPrefetch *prefetch
@@ -557,11 +501,7 @@ unwind_stack_for_thread(
557501

558502
if (cpu_status == THREAD_STATE_UNKNOWN) {
559503
status_flags |= THREAD_STATUS_UNKNOWN;
560-
} else if (cpu_status == THREAD_STATE_RUNNING &&
561-
(GET_MEMBER(_thread_status, ts, unwinder->debug_offsets.thread_state.status).bound_gilstate ||
562-
!has_bound_stack(unwinder, tid))) {
563-
// An unbound state may still be doing native work after a callback.
564-
// Exclude it only if another bound stack shares the same OS thread.
504+
} else if (cpu_status == THREAD_STATE_RUNNING) {
565505
status_flags |= THREAD_STATUS_ON_CPU;
566506
}
567507

@@ -671,6 +611,13 @@ unwind_stack_for_thread(
671611
goto error;
672612
}
673613

614+
if ((unwinder->mode == PROFILING_MODE_CPU || unwinder->mode == PROFILING_MODE_ALL) &&
615+
!GET_MEMBER(_thread_status, ts, unwinder->debug_offsets.thread_state.status).bound_gilstate &&
616+
PyList_GET_SIZE(frame_info) != 0) {
617+
status_flags |= THREAD_STATUS_UNBOUND;
618+
*has_unbound = 1;
619+
}
620+
674621
// Always use status_flags
675622
PyObject *py_status = PyLong_FromLong(status_flags);
676623
if (py_status == NULL) {

0 commit comments

Comments
 (0)