@@ -39,37 +39,46 @@ ThreadInfo::unwind(EchionSampler& echion, PyThreadState* tstate, microsecond_t w
3939}
4040
4141// ----------------------------------------------------------------------------
42- Result< void >
43- ThreadInfo::unwind_tasks (EchionSampler& echion, PyThreadState* tstate, microsecond_t wall_time_us)
42+ size_t
43+ ThreadInfo::find_upper_python_stack_size (EchionSampler& echion) const
4444{
45- // The size of the "pure Python" stack (before asyncio Frames).
4645 // Defaults to the full Python stack size (and updated if we find the boundary frame)
4746 size_t upper_python_stack_size = python_stack.size ();
4847
4948 // Check if the Python stack contains the asyncio boundary frame.
5049 // For regular asyncio, this is "Handle._run" from asyncio/events.py.
5150 // For uvloop, this is "Runner.run" from asyncio/runners.py (uvloop uses asyncio.Runner internally).
52- // To avoid having to do string comparisons every time we unwind Tasks, we keep track
53- // of the cache key of the boundary frame.
51+ // To avoid having to do string comparisons every time we unwind Tasks, we memoize the interned
52+ // name and filename of the boundary Frame the first time we identify it. Those are already
53+ // StringTable keys, so recognizing the boundary afterwards costs two integer comparisons.
54+ //
55+ // Do NOT memoize Frame::cache_key here: it mixes in the bytecode offset, so it would only match
56+ // the boundary Frame while it sits on the instruction it happened to be on when we first saw it.
57+ // A single sample taken while, say, Runner.run is still in its prologue would then hide the
58+ // boundary for the rest of the process, and every Task stack would be rendered with the
59+ // coroutine frames at the leaf and the whole thread stack appended below them.
5460
55- // Note: We use separate cache keys for asyncio and uvloop because switching between them
61+ // Note: We memoize asyncio and uvloop separately because switching between them
5662 // (though unlikely at runtime) would cause incorrect boundary detection otherwise.
57- auto & asyncio_frame_cache_key = echion.asyncio_frame_cache_key ();
58- auto & uvloop_frame_cache_key = echion.uvloop_frame_cache_key ();
63+ auto & asyncio_boundary_frame = echion.asyncio_boundary_frame ();
64+ auto & uvloop_boundary_frame = echion.uvloop_boundary_frame ();
65+
66+ auto & boundary_frame = using_uvloop ? uvloop_boundary_frame : asyncio_boundary_frame;
67+
68+ for (size_t i = 0 ; i < python_stack.size (); i++) {
69+ const auto & frame = python_stack[i];
5970
60- auto & frame_cache_key = using_uvloop ? uvloop_frame_cache_key : asyncio_frame_cache_key ;
71+ bool is_boundary_frame = false ;
6172
62- if (!frame_cache_key ) {
63- for ( size_t i = 0 ; i < python_stack. size (); i++) {
64- const auto & frame = python_stack[i];
73+ if (boundary_frame ) {
74+ is_boundary_frame = frame. name == boundary_frame-> name && frame. filename == boundary_frame-> filename ;
75+ } else {
6576 auto maybe_frame_name = echion.string_table ().lookup (frame.name );
6677 if (!maybe_frame_name) {
6778 continue ;
6879 }
6980 const auto & frame_name = maybe_frame_name->get ();
7081
71- bool is_boundary_frame = false ;
72-
7382 if (using_uvloop) {
7483 // For uvloop, the boundary frame depends on the Python version:
7584 // - Python 3.11+: Runner.run from asyncio/runners.py (uvloop uses asyncio.Runner)
@@ -113,25 +122,26 @@ ThreadInfo::unwind_tasks(EchionSampler& echion, PyThreadState* tstate, microseco
113122 }
114123
115124 if (is_boundary_frame) {
116- // Although Frames are stored in an LRUCache, the cache key is ALWAYS the same
117- // even if the Frame gets evicted from the cache.
118- // This means we can keep the cache key and reuse it to determine
119- // whether we see the boundary Frame in the Python stack.
120- frame_cache_key = frame.cache_key ;
121- upper_python_stack_size = python_stack.size () - i;
122- break ;
125+ boundary_frame = BoundaryFrame{ frame.name , frame.filename };
123126 }
124127 }
125- } else {
126- for (size_t i = 0 ; i < python_stack.size (); i++) {
127- const auto & frame = python_stack[i];
128- if (frame.cache_key == *frame_cache_key) {
129- upper_python_stack_size = python_stack.size () - i;
130- break ;
131- }
128+
129+ if (is_boundary_frame) {
130+ upper_python_stack_size = python_stack.size () - i;
131+ break ;
132132 }
133133 }
134134
135+ return upper_python_stack_size;
136+ }
137+
138+ // ----------------------------------------------------------------------------
139+ Result<void >
140+ ThreadInfo::unwind_tasks (EchionSampler& echion, PyThreadState* tstate, microsecond_t wall_time_us)
141+ {
142+ // The size of the "pure Python" stack (before asyncio Frames).
143+ const size_t upper_python_stack_size = find_upper_python_stack_size (echion);
144+
135145 std::vector<TaskInfo::Ref> leaf_tasks;
136146 std::unordered_set<PyObject*> parent_tasks;
137147 std::unordered_map<PyObject*, TaskInfo::Ref> waitee_map; // Indexed by task origin
0 commit comments