Skip to content

Commit 8a1121c

Browse files
committed
feat: support render python async stacks
Signed-off-by: bppps <bpppsaka@gmail.com>
1 parent d617a95 commit 8a1121c

File tree

10 files changed

+1089
-18
lines changed

10 files changed

+1089
-18
lines changed

docs/WIKI.md

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ help [command]
4444
View Python execution stack information for all threads currently running in the process, and support analyzing native stacks and exporting to files.
4545

4646
```shell
47-
stack [pid] [-f <value>] [--native]
47+
stack [pid] [-f <value>] [--native] [-a|--async]
4848
```
4949

5050
##### Parameter Analysis
@@ -53,6 +53,7 @@ stack [pid] [-f <value>] [--native]
5353
| pid | No | Process ID to analyze, defaults to the injected process ID | 3303 |
5454
| -f, --filepath | No | File path to export thread stacks to | /home/admin/stack.log |
5555
| --native | No | Whether to analyze native stacks of Python threads, defaults to False | --native |
56+
| -a, --async | No | Whether to display async coroutine/task stacks | -a |
5657

5758
##### Output Display
5859
Command examples:
@@ -64,6 +65,10 @@ stack
6465
# View native stacks of Python threads
6566
stack --native
6667

68+
# View async coroutine/task stacks
69+
stack -a
70+
stack --async
71+
6772
# Export execution stack information to a file
6873
stack -f ./stack.log
6974
```
@@ -80,19 +85,58 @@ Analyzing native thread stacks:
8085
View Python execution stack information for all threads currently running in the process, and support exporting to files.
8186

8287
```shell
83-
stack [filepath]
88+
stack [filepath] [-a|--async]
8489
```
8590

8691
##### Parameter Analysis
8792
| Parameter | Required | Meaning | Example |
8893
| --- | --- | --- | --- |
8994
| filepath | No | File path to export thread stacks to | /home/admin/stack.log |
95+
| -a, --async | No | Whether to display async coroutine/task stacks | -a |
9096

9197
##### Output Display
9298
Executing the `stack` command will display stack information for all threads in the console.
9399

94100
![img.png](https://raw.githubusercontent.com/alibaba/PyFlightProfiler/refs/heads/main/docs/images/stack_mac.png)
95101

102+
### Async Coroutine Stack: stack --async
103+
View all async coroutines/tasks running across all event loops in all threads.
104+
105+
```shell
106+
stack -a
107+
stack --async
108+
```
109+
110+
The async stack display shows:
111+
- **Thread**: The thread where the event loop is running
112+
- **Task Name**: The asyncio Task name
113+
- **State**: Task state (PENDING, WAITING, FINISHED, CANCELLED, FAILED)
114+
- **Coroutine**: The coroutine function name
115+
- **Stack**: Full coroutine call chain following the `cr_await` chain
116+
117+
Example output:
118+
```
119+
============================================================
120+
Async Coroutine/Task Stacks
121+
============================================================
122+
123+
Thread: AsyncEventLoop (tid: 0x16f8a7000)
124+
--------------------------------------------------
125+
126+
Task #1: FetchAPI1
127+
State: WAITING
128+
Coroutine: fetch_data_loop
129+
Stack (3 frames):
130+
File "/app/demo.py", line 95, in fetch_data_loop
131+
await fetch_data(name, delay)
132+
File "/app/demo.py", line 26, in fetch_data
133+
await asyncio.sleep(delay)
134+
File "/usr/lib/python3.9/asyncio/tasks.py", line 649, in sleep
135+
return await future
136+
137+
============================================================
138+
```
139+
96140

97141
## Method Execution Observation: watch
98142
### Observing Method Input, Output, and Time Consumption

docs/WIKI_zh.md

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ help [command]
4444
查看进程当前运行的所有线程的Python执行栈信息,并支持分析native栈以及导出到文件。
4545

4646
```shell
47-
stack [pid] [-f <value>] [--native]
47+
stack [pid] [-f <value>] [--native] [-a|--async]
4848
```
4949

5050
##### 参数解析
@@ -53,6 +53,7 @@ stack [pid] [-f <value>] [--native]
5353
| pid || 分析的进程ID,默认为被注入进程的ID | 3303 |
5454
| -f, --filepath || 线程栈导出到的文件位置 | /home/admin/stack.log |
5555
| --native || 是否分析Python线程的本地栈,默认为False | --native |
56+
| -a, --async || 是否展示异步协程/任务调用栈 | -a |
5657

5758
##### 输出展示
5859
命令示例:
@@ -64,6 +65,10 @@ stack
6465
# 查看Python线程的native栈
6566
stack --native
6667

68+
# 查看异步协程/任务调用栈
69+
stack -a
70+
stack --async
71+
6772
# 导出执行栈信息到文件中
6873
stack -f ./stack.log
6974
```
@@ -80,19 +85,58 @@ stack -f ./stack.log
8085
查看进程当前运行的所有线程的Python执行栈信息,并支持导出到文件。
8186

8287
```shell
83-
stack [filepath]
88+
stack [filepath] [-a|--async]
8489
```
8590

8691
##### 参数解析
8792
| 参数 | 必填 | 含义 | 示例 |
8893
| --- | --- | --- | --- |
8994
| filepath || 线程栈导出到的文件位置 | /home/admin/stack.log |
95+
| -a, --async || 是否展示异步协程/任务调用栈 | -a |
9096

9197
##### 输出展示
9298
执行`stack`命令可在控制台展示所有线程的栈信息。
9399

94100
![img.png](https://raw.githubusercontent.com/alibaba/PyFlightProfiler/refs/heads/main/docs/images/stack_mac.png)
95101

102+
### 异步协程栈: stack --async
103+
查看进程中所有线程、所有事件循环中运行的异步协程/任务。
104+
105+
```shell
106+
stack -a
107+
stack --async
108+
```
109+
110+
异步协程栈展示内容包括:
111+
- **Thread**: 事件循环所在的线程
112+
- **Task Name**: asyncio Task 的名称
113+
- **State**: 任务状态(PENDING, WAITING, FINISHED, CANCELLED, FAILED)
114+
- **Coroutine**: 协程函数名
115+
- **Stack**: 完整的协程调用链(通过 `cr_await` 链递归追踪)
116+
117+
输出示例:
118+
```
119+
============================================================
120+
Async Coroutine/Task Stacks
121+
============================================================
122+
123+
Thread: AsyncEventLoop (tid: 0x16f8a7000)
124+
--------------------------------------------------
125+
126+
Task #1: FetchAPI1
127+
State: WAITING
128+
Coroutine: fetch_data_loop
129+
Stack (3 frames):
130+
File "/app/demo.py", line 95, in fetch_data_loop
131+
await fetch_data(name, delay)
132+
File "/app/demo.py", line 26, in fetch_data
133+
await asyncio.sleep(delay)
134+
File "/usr/lib/python3.9/asyncio/tasks.py", line 649, in sleep
135+
return await future
136+
137+
============================================================
138+
```
139+
96140

97141
## 方法执行观测watch
98142
### 观察执行方法输入、输出及耗时

flight_profiler/code_inject.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,6 @@ def run_app():
4646
loop.run_until_complete(asyncio.wait(tasks))
4747

4848

49-
profile_thread = threading.Thread(target=run_app)
49+
profile_thread = threading.Thread(target=run_app, name="flight-profiler-injector")
5050
profile_thread.start()
5151
logger.info("pyFlightProfiler: start code inject successfully")

flight_profiler/help_descriptions.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -211,9 +211,9 @@ def _build_help_msg(self) -> str:
211211

212212
if is_linux():
213213
STACK_COMMAND_DESCRIPTION = CommandDescription(
214-
usage=["stack [pid] [-f <value>] [--native]"],
214+
usage=["stack [pid] [-f <value>] [--native] [-a|--async]"],
215215
summary="Inspect stack frames of current running process.",
216-
examples=["stack", "stack --native", "stack -f ./stack.log"],
216+
examples=["stack", "stack --native", "stack -a", "stack --async", "stack -f ./stack.log"],
217217
wiki="https://github.com/alibaba/PyFlightProfiler/blob/main/docs/WIKI.md",
218218
options=[
219219
(
@@ -222,15 +222,19 @@ def _build_help_msg(self) -> str:
222222
),
223223
("-f, --filepath", "redirect thread stack to filepath."),
224224
("--native", "display native stack frames."),
225+
("-a, --async", "display async coroutine/task stacks."),
225226
],
226227
)
227228
else:
228229
STACK_COMMAND_DESCRIPTION = CommandDescription(
229-
usage=["stack [filepath]"],
230+
usage=["stack [filepath] [-a|--async]"],
230231
summary="Inspect stack frames of current running process.",
231-
examples=["stack", "stack ./stack.log"],
232+
examples=["stack", "stack -a", "stack --async", "stack ./stack.log"],
232233
wiki="https://github.com/alibaba/PyFlightProfiler/blob/main/docs/WIKI.md",
233-
options=[("<filepath>", "redirect thread stack to filepath.")],
234+
options=[
235+
("<filepath>", "redirect thread stack to filepath."),
236+
("-a, --async", "display async coroutine/task stacks."),
237+
],
234238
)
235239

236240
TRACE_COMMAND_DESCRIPTION = CommandDescription(

flight_profiler/plugins/stack/cli_plugin_stack.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,48 @@ def __analyze_under_linux(self, params: StackParams):
5656
f"{COLOR_GREEN}write {stack_literal} to {params.filepath} successfully!{COLOR_END}"
5757
)
5858

59+
def __show_coroutine_stacks(self, params: StackParams):
60+
"""
61+
Show async coroutine stacks via server communication.
62+
Coroutines must be fetched from the target process's event loop.
63+
"""
64+
body = {"target": "stack", "param": "async"}
65+
try:
66+
client = FlightClient(host="localhost", port=self.port)
67+
except:
68+
show_error_info("Target process exited!")
69+
return
70+
try:
71+
coro_lines: List[str] = []
72+
for line in client.request_stream(body):
73+
if line:
74+
line = line.decode("utf-8")
75+
if params.filepath is not None:
76+
coro_lines.append(line)
77+
else:
78+
print(line)
79+
if params.filepath is not None:
80+
with open(params.filepath, "w") as f:
81+
for line in coro_lines:
82+
print(line, file=f, flush=True)
83+
print(
84+
f"{COLOR_GREEN}write coroutine stacks to {params.filepath} successfully!{COLOR_END}"
85+
)
86+
finally:
87+
client.close()
88+
5989
def do_action(self, cmd):
6090
try:
6191
stack_param: StackParams = global_stack_parser.parse_stack_params(cmd)
6292
except:
6393
print(self.get_help())
6494
return
95+
96+
# Handle async coroutine stack display
97+
if stack_param.async_stack:
98+
self.__show_coroutine_stacks(stack_param)
99+
return
100+
65101
if is_linux():
66102
try:
67103
self.__analyze_under_linux(stack_param)

0 commit comments

Comments
 (0)