Skip to content

Commit 9f3c682

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

2 files changed

Lines changed: 88 additions & 161 deletions

File tree

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

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -114,20 +114,21 @@ class ThreadInfo
114114
};
115115

116116
private:
117+
using TaskAddressCallback = std::function<void(TaskObj*)>;
118+
117119
void reset_cycle_state() noexcept;
118120
void render_unwound_stacks(EchionSampler&);
119121
[[nodiscard]] Result<void> unwind_tasks(EchionSampler&, PyThreadState*, microsecond_t wall_time_us);
120122
void unwind_greenlets(EchionSampler&, PyThreadState*, unsigned long, microsecond_t wall_time_us);
121123
[[nodiscard]] Result<std::vector<TaskInfo::Ptr>> get_all_tasks(EchionSampler&, PyThreadState* tstate);
124+
[[nodiscard]] Result<void> for_each_task_address(EchionSampler&,
125+
PyThreadState* tstate,
126+
const TaskAddressCallback& callback);
122127
#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);
128+
[[nodiscard]] Result<void> visit_thread_task_addresses(const TaskAddressCallback& callback);
129+
[[nodiscard]] Result<void> visit_interpreter_task_addresses(PyThreadState* tstate,
130+
const TaskAddressCallback& callback);
131+
[[nodiscard]] Result<void> visit_linked_task_addresses(uintptr_t head_addr, const TaskAddressCallback& callback);
131132
#endif
132133
};
133134

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

Lines changed: 79 additions & 153 deletions
Original file line numberDiff line numberDiff line change
@@ -411,236 +411,162 @@ 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::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

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

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

0 commit comments

Comments
 (0)