py-spy 默认不支持 async/await 调用栈,必须加 --idle 参数启用协程感知模式,并配合 --duration 才能捕获 await 点的协程帧;Python 3.11+ 和 py-spy 0.4.0+ 组合更可靠,旧版本或 uvloop 下易显示为 <unknown>。

py-spy 默认不支持 async/await 调用栈,必须显式启用采样器
py-spy 默认只采集 CPython 的主线程 Python 帧(PyFrameObject),而 asyncio 事件循环中协程的挂起/恢复由 coro.send() 或 await 触发,实际帧对象可能被优化掉或未被标准采样器捕获。不加配置直接运行 py-spy record -p <pid>,看到的几乎全是 select.epoll_wait、asyncio.run 或空栈,根本看不到你自己的 async def fetch_data() 在哪卡着。
解决方法是强制启用协程感知模式:
- 使用
--native+--subprocesses不够,关键要加--idle—— 它会让 py-spy 主动扫描处于ASYNCIO_TASK状态的协程对象 - 必须搭配
--duration(如30)避免过早退出,因为协程栈需要在 await 点被“抓拍”到 - 推荐命令:
py-spy record -p <pid> --duration 30 --idle --output profile.svg
asyncio 版本和 Python 版本直接影响 py-spy 能否识别协程帧
py-spy 对协程的支持严重依赖底层 asyncio 内部结构。Python 3.11+ 引入了 PyAsyncGenObject 和更稳定的 _asyncio.Task 布局,py-spy 0.4.0+ 才能可靠解析;而 Python 3.8–3.10 下,若用的是自定义事件循环(如 uvloop),py-spy 很可能把所有协程都识别为 <unknown> 或显示为 asyncio.events.Handle._run。
验证是否生效:
立即学习“Python免费学习笔记(深入)”;
- 运行后检查输出 SVG 中是否有带
async def关键字的函数名(不是<module>或<lambda>) - 用
py-spy top -p <pid> --idle实时看,顶部应出现类似myapp.api.handle_request(而非全在asyncio.base_events.Server._accept_connection) - 若仍无协程帧,降级尝试 py-spy 0.4.4(对 3.9 兼容性最好),或升级到 Python 3.12 + py-spy 0.9.0+
无法看到 await 后续函数调用?检查是否被 awaitable 对象绕过帧记录
py-spy 只能捕获「当前正在执行」的协程帧。如果协程里写了 await asyncio.sleep(0)、await aiohttp.ClientSession.get(...) 这类真正交出控制权的操作,它会在下一次事件循环 tick 中被重新调度,此时 py-spy 有概率采样到该协程的入口帧;但若用了 await some_sync_function()(且该函数内部没触发 await),或用了 asyncio.to_thread() 但线程未被 py-spy 跟踪,则调用栈会断在 to_thread 内部,看不到你自己的函数。
实操建议:
- 避免在 hot path 中用
await asyncio.sleep(0)模拟让出——它会让 py-spy 更难稳定捕获栈 - 对可疑的
await行,手动插入await asyncio.sleep(0.001)(延长可采样窗口)辅助定位 - 确认你 await 的对象确实是真正的协程对象:用
inspect.iscoroutine()或inspect.isawaitable()检查,别误把返回Future的同步函数当协程
生成火焰图后协程名显示为 <coro> 或地址?说明符号未正确解析
常见现象:SVG 中某一层写的是 <coro> at 0x7f...>,而不是函数名。这通常是因为 py-spy 没读取到协程对象的 cr_frame.f_code.co_name —— 可能因协程已结束、帧被 gc 回收,或代码运行在 frozen 模块(如 PyInstaller 打包后)导致 co_filename 为空。
缓解方式:
- 启动目标进程时加
-X dev(启用开发模式),减少帧优化 - 确保目标进程未启用
sys.settrace(比如被 pytest-cov、debugpy 或某些监控 SDK 注入),否则会干扰 py-spy 的帧遍历 - 用
py-spy dump -p <pid> --idle查看原始文本输出,比 SVG 更容易发现是否真有函数名字段缺失
协程栈不是“天然可见”的,它依赖事件循环状态、Python 版本、py-spy 版本三者对齐。最常被忽略的是:没加 --idle 就以为自己在看 async 栈,结果全程在分析 epoll_wait。


















