<div dir="ltr"><div dir="ltr"><div dir="ltr"><div dir="ltr"><div dir="ltr">Hi,<div><br></div><div>(Apologies in advance for the long email. This is also my first time using the mailing list, so sorry in advance for any missteps.)</div><div><br></div><div>Briefly: we have been working on incorporating C++ coroutine frames into stack traces.</div><div>The particular issue we have been trying to solve is that many common tools that obtain stack traces (e.g., <font face="monospace">perf</font>) do not appear to have a mechanism for discovering or traversing C++ coroutine frames.</div><div>It was suggested to us that SFrames could perhaps facilitate this across many tools, and so we're reaching out here to discuss whether/how that may be achievable, as we are not too familiar with SFrame details.</div><div><br></div><div>As C++ coroutines have a very high amount of complexity, I have attempted to distill down the relevant details of our current design below, along with a quick primer on how C++ coroutines work from the user side.</div><div></div><div><div><br>Overall, we're wondering if SFrames (either as they exist now, or via a practical extension) would be suitable for this "stitching" or "interleaving" of coroutine stack frames, and if so, what we could plan for and/or expect on that front.</div><div><br></div><div>Thank you.</div><div><br></div><hr style="font-family:"Times New Roman";font-size:medium">A toy coroutine may look like this:<span style="color:rgb(0,0,0);font-family:"Times New Roman";font-size:medium"></span></div><blockquote style="margin:0 0 0 40px;border:none;padding:0px"><div><font face="monospace"><font color="#9900ff">Task</font><int> factorial(int n) {</font></div></blockquote><blockquote style="margin:0 0 0 40px;border:none;padding:0px"><div><font face="monospace"> printf("n = %d\n", n);</font></div></blockquote><blockquote style="margin:0 0 0 40px;border:none;padding:0px"><div><font face="monospace"> if (n == 0) {</font></div><div><font face="monospace"> <font color="#0000ff">co_return</font> 1;</font></div><div><font face="monospace"> }</font></div></blockquote><blockquote style="margin:0 0 0 40px;border:none;padding:0px"><div><font face="monospace"> int subresult = </font><span style="font-family:monospace"><font color="#0000ff">co_await</font> factorial(n - 1);</span></div></blockquote><blockquote style="margin:0 0 0 40px;border:none;padding:0px"><div><font face="monospace"> <font color="#0000ff">co_return</font> n * subresult;</font></div><div><font face="monospace">}</font></div></blockquote><div>The <font face="monospace">Task</font> type internally holds a <font face="monospace">std::coroutine_handle<></font> object, which is basically an opaque <font face="monospace">void*</font> pointing to the various bookkeeping data of the coroutine.</div><div>The <font face="monospace">Task</font> type is specially marked to signify to the compiler that this function is a coroutine, and defines various behaviors, such as those of the keywords <font face="monospace">co_await</font> and <font face="monospace">co_return</font> (e.g., by storing the provided values in heap memory somewhere for later retrieval).</div><div><br></div><div>Ordinarily, if we have</div><div><blockquote style="margin:0 0 0 40px;border:none;padding:0px"><div><font face="monospace">int main() {</font></div></blockquote><blockquote style="margin:0 0 0 40px;border:none;padding:0px"><div><font face="monospace"> factorial(10)</font><span style="font-family:monospace">.Resume(); // Run the coroutine</span></div></blockquote><blockquote style="margin:0 0 0 40px;border:none;padding:0px"><div><font face="monospace">}</font></div></blockquote></div><div>and if we then stop the program to obtain a stack trace arbitrarily, we may obtain a typical CPU stack trace such as this:</div><div><blockquote style="margin:0 0 0 40px;border:none;padding:0px"><font face="monospace">printf()<br>factorial(n=3)</font><br></blockquote></div><blockquote style="margin:0 0 0 40px;border:none;padding:0px"><div><blockquote style="margin:0 0 0 40px;border:none;padding:0px"><i>(Note that I'm including the <font face="monospace">n=</font> values for illustrative purposes; they of course would not be apparent solely from a list of instruction addresses.)</i></blockquote></div></blockquote><div><blockquote style="margin:0 0 0 40px;border:none;padding:0px"><font face="monospace">std::coroutine_handle<>::resume()<br></font></blockquote></div><blockquote style="margin:0 0 0 40px;border:none;padding:0px"><div><blockquote style="margin:0 0 0 40px;border:none;padding:0px"><i>(Note that the <font face="monospace"><a href="https://en.cppreference.com/w/cpp/coroutine/coroutine_handle/resume.html" target="_blank">std::coroutine_handle<>::resume()</a></font> function uses a compiler intrinsic such as <font face="monospace"><a href="https://clang.llvm.org/docs/LanguageExtensions.html#c-coroutines-support-builtins" target="_blank">__builtin_coro_resume()</a></font> to resume the coroutine underneath.)</i></blockquote></div></blockquote><div><blockquote style="margin:0 0 0 40px;border:none;padding:0px"><font face="monospace">Task::Resume()<br>main()</font></blockquote></div><div><br></div><div>However, what we <i>wish</i> to obtain in such a situation is the following:</div><div><blockquote style="margin:0px 0px 0px 40px;border:none;padding:0px"><font face="monospace">printf()<br>factorial(n=3)<br></font><span style="font-family:monospace"><font color="#990000">factorial(n=2) (.resume)</font><br></span><span style="font-family:monospace"><font color="#990000">factorial(n=1) (.resume)</font></span><font face="monospace"><br>std::coroutine_handle<>::resume()</font></blockquote></div><div><blockquote style="margin:0px 0px 0px 40px;border:none;padding:0px"><font face="monospace">Task<>::Resume()<br>main()</font></blockquote></div><div><br></div><div>The information required for observing <font face="monospace">factorial(2)</font> and <font face="monospace">factorial(1)</font> does exist in memory, but it is not part of the ordinary thread call stack.</div><div>In our implementation, it is represented via a singly-linked list:</div><div><ol><li>The currently-running coroutine (in this case, <font face="monospace">factorial(3)</font>), which is the head of the list, is maintained in a known <b>thread-local</b> variable.</li><li>Each coroutine also remembers the address of its caller (a.k.a. waiter, a.k.a. continuation).</li></ol></div><div>Typically, the address of the calling coroutine is stored inside a so-called "Promise" object on the heap associated with the <font face="monospace">Task</font>, whose address is obtained via invoking <font face="monospace">std::coroutine_handle<T>::promise()</font> on the coroutine handle embedded inside the <font face="monospace">Task</font>.</div><div>Each coroutine saves/restores the handle to its caller at various points via hooks provided through the language & compiler.</div><div>When the compiler asks the coroutine "<i>Now that you are suspended, what should I resume next?</i>" (via <a href="https://en.cppreference.com/w/cpp/language/coroutines.html#:~:text=if%20await_suspend%20returns%20a%20coroutine%20handle%20for%20some%20other%20coroutine%2C%20that%20handle%20is%20resumed" target="_blank">complicated machinery</a>), the coroutine responds with the handle of its caller (the continuation).</div><span style="color:rgb(0,0,0);font-family:"Times New Roman";font-size:medium"></span><div><br></div><div>The missing piece of the puzzle is: how would the interleaving process figure out <b>where</b> to insert the coroutine frames into the ordinary stack trace?</div><div>Conceptually, we need some way to identify <font face="monospace">Task::Resume()</font> in the stack trace, and insert everything beneath the <font face="monospace">std::coroutine_handle<>::resume()</font> that it is eventually calling downstream.</div><div><br></div><div>Right now we accomplish this via some dynamic probing at program initialization:</div><div><ol><li>We block the inlining of <font face="monospace">Task::Resume()</font></li><li>We force the inlining of <font face="monospace">std::coroutine_handle<>::resume()</font> (which just reduces to an intrinsic, <font face="monospace">__builtin_coro_resume()</font>)</li><li>We call a dummy coroutine whose sole job is to return its own <font face="monospace">__builtin_return_address(0)</font>.</li></ol><div>This return address is in the body of <font face="monospace">Task<>::Resume</font>, and this forces it to be the unique address in the entire program that all <font face="monospace">Task<T></font> coroutines eventually return to.</div></div><div>Therefore, we perform a linear search for it starting from the leaf frame on the stack, and we insert all the asynchronous frames after that.</div></div>
</div>
</div>
</div>
</div>