@@ -411,236 +411,162 @@ 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::visit_thread_task_addresses ( const TaskAddressCallback& callback )
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 visit_linked_task_addresses (this ->tstate_addr + asyncio_tasks_head_offset, callback);
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::visit_interpreter_task_addresses (PyThreadState* tstate, const TaskAddressCallback& callback)
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 visit_linked_task_addresses (head_addr, callback);
445434}
446435
447436Result<void >
448- ThreadInfo::get_tasks_from_linked_list (EchionSampler& echion, uintptr_t head_addr, std::vector<TaskInfo::Ptr>& tasks )
437+ ThreadInfo::visit_linked_task_addresses ( uintptr_t head_addr, const TaskAddressCallback& callback )
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 );
487-
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;
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+ callback (reinterpret_cast <TaskObj*>(task_addr));
491464
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<void >
474+ ThreadInfo::for_each_task_address (EchionSampler& echion, PyThreadState* tstate, const TaskAddressCallback& callback )
512475{
513- std::vector<TaskInfo::Ptr> tasks;
514- if (this ->asyncio_loop == 0 )
515- return tasks;
516-
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
476+ if (this ->asyncio_loop == 0 ) {
477+ return Result<void >::ok ();
478+ }
479+
480+ // Native tasks can appear in both lists. Consumers already tolerate duplicate task objects.
523481 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- }
482+ (void )visit_thread_task_addresses (callback);
483+ (void )visit_interpreter_task_addresses (tstate, callback);
484+ }
485+
486+ // Python 3.14 stores only third-party Task implementations in _scheduled_tasks.
487+ if (auto scheduled = echion.asyncio_scheduled_tasks (); scheduled != nullptr ) {
488+ if (auto maybe_set = MirrorSet::create (scheduled)) {
489+ if (auto maybe_tasks = maybe_set->as_unordered_set ()) {
490+ for (auto task : *maybe_tasks) {
491+ callback (reinterpret_cast <TaskObj*>(task));
548492 }
549493 }
550494 }
551495 }
552496
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) {
497+ if (auto eager = echion.asyncio_eager_tasks (); eager != nullptr ) {
498+ auto maybe_set = MirrorSet::create (eager);
499+ if (!maybe_set) {
557500 return ErrorKind::TaskInfoError;
558501 }
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) {
502+ auto maybe_tasks = maybe_set->as_unordered_set ();
503+ if (!maybe_tasks) {
564504 return ErrorKind::TaskInfoError;
565505 }
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- }
506+ for (auto task : *maybe_tasks) {
507+ callback (reinterpret_cast <TaskObj*>(task));
575508 }
576509 }
577510
578- return tasks ;
511+ return Result< void >:: ok () ;
579512}
580513#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*)
514+ Result<void >
515+ ThreadInfo::for_each_task_address (EchionSampler& echion, PyThreadState*, const TaskAddressCallback& callback)
584516{
585- std::vector<TaskInfo::Ptr> tasks;
586- if ( this -> asyncio_loop == 0 )
587- return tasks;
517+ if ( this -> asyncio_loop == 0 ) {
518+ return Result< void >:: ok ();
519+ }
588520
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) {
521+ auto maybe_set = MirrorSet::create (echion.asyncio_scheduled_tasks ());
522+ if (!maybe_set) {
592523 return ErrorKind::TaskInfoError;
593524 }
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) {
525+ auto maybe_scheduled = maybe_set->as_unordered_set ();
526+ if (!maybe_scheduled) {
598527 return ErrorKind::TaskInfoError;
599528 }
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- }
529+ for (auto weakref_addr : *maybe_scheduled) {
530+ PyWeakReference weakref;
531+ if (!copy_type (weakref_addr, weakref)) {
532+ callback (reinterpret_cast <TaskObj*>(weakref.wr_object ));
612533 }
613534 }
614535
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) {
536+ if (auto eager = echion.asyncio_eager_tasks (); eager != nullptr ) {
537+ auto maybe_eager_set = MirrorSet::create (eager);
538+ if (!maybe_eager_set) {
619539 return ErrorKind::TaskInfoError;
620540 }
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) {
541+ auto maybe_eager = maybe_eager_set->as_unordered_set ();
542+ if (!maybe_eager) {
626543 return ErrorKind::TaskInfoError;
627544 }
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- }
545+ for (auto task : *maybe_eager) {
546+ callback (reinterpret_cast <TaskObj*>(task));
637547 }
638548 }
639549
640- return tasks ;
550+ return Result< void >:: ok () ;
641551}
642552#endif // PY_VERSION_HEX >= 0x030e0000
643553
554+ Result<std::vector<TaskInfo::Ptr>>
555+ ThreadInfo::get_all_tasks (EchionSampler& echion, PyThreadState* tstate)
556+ {
557+ std::vector<TaskInfo::Ptr> tasks;
558+ auto result = for_each_task_address (echion, tstate, [&](TaskObj* task_addr) {
559+ auto maybe_task = TaskInfo::create (echion, task_addr);
560+ if (maybe_task && reinterpret_cast <uintptr_t >((*maybe_task)->loop ) == this ->asyncio_loop ) {
561+ tasks.push_back (std::move (*maybe_task));
562+ }
563+ });
564+ if (!result) {
565+ return result.error ();
566+ }
567+ return tasks;
568+ }
569+
644570// ----------------------------------------------------------------------------
645571void
646572ThreadInfo::unwind_greenlets (EchionSampler& echion,
0 commit comments