Skip to content

Commit ff71b2a

Browse files
author
Saul Cooperman
committed
Support subinterpreters when reporting Python stacks
Iterate over all interpreters in the linked list when determining the PID offset, fixing an issue where pystack only inspected the first interpreter, which is not guaranteed to be the main interpreter. This ensures the correct TID is located even when multiple subinterpreters are present. Add support for subinterpreters in pure Python stack reporting. Threads running in subinterpreters are now detected and grouped by interpreter ID. Native stack reporting for subinterpreters is not yet supported. Signed-off-by: Saul Cooperman <saulcoops@gmail.com>
1 parent d6194db commit ff71b2a

23 files changed

+1009
-166
lines changed

Dockerfile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ RUN apt-get update \
6666
python3.13-dev \
6767
python3.13-dbg \
6868
python3.13-venv \
69+
python3.14-dev \
70+
python3.14-dbg \
71+
python3.14-venv \
6972
make \
7073
cmake \
7174
gdb \

news/279.bugfix.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Fix an issue where the PID offset could not be determined when multiple
2+
subinterpreters were present. Previously, pystack only checked the first
3+
interpreter in the linked list, which was not guaranteed to be the main
4+
interpreter. The fix now iterates over all interpreters and correctly locates
5+
the TID.

news/279.feature.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Add support for subinterpreters when reporting pure Python stacks. Threads
2+
running in subinterpreters are now identified and grouped by interpreter ID.
3+
Native stack reporting for subinterpreters is not yet supported.

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777
"src/pystack/_pystack.pyx",
7878
"src/pystack/_pystack/corefile.cpp",
7979
"src/pystack/_pystack/elf_common.cpp",
80+
"src/pystack/_pystack/interpreter.cpp",
8081
"src/pystack/_pystack/logging.cpp",
8182
"src/pystack/_pystack/mem.cpp",
8283
"src/pystack/_pystack/process.cpp",

src/pystack/__init__.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
from ._version import __version__
2-
from .traceback_formatter import print_thread
32

43
__all__ = [
54
"__version__",
6-
"print_thread",
75
]

src/pystack/__main__.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,13 @@
1919
from pystack.process import is_gzip
2020

2121
from . import errors
22-
from . import print_thread
2322
from .colors import colored
2423
from .engine import CoreFileAnalyzer
2524
from .engine import NativeReportingMode
2625
from .engine import StackMethod
2726
from .engine import get_process_threads
2827
from .engine import get_process_threads_for_core
28+
from .traceback_formatter import TracebackPrinter
2929

3030
PERMISSION_ERROR_MSG = "Operation not permitted"
3131
NO_SUCH_PROCESS_ERROR_MSG = "No such process"
@@ -285,14 +285,17 @@ def process_remote(parser: argparse.ArgumentParser, args: argparse.Namespace) ->
285285
if not args.block and args.native_mode != NativeReportingMode.OFF:
286286
parser.error("Native traces are only available in blocking mode")
287287

288+
printer = TracebackPrinter(
289+
native_mode=args.native_mode, include_subinterpreters=True
290+
)
288291
for thread in get_process_threads(
289292
args.pid,
290293
stop_process=args.block,
291294
native_mode=args.native_mode,
292295
locals=args.locals,
293296
method=StackMethod.ALL if args.exhaustive else StackMethod.AUTO,
294297
):
295-
print_thread(thread, args.native_mode)
298+
printer.print_thread(thread)
296299

297300

298301
def format_psinfo_information(psinfo: Dict[str, Any]) -> str:
@@ -414,6 +417,7 @@ def process_core(parser: argparse.ArgumentParser, args: argparse.Namespace) -> N
414417
elf_id if elf_id else "<MISSING>",
415418
)
416419

420+
printer = TracebackPrinter(args.native_mode, include_subinterpreters=True)
417421
for thread in get_process_threads_for_core(
418422
corefile,
419423
executable,
@@ -422,7 +426,7 @@ def process_core(parser: argparse.ArgumentParser, args: argparse.Namespace) -> N
422426
locals=args.locals,
423427
method=StackMethod.ALL if args.exhaustive else StackMethod.AUTO,
424428
):
425-
print_thread(thread, args.native_mode)
429+
printer.print_thread(thread)
426430

427431

428432
if __name__ == "__main__": # pragma: no cover

src/pystack/_pystack.pyx

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ from _pystack.elf_common cimport CoreFileAnalyzer as NativeCoreFileAnalyzer
2222
from _pystack.elf_common cimport ProcessAnalyzer as NativeProcessAnalyzer
2323
from _pystack.elf_common cimport SectionInfo
2424
from _pystack.elf_common cimport getSectionInfo
25+
from _pystack.interpreter cimport InterpreterUtils
2526
from _pystack.logging cimport initializePythonLoggerInterface
2627
from _pystack.mem cimport AbstractRemoteMemoryManager
2728
from _pystack.mem cimport MemoryMapInformation as CppMemoryMapInformation
@@ -462,6 +463,7 @@ cdef object _construct_threads_from_interpreter_state(
462463
bint add_native_traces,
463464
bint resolve_locals,
464465
):
466+
interpreter_id = InterpreterUtils.getInterpreterId(manager, head)
465467
LOGGER.info("Fetching Python threads")
466468
threads = []
467469

@@ -486,6 +488,7 @@ cdef object _construct_threads_from_interpreter_state(
486488
current_thread.isGilHolder(),
487489
current_thread.isGCCollecting(),
488490
python_version,
491+
interpreter_id,
489492
name=get_thread_name(pid, current_thread.Tid()),
490493
)
491494
)
@@ -622,7 +625,7 @@ def _get_process_threads(
622625
)
623626

624627
all_tids = list(manager.get().Tids())
625-
if head:
628+
while head:
626629
add_native_traces = native_mode != NativeReportingMode.OFF
627630
for thread in _construct_threads_from_interpreter_state(
628631
manager,
@@ -635,6 +638,7 @@ def _get_process_threads(
635638
if thread.tid in all_tids:
636639
all_tids.remove(thread.tid)
637640
yield thread
641+
head = InterpreterUtils.getNextInterpreter(manager, head)
638642

639643
if native_mode == NativeReportingMode.ALL:
640644
yield from _construct_os_threads(manager, pid, all_tids)
@@ -769,14 +773,20 @@ def _get_process_threads_for_core(
769773
770774
all_tids = list(manager.get().Tids())
771775
772-
if head:
773-
native = native_mode in {NativeReportingMode.PYTHON, NativeReportingMode.ALL}
776+
while head:
777+
add_native_traces = native_mode != NativeReportingMode.OFF
774778
for thread in _construct_threads_from_interpreter_state(
775-
manager, head, pymanager.pid, pymanager.python_version, native, locals
779+
manager,
780+
head,
781+
pymanager.pid,
782+
pymanager.python_version,
783+
add_native_traces,
784+
locals,
776785
):
777786
if thread.tid in all_tids:
778787
all_tids.remove(thread.tid)
779788
yield thread
789+
head = InterpreterUtils.getNextInterpreter(manager, head)
780790
781791
if native_mode == NativeReportingMode.ALL:
782792
yield from _construct_os_threads(manager, pymanager.pid, all_tids)

src/pystack/_pystack/CMakeLists.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ add_library(_pystack STATIC
2121
pythread.cpp
2222
version.cpp
2323
elf_common.cpp
24-
pytypes.cpp)
24+
pytypes.cpp
25+
interpreter.cpp)
2526
set_property(TARGET _pystack PROPERTY POSITION_INDEPENDENT_CODE ON)
2627
include_directories("." "cpython" ${PYTHON_INCLUDE_DIRS})

src/pystack/_pystack/cpython/interpreter.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -375,10 +375,10 @@ struct _gil_runtime_state
375375
int locked;
376376
unsigned long switch_number;
377377
pthread_cond_t cond;
378-
pthread_cond_t mutex;
378+
pthread_mutex_t mutex;
379379
#ifdef FORCE_SWITCHING
380380
pthread_cond_t switch_cond;
381-
pthread_cond_t switch_mutex;
381+
pthread_mutex_t switch_mutex;
382382
#endif
383383
};
384384

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
#include <memory>
2+
3+
#include "interpreter.h"
4+
#include "logging.h"
5+
#include "process.h"
6+
#include "structure.h"
7+
#include "version.h"
8+
9+
namespace pystack {
10+
11+
remote_addr_t
12+
InterpreterUtils::getNextInterpreter(
13+
const std::shared_ptr<const AbstractProcessManager>& manager,
14+
remote_addr_t interpreter_addr)
15+
{
16+
Structure<py_is_v> is(manager, interpreter_addr);
17+
return is.getField(&py_is_v::o_next);
18+
}
19+
20+
int64_t
21+
InterpreterUtils::getInterpreterId(
22+
const std::shared_ptr<const AbstractProcessManager>& manager,
23+
remote_addr_t interpreter_addr)
24+
{
25+
if (!manager->versionIsAtLeast(3, 7)) {
26+
// No support for subinterpreters so the only interpreter is ID 0.
27+
return 0;
28+
}
29+
30+
Structure<py_is_v> is(manager, interpreter_addr);
31+
int64_t id_value = is.getField(&py_is_v::o_id);
32+
33+
return id_value;
34+
}
35+
36+
} // namespace pystack

0 commit comments

Comments
 (0)