@@ -411,236 +411,167 @@ ThreadInfo::unwind_tasks(EchionSampler& echion, PyThreadState* tstate, microseco
411411// ----------------------------------------------------------------------------
412412#if PY_VERSION_HEX >= 0x030e0000
413413Result<void >
414- ThreadInfo::get_tasks_from_thread_linked_list (EchionSampler& echion, std::vector<TaskInfo::Ptr >& tasks)
414+ ThreadInfo::get_task_addresses_from_thread_linked_list ( std::vector<TaskObj* >& tasks)
415415{
416416 if (this ->tstate_addr == 0 || this ->asyncio_loop == 0 ) {
417417 return ErrorKind::TaskInfoError;
418418 }
419419
420- // Calculate thread state's asyncio_tasks_head remote address
421- // Note: Since 3.13+, every PyThreadState is actually allocated as a _PyThreadStateImpl.
422- // We use PyThreadState* everywhere and cast to _PyThreadStateImpl* only when we need
423- // to access asyncio_tasks_head (which is only available in Python 3.14+).
424- // Since tstate_addr is a remote address, we calculate the offset and add it to the address.
425- // get_tasks_from_linked_list will handle copying the head node from remote memory internally.
426420 constexpr size_t asyncio_tasks_head_offset = offsetof (_PyThreadStateImpl, asyncio_tasks_head);
427- uintptr_t head_addr = this ->tstate_addr + asyncio_tasks_head_offset;
428-
429- return get_tasks_from_linked_list (echion, head_addr, tasks);
421+ return get_task_addresses_from_linked_list (this ->tstate_addr + asyncio_tasks_head_offset, tasks);
430422}
431423
432424Result<void >
433- ThreadInfo::get_tasks_from_interpreter_linked_list (EchionSampler& echion,
434- PyThreadState* tstate,
435- std::vector<TaskInfo::Ptr>& tasks)
425+ ThreadInfo::get_task_addresses_from_interpreter_linked_list (PyThreadState* tstate, std::vector<TaskObj*>& tasks)
436426{
437427 if (tstate == nullptr || tstate->interp == nullptr || this ->asyncio_loop == 0 ) {
438428 return ErrorKind::TaskInfoError;
439429 }
440430
441431 constexpr size_t asyncio_tasks_head_offset = offsetof (PyInterpreterState, asyncio_tasks_head);
442- uintptr_t head_addr = reinterpret_cast <uintptr_t >(tstate->interp ) + asyncio_tasks_head_offset;
443-
444- return get_tasks_from_linked_list (echion, head_addr, tasks);
432+ const uintptr_t head_addr = reinterpret_cast <uintptr_t >(tstate->interp ) + asyncio_tasks_head_offset;
433+ return get_task_addresses_from_linked_list (head_addr, tasks);
445434}
446435
447436Result<void >
448- ThreadInfo::get_tasks_from_linked_list (EchionSampler& echion, uintptr_t head_addr, std::vector<TaskInfo::Ptr >& tasks)
437+ ThreadInfo::get_task_addresses_from_linked_list ( uintptr_t head_addr, std::vector<TaskObj* >& tasks)
449438{
450439 if (head_addr == 0 || this ->asyncio_loop == 0 ) {
451440 return ErrorKind::TaskInfoError;
452441 }
453442
454- // Copy head node struct from remote memory to local memory
455- struct llist_node head_node_local;
456- if (copy_type (reinterpret_cast <void *>(head_addr), head_node_local)) {
443+ struct llist_node current_node;
444+ if (copy_type (reinterpret_cast <void *>(head_addr), current_node)) {
457445 return ErrorKind::TaskInfoError;
458446 }
459447
460- // Check if list is empty (head points to itself in circular list)
461- uintptr_t head_addr_uint = head_addr;
462- uintptr_t next_as_uint = reinterpret_cast <uintptr_t >(head_node_local.next );
463- uintptr_t prev_as_uint = reinterpret_cast <uintptr_t >(head_node_local.prev );
464- if (next_as_uint == head_addr_uint && prev_as_uint == head_addr_uint) {
448+ const uintptr_t head_addr_uint = head_addr;
449+ if (reinterpret_cast <uintptr_t >(current_node.next ) == head_addr_uint &&
450+ reinterpret_cast <uintptr_t >(current_node.prev ) == head_addr_uint) {
465451 return Result<void >::ok ();
466452 }
467453
468- struct llist_node current_node = head_node_local; // Start with head node
469-
470- // Copied from CPython's _remote_debugging_module.c: MAX_ITERATIONS
471- const size_t MAX_ITERATIONS = 1 << 16 ;
454+ constexpr size_t max_iterations = 1 << 16 ;
472455 size_t iteration_count = 0 ;
473-
474- // Iterate over linked-list. The linked list is circular, so we stop
475- // when we're back at head.
476456 while (reinterpret_cast <uintptr_t >(current_node.next ) != head_addr_uint) {
477- // Safety: prevent infinite loops
478- if (++iteration_count > MAX_ITERATIONS ) {
457+ if (++iteration_count > max_iterations || current_node.next == nullptr ) {
479458 return ErrorKind::TaskInfoError;
480459 }
481460
482- if (current_node.next == nullptr ) {
483- return ErrorKind::TaskInfoError; // nullptr pointer - invalid list
484- }
485-
486- uintptr_t next_node_addr = reinterpret_cast <uintptr_t >(current_node.next );
461+ const uintptr_t next_node_addr = reinterpret_cast <uintptr_t >(current_node.next );
462+ const uintptr_t task_addr = next_node_addr - offsetof (TaskObj, task_node);
463+ tasks.push_back (reinterpret_cast <TaskObj*>(task_addr));
487464
488- // Calculate task_addr from current_node.next
489- size_t task_node_offset_val = offsetof (TaskObj, task_node);
490- uintptr_t task_addr_uint = next_node_addr - task_node_offset_val;
491-
492- // Create TaskInfo for the task
493- auto maybe_task_info = TaskInfo::create (echion, reinterpret_cast <TaskObj*>(task_addr_uint));
494- if (maybe_task_info) {
495- auto & task_info = *maybe_task_info;
496- if (task_info->loop == reinterpret_cast <PyObject*>(this ->asyncio_loop )) {
497- tasks.push_back (std::move (task_info));
498- }
499- }
500-
501- // Read next node from current_node.next into current_node
502465 if (copy_type (reinterpret_cast <void *>(next_node_addr), current_node)) {
503- return ErrorKind::TaskInfoError; // Failed to read next node
466+ return ErrorKind::TaskInfoError;
504467 }
505468 }
506469
507470 return Result<void >::ok ();
508471}
509472
510- Result<std::vector<TaskInfo::Ptr >>
511- ThreadInfo::get_all_tasks (EchionSampler& echion, PyThreadState* tstate)
473+ Result<std::vector<TaskObj* >>
474+ ThreadInfo::get_all_task_addresses (EchionSampler& echion, PyThreadState* tstate)
512475{
513- std::vector<TaskInfo::Ptr > tasks;
514- if (this ->asyncio_loop == 0 )
476+ std::vector<TaskObj* > tasks;
477+ if (this ->asyncio_loop == 0 ) {
515478 return tasks;
479+ }
516480
517- // Python 3.14+: Native tasks are in linked-list per thread AND per interpreter
518- // CPython iterates over both:
519- // 1. Per-thread list: tstate->asyncio_tasks_head (active tasks)
520- // 2. Per-interpreter list: interp->asyncio_tasks_head (lingering tasks)
521- // First, get tasks from this thread's linked-list (if tstate_addr is set)
522- // Note: We continue processing even if one source fails to maximize partial results
481+ // Native tasks can appear in both lists. Consumers already tolerate duplicate task objects.
523482 if (tstate != nullptr && this ->tstate_addr != 0 ) {
524- (void )get_tasks_from_thread_linked_list (echion, tasks);
525-
526- // Second, get tasks from interpreter's linked-list (lingering tasks)
527- (void )get_tasks_from_interpreter_linked_list (echion, tstate, tasks);
528- }
529-
530- // Handle third-party tasks from Python _scheduled_tasks WeakSet
531- // In Python 3.14+, _scheduled_tasks is a Python-level weakref.WeakSet() that only contains
532- // tasks that don't inherit from asyncio.Task. Native asyncio.Task instances are stored
533- // in linked-lists (handled above) and are NOT added to _scheduled_tasks.
534- // This is typically empty in practice, but we handle it for completeness.
535- auto asyncio_scheduled_tasks = echion.asyncio_scheduled_tasks ();
536- if (asyncio_scheduled_tasks != nullptr ) {
537- if (auto maybe_scheduled_tasks_set = MirrorSet::create (asyncio_scheduled_tasks)) {
538- auto scheduled_tasks_set = std::move (*maybe_scheduled_tasks_set);
539- if (auto maybe_scheduled_tasks = scheduled_tasks_set.as_unordered_set ()) {
540- auto scheduled_tasks = std::move (*maybe_scheduled_tasks);
541- for (auto task_addr : scheduled_tasks) {
542- // In WeakSet.data (set), elements are the Task objects themselves
543- auto maybe_task_info = TaskInfo::create (echion, reinterpret_cast <TaskObj*>(task_addr));
544- if (maybe_task_info &&
545- (*maybe_task_info)->loop == reinterpret_cast <PyObject*>(this ->asyncio_loop )) {
546- tasks.push_back (std::move (*maybe_task_info));
547- }
483+ (void )get_task_addresses_from_thread_linked_list (tasks);
484+ (void )get_task_addresses_from_interpreter_linked_list (tstate, tasks);
485+ }
486+
487+ // Python 3.14 stores only third-party Task implementations in _scheduled_tasks.
488+ if (auto scheduled = echion.asyncio_scheduled_tasks (); scheduled != nullptr ) {
489+ if (auto maybe_set = MirrorSet::create (scheduled)) {
490+ if (auto maybe_tasks = maybe_set->as_unordered_set ()) {
491+ for (auto task : *maybe_tasks) {
492+ tasks.push_back (reinterpret_cast <TaskObj*>(task));
548493 }
549494 }
550495 }
551496 }
552497
553- auto asyncio_eager_tasks = echion.asyncio_eager_tasks ();
554- if (asyncio_eager_tasks != nullptr ) {
555- auto maybe_eager_tasks_set = MirrorSet::create (asyncio_eager_tasks);
556- if (!maybe_eager_tasks_set) {
498+ if (auto eager = echion.asyncio_eager_tasks (); eager != nullptr ) {
499+ auto maybe_set = MirrorSet::create (eager);
500+ if (!maybe_set) {
557501 return ErrorKind::TaskInfoError;
558502 }
559-
560- auto eager_tasks_set = std::move (*maybe_eager_tasks_set);
561-
562- auto maybe_eager_tasks = eager_tasks_set.as_unordered_set ();
563- if (!maybe_eager_tasks) {
503+ auto maybe_tasks = maybe_set->as_unordered_set ();
504+ if (!maybe_tasks) {
564505 return ErrorKind::TaskInfoError;
565506 }
566-
567- auto eager_tasks = std::move (*maybe_eager_tasks);
568- for (auto task_addr : eager_tasks) {
569- auto maybe_task_info = TaskInfo::create (echion, reinterpret_cast <TaskObj*>(task_addr));
570- if (maybe_task_info) {
571- if ((*maybe_task_info)->loop == reinterpret_cast <PyObject*>(this ->asyncio_loop )) {
572- tasks.push_back (std::move (*maybe_task_info));
573- }
574- }
507+ for (auto task : *maybe_tasks) {
508+ tasks.push_back (reinterpret_cast <TaskObj*>(task));
575509 }
576510 }
577511
578512 return tasks;
579513}
580514#else
581- // Pre-Python 3.14: get_all_tasks uses WeakSet approach
582- Result<std::vector<TaskInfo::Ptr>>
583- ThreadInfo::get_all_tasks (EchionSampler& echion, PyThreadState*)
515+ Result<std::vector<TaskObj*>>
516+ ThreadInfo::get_all_task_addresses (EchionSampler& echion, PyThreadState*)
584517{
585- std::vector<TaskInfo::Ptr > tasks;
586- if (this ->asyncio_loop == 0 )
518+ std::vector<TaskObj* > tasks;
519+ if (this ->asyncio_loop == 0 ) {
587520 return tasks;
521+ }
588522
589- auto asyncio_scheduled_tasks = echion.asyncio_scheduled_tasks ();
590- auto maybe_scheduled_tasks_set = MirrorSet::create (asyncio_scheduled_tasks);
591- if (!maybe_scheduled_tasks_set) {
523+ auto maybe_set = MirrorSet::create (echion.asyncio_scheduled_tasks ());
524+ if (!maybe_set) {
592525 return ErrorKind::TaskInfoError;
593526 }
594-
595- auto scheduled_tasks_set = std::move (*maybe_scheduled_tasks_set);
596- auto maybe_scheduled_tasks = scheduled_tasks_set.as_unordered_set ();
597- if (!maybe_scheduled_tasks) {
527+ auto maybe_scheduled = maybe_set->as_unordered_set ();
528+ if (!maybe_scheduled) {
598529 return ErrorKind::TaskInfoError;
599530 }
600-
601- auto scheduled_tasks = std::move (*maybe_scheduled_tasks);
602- for (auto task_wr_addr : scheduled_tasks) {
603- PyWeakReference task_wr;
604- if (copy_type (task_wr_addr, task_wr))
605- continue ;
606-
607- auto maybe_task_info = TaskInfo::create (echion, reinterpret_cast <TaskObj*>(task_wr.wr_object ));
608- if (maybe_task_info) {
609- if (reinterpret_cast <uintptr_t >((*maybe_task_info)->loop ) == this ->asyncio_loop ) {
610- tasks.push_back (std::move (*maybe_task_info));
611- }
531+ for (auto weakref_addr : *maybe_scheduled) {
532+ PyWeakReference weakref;
533+ if (!copy_type (weakref_addr, weakref)) {
534+ tasks.push_back (reinterpret_cast <TaskObj*>(weakref.wr_object ));
612535 }
613536 }
614537
615- auto asyncio_eager_tasks = echion.asyncio_eager_tasks ();
616- if (asyncio_eager_tasks != nullptr ) {
617- auto maybe_eager_tasks_set = MirrorSet::create (asyncio_eager_tasks);
618- if (!maybe_eager_tasks_set) {
538+ if (auto eager = echion.asyncio_eager_tasks (); eager != nullptr ) {
539+ auto maybe_eager_set = MirrorSet::create (eager);
540+ if (!maybe_eager_set) {
619541 return ErrorKind::TaskInfoError;
620542 }
621-
622- auto eager_tasks_set = std::move (*maybe_eager_tasks_set);
623-
624- auto maybe_eager_tasks = eager_tasks_set.as_unordered_set ();
625- if (!maybe_eager_tasks) {
543+ auto maybe_eager = maybe_eager_set->as_unordered_set ();
544+ if (!maybe_eager) {
626545 return ErrorKind::TaskInfoError;
627546 }
628-
629- auto eager_tasks = std::move (*maybe_eager_tasks);
630- for (auto task_addr : eager_tasks) {
631- auto maybe_task_info = TaskInfo::create (echion, reinterpret_cast <TaskObj*>(task_addr));
632- if (maybe_task_info) {
633- if (reinterpret_cast <uintptr_t >((*maybe_task_info)->loop ) == this ->asyncio_loop ) {
634- tasks.push_back (std::move (*maybe_task_info));
635- }
636- }
547+ for (auto task : *maybe_eager) {
548+ tasks.push_back (reinterpret_cast <TaskObj*>(task));
637549 }
638550 }
639551
640552 return tasks;
641553}
642554#endif // PY_VERSION_HEX >= 0x030e0000
643555
556+ Result<std::vector<TaskInfo::Ptr>>
557+ ThreadInfo::get_all_tasks (EchionSampler& echion, PyThreadState* tstate)
558+ {
559+ auto maybe_addresses = get_all_task_addresses (echion, tstate);
560+ if (!maybe_addresses) {
561+ return maybe_addresses.error ();
562+ }
563+
564+ std::vector<TaskInfo::Ptr> tasks;
565+ tasks.reserve (maybe_addresses->size ());
566+ for (TaskObj* task_addr : *maybe_addresses) {
567+ auto maybe_task = TaskInfo::create (echion, task_addr);
568+ if (maybe_task && reinterpret_cast <uintptr_t >((*maybe_task)->loop ) == this ->asyncio_loop ) {
569+ tasks.push_back (std::move (*maybe_task));
570+ }
571+ }
572+ return tasks;
573+ }
574+
644575// ----------------------------------------------------------------------------
645576void
646577ThreadInfo::unwind_greenlets (EchionSampler& echion,
0 commit comments