Skip to content

Commit 1fdd48c

Browse files
committed
refactor(profiling): separate task address discovery
1 parent dd4d94c commit 1fdd48c

2 files changed

Lines changed: 84 additions & 156 deletions

File tree

ddtrace/internal/datadog/profiling/stack/echion/echion/threads.h

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -119,15 +119,12 @@ class ThreadInfo
119119
[[nodiscard]] Result<void> unwind_tasks(EchionSampler&, PyThreadState*, microsecond_t wall_time_us);
120120
void unwind_greenlets(EchionSampler&, PyThreadState*, unsigned long, microsecond_t wall_time_us);
121121
[[nodiscard]] Result<std::vector<TaskInfo::Ptr>> get_all_tasks(EchionSampler&, PyThreadState* tstate);
122+
[[nodiscard]] Result<std::vector<TaskObj*>> get_all_task_addresses(EchionSampler&, PyThreadState* tstate);
122123
#if PY_VERSION_HEX >= 0x030e0000
123-
[[nodiscard]] Result<void> get_tasks_from_thread_linked_list(EchionSampler& echion,
124-
std::vector<TaskInfo::Ptr>& tasks);
125-
[[nodiscard]] Result<void> get_tasks_from_interpreter_linked_list(EchionSampler& echion,
126-
PyThreadState* tstate,
127-
std::vector<TaskInfo::Ptr>& tasks);
128-
[[nodiscard]] Result<void> get_tasks_from_linked_list(EchionSampler& echion,
129-
uintptr_t head_addr,
130-
std::vector<TaskInfo::Ptr>& tasks);
124+
[[nodiscard]] Result<void> get_task_addresses_from_thread_linked_list(std::vector<TaskObj*>& tasks);
125+
[[nodiscard]] Result<void> get_task_addresses_from_interpreter_linked_list(PyThreadState* tstate,
126+
std::vector<TaskObj*>& tasks);
127+
[[nodiscard]] Result<void> get_task_addresses_from_linked_list(uintptr_t head_addr, std::vector<TaskObj*>& tasks);
131128
#endif
132129
};
133130

ddtrace/internal/datadog/profiling/stack/src/echion/threads.cc

Lines changed: 79 additions & 148 deletions
Original file line numberDiff line numberDiff line change
@@ -411,236 +411,167 @@ ThreadInfo::unwind_tasks(EchionSampler& echion, PyThreadState* tstate, microseco
411411
// ----------------------------------------------------------------------------
412412
#if PY_VERSION_HEX >= 0x030e0000
413413
Result<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

432424
Result<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

447436
Result<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
// ----------------------------------------------------------------------------
645576
void
646577
ThreadInfo::unwind_greenlets(EchionSampler& echion,

0 commit comments

Comments
 (0)