@@ -205,6 +205,36 @@ ThreadInfo::unwind(PyThreadState* tstate)
205205inline Result<void >
206206ThreadInfo::unwind_tasks ()
207207{
208+ // Check if the Python stack contains "_run".
209+ // To avoid having to do string comparisons every time we unwind Tasks, we keep track
210+ // of the cache key of the "_run" Frame.
211+ static std::optional<Frame::Key> frame_cache_key;
212+ bool expect_at_least_one_running_task = false ;
213+ if (!frame_cache_key) {
214+ for (size_t i = 0 ; i < python_stack.size (); i++) {
215+ const auto & frame = python_stack[i].get ();
216+ const auto & frame_name = string_table.lookup (frame.name )->get ();
217+ if (frame_name.size () >= 4 && frame_name.rfind (" _run" ) == frame_name.size () - 4 ) {
218+
219+ // Although Frames are stored in an LRUCache, the cache key is ALWAYS the same
220+ // even if the Frame gets evicted from the cache.
221+ // This means we can keep the cache key and re-use it to determine
222+ // whether we see the "_run" Frame in the Python stack.
223+ frame_cache_key = frame.cache_key ;
224+ expect_at_least_one_running_task = true ;
225+ break ;
226+ }
227+ }
228+ } else {
229+ for (size_t i = 0 ; i < python_stack.size (); i++) {
230+ const auto & frame = python_stack[i].get ();
231+ if (frame.cache_key == *frame_cache_key) {
232+ expect_at_least_one_running_task = true ;
233+ break ;
234+ }
235+ }
236+ }
237+
208238 std::vector<TaskInfo::Ref> leaf_tasks;
209239 std::unordered_set<PyObject*> parent_tasks;
210240 std::unordered_map<PyObject*, TaskInfo::Ref> waitee_map; // Indexed by task origin
@@ -217,6 +247,22 @@ ThreadInfo::unwind_tasks()
217247 }
218248
219249 auto all_tasks = std::move (*maybe_all_tasks);
250+
251+ bool saw_at_least_one_running_task = false ;
252+ std::string running_task_name;
253+ for (const auto & task_ref : all_tasks) {
254+ const auto & task = task_ref.get ();
255+ if (task->is_on_cpu ) {
256+ running_task_name = string_table.lookup (task->name )->get ();
257+ saw_at_least_one_running_task = true ;
258+ break ;
259+ }
260+ }
261+
262+ if (saw_at_least_one_running_task != expect_at_least_one_running_task) {
263+ return ErrorKind::TaskInfoError;
264+ }
265+
220266 {
221267 std::lock_guard<std::mutex> lock (task_link_map_lock);
222268
@@ -284,8 +330,13 @@ ThreadInfo::unwind_tasks()
284330 for (auto current_task = leaf_task;;) {
285331 auto & task = current_task.get ();
286332
333+ auto maybe_task_stack_size = task.unwind (stack, task.is_on_cpu ? upper_python_stack_size : unused);
334+ if (!maybe_task_stack_size) {
335+ return ErrorKind::TaskInfoError;
336+ }
337+
287338 // The task_stack_size includes both the coroutines frames and the "upper" Python synchronous frames
288- size_t task_stack_size = task. unwind (stack, task. is_on_cpu ? upper_python_stack_size : unused );
339+ size_t task_stack_size = std::move (*maybe_task_stack_size );
289340 if (task.is_on_cpu ) {
290341 // Get the "bottom" part of the Python synchronous Stack, that is to say the
291342 // synchronous functions and coroutines called by the Task's outermost coroutine
0 commit comments