1+ #include < algorithm>
12#include < nanobind/nanobind.h>
23#include < nanobind/stl/filesystem.h>
34#include < nanobind/stl/function.h>
1112
1213#include < filesystem>
1314#include < memory>
15+ #include < numeric>
1416#include < optional>
17+ #include < ranges>
1518#include < unordered_set>
19+ #include < vector>
1620
1721#include " corefile.h"
1822#include " elf_common.h"
23+ #include " interpreter.h"
1924#include " logging.h"
2025#include " maps_parser.h"
2126#include " mem.h"
27+ #include " native_frame.h"
2228#include " process.h"
2329#include " thread_builder.h"
2430
@@ -490,6 +496,7 @@ buildPyThreadObject(
490496 native_frames,
491497 thread.gil_status ,
492498 thread.gc_status ,
499+ thread.interpreter_id ,
493500 nb::make_tuple (python_version.first , python_version.second ),
494501 " name" _a = thread.name ? nb::cast (*thread.name ) : nb::none ());
495502}
@@ -534,6 +541,107 @@ logMemoryMaps(const std::vector<pystack::VirtualMap>& maps, const char* source)
534541 }
535542}
536543
544+ std::vector<pystack::PyThreadData>
545+ _slice_native_stack (std::vector<pystack::PyThreadData> data)
546+ {
547+ // Capture a canonical
548+ auto canonical_thread =
549+ std::find_if (data.begin (), data.end (), [](const pystack::PyThreadData& py_thread_data) {
550+ return !py_thread_data.native_frames .empty ();
551+ });
552+ if (canonical_thread == data.end ()) {
553+ return data;
554+ }
555+
556+ // Capture canonical frames and python version
557+ const std::vector<pystack::NativeFrame> canonical_frames = canonical_thread->native_frames ;
558+ const auto python_version = data[0 ].python_version ;
559+
560+ std::vector<std::size_t > eval_index;
561+ for (std::size_t i = 0 ; i < canonical_frames.size (); ++i) {
562+ if (pystack::is_eval_frame (canonical_frames[i].symbol , python_version)) {
563+ eval_index.push_back (i);
564+ }
565+ }
566+
567+ const auto total_entry_frames = static_cast <std::size_t >(
568+ std::accumulate (data.begin (), data.end (), 0 , [](int acc, const pystack::PyThreadData& d) {
569+ return acc
570+ + static_cast <int >(std::count_if (
571+ d.frames .begin (),
572+ d.frames .end (),
573+ [](const pystack::PyFrameData& frame) { return frame.is_entry ; }));
574+ }));
575+
576+ if (eval_index.size () != total_entry_frames) {
577+ return data;
578+ }
579+
580+ std::vector<pystack::PyThreadData> ordered_threads = std::move (data);
581+ // Sort by:
582+ // 1. With stack anchor (!=0) before without
583+ // 2. Stack anchor in descending order
584+ // 3. Index in PyThreadData (handled by stable_sort)
585+ std::stable_sort (
586+ ordered_threads.begin (),
587+ ordered_threads.end (),
588+ [](const pystack::PyThreadData& a, const pystack::PyThreadData& b) {
589+ return std::make_tuple (a.stack_anchor == 0 ? 1 : 0 , -a.stack_anchor )
590+ < std::make_tuple (b.stack_anchor == 0 ? 1 : 0 , -b.stack_anchor );
591+ });
592+
593+ // Slice frames according to eval frames per python thread
594+ std::size_t cursor = 0 ;
595+ for (auto & thread_data : ordered_threads) {
596+ const auto required_eval_frames = static_cast <std::size_t >(std::count_if (
597+ thread_data.frames .begin (),
598+ thread_data.frames .end (),
599+ [](const pystack::PyFrameData& py_frame) { return py_frame.is_entry ; }));
600+
601+ if (required_eval_frames == 0 ) {
602+ continue ;
603+ }
604+
605+ const std::size_t end = cursor + required_eval_frames;
606+ const std::size_t from = eval_index[cursor];
607+ const std::size_t to = end < eval_index.size () ? eval_index[end] : canonical_frames.size ();
608+ thread_data.native_frames .assign (canonical_frames.begin () + from, canonical_frames.begin () + to);
609+ cursor = end;
610+ }
611+ return ordered_threads;
612+ }
613+
614+ std::vector<pystack::PyThreadData>
615+ _normalize_threads (std::vector<pystack::PyThreadData> threads, NativeReportingMode native_mode)
616+ {
617+ if (native_mode == NativeReportingMode::OFF) {
618+ return threads;
619+ }
620+
621+ // First pass: bucket threads by TID (capture index only)
622+ std::unordered_map<int , std::vector<std::size_t >> indices_by_tid;
623+ for (std::size_t i = 0 ; i < threads.size (); ++i) {
624+ indices_by_tid[threads[i].tid ].push_back (i);
625+ }
626+
627+ // Second pass: for groups that share a TID, slice native stacks.
628+ for (auto & [_, indices] : indices_by_tid) {
629+ if (indices.size () <= 1 ) {
630+ continue ;
631+ }
632+ std::vector<pystack::PyThreadData> group;
633+ for (const std::size_t idx : indices) {
634+ group.push_back (std::move (threads[idx]));
635+ }
636+ auto sliced = _slice_native_stack (std::move (group));
637+ for (std::size_t i = 0 ; i < indices.size (); ++i) {
638+ threads[indices[i]] = std::move (sliced[i]);
639+ }
640+ }
641+
642+ return threads;
643+ }
644+
537645nb::object
538646get_process_threads (
539647 pid_t pid,
@@ -571,21 +679,28 @@ get_process_threads(
571679 } else {
572680 python_version = manager->python_version ();
573681 std::vector<int > all_tids = pystack::getThreadIds (manager->get_manager ());
574-
575- if (head != 0 ) {
576- bool add_native = native_mode != NativeReportingMode::OFF;
577- python_threads = pystack::buildThreadsFromInterpreter (
578- manager->get_manager (),
579- head,
580- pid,
581- add_native,
582- locals);
583-
584- for (const auto & thread : python_threads) {
682+ bool add_native = native_mode != NativeReportingMode::OFF;
683+
684+ while (head) {
685+ std::vector<pystack::PyThreadData> new_threads =
686+ pystack::buildThreadsFromInterpreter (
687+ manager->get_manager (),
688+ head,
689+ pid,
690+ add_native,
691+ locals);
692+
693+ for (const auto & thread : new_threads) {
585694 all_tids.erase (
586695 std::remove (all_tids.begin (), all_tids.end (), thread.tid ),
587696 all_tids.end ());
588697 }
698+ python_threads.insert (
699+ python_threads.end (),
700+ std::make_move_iterator (new_threads.begin ()),
701+ std::make_move_iterator (new_threads.end ()));
702+
703+ head = pystack::InterpreterUtils::getNextInterpreter (manager->get_manager (), head);
589704 }
590705
591706 if (native_mode == NativeReportingMode::ALL) {
@@ -606,7 +721,7 @@ get_process_threads(
606721 }
607722
608723 nb::list result;
609- for (const auto & thread : python_threads) {
724+ for (const auto & thread : _normalize_threads ( python_threads, native_mode) ) {
610725 result.append (buildPyThreadObject (thread, types, python_version));
611726 }
612727 for (const auto & thread : native_only_threads) {
@@ -651,11 +766,11 @@ get_process_threads_for_core(
651766 }
652767
653768 nb::list result;
769+ std::vector<pystack::PyThreadData> ret_cpp;
654770 std::vector<int > all_tids = pystack::getThreadIds (manager->get_manager ());
771+ bool add_native = native_mode != NativeReportingMode::OFF;
655772
656- if (head != 0 ) {
657- bool add_native = native_mode == NativeReportingMode::PYTHON
658- || native_mode == NativeReportingMode::ALL;
773+ while (head) {
659774 auto threads = pystack::buildThreadsFromInterpreter (
660775 manager->get_manager (),
661776 head,
@@ -664,11 +779,16 @@ get_process_threads_for_core(
664779 locals);
665780
666781 for (const auto & thread : threads) {
667- result.append (buildPyThreadObject (thread, types, manager->python_version ()));
668782 all_tids.erase (
669783 std::remove (all_tids.begin (), all_tids.end (), thread.tid ),
670784 all_tids.end ());
671785 }
786+ ret_cpp.insert (
787+ ret_cpp.end (),
788+ std::make_move_iterator (threads.begin ()),
789+ std::make_move_iterator (threads.end ()));
790+
791+ head = pystack::InterpreterUtils::getNextInterpreter (manager->get_manager (), head);
672792 }
673793
674794 if (native_mode == NativeReportingMode::ALL) {
@@ -678,6 +798,10 @@ get_process_threads_for_core(
678798 }
679799 }
680800
801+ for (const auto & thread : _normalize_threads (ret_cpp, native_mode)) {
802+ result.append (buildPyThreadObject (thread, types, manager->python_version ()));
803+ }
804+
681805 return result;
682806 } catch (const NotEnoughInformationError&) {
683807 throw ;
@@ -863,4 +987,26 @@ NB_MODULE(_pystack, m)
863987 // intercept_runtime_errors decorator - re-export from pystack.errors
864988 nb::module_ pystack_errors = nb::module_::import_ (" pystack.errors" );
865989 m.attr (" intercept_runtime_errors" ) = pystack_errors.attr (" intercept_runtime_errors" );
990+
991+ nb::enum_<pystack::NativeFrame::FrameType>(m, " NativeFrameType" )
992+ .value (" IGNORE" , pystack::NativeFrame::FrameType::IGNORE)
993+ .value (" EVAL" , pystack::NativeFrame::FrameType::EVAL)
994+ .value (" OTHER" , pystack::NativeFrame::FrameType::OTHER);
995+
996+ m.def (" is_eval_frame" ,
997+ &pystack::is_eval_frame,
998+ " symbol" _a,
999+ " python_version" _a,
1000+ " Return True if the symbol is a CPython eval frame function" );
1001+
1002+ m.def (
1003+ " frame_type" ,
1004+ [](const std::string& symbol, std::optional<std::pair<int , int >> python_version) {
1005+ pystack::NativeFrame frame{};
1006+ frame.symbol = symbol;
1007+ return pystack::frame_type (frame, python_version);
1008+ },
1009+ " symbol" _a,
1010+ " python_version" _a = nb::none (),
1011+ " Return the FrameType for a native frame symbol" );
8661012}
0 commit comments