Skip to content

Commit 3efd2f4

Browse files
authored
gh-149296: Add dump subcommand to sampling profiler for one-shot stack snapshots (#149297)
Adds `python -m profiling.sampling dump <pid>`, which prints a single traceback-style snapshot of a running process's Python stack via the existing `_remote_debugging` unwinder. Supports per-thread status, source line highlighting, optional bytecode opcodes, and async-aware task reconstruction (`--async-aware`, default `--async-mode=all`).
1 parent 2e94f14 commit 3efd2f4

11 files changed

Lines changed: 1389 additions & 117 deletions

File tree

Doc/library/profiling.sampling.rst

Lines changed: 120 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,10 @@ Attach to a running process by PID::
153153

154154
python -m profiling.sampling attach 12345
155155

156+
Print a single snapshot of a running process's stack::
157+
158+
python -m profiling.sampling dump 12345
159+
156160
Use live mode for real-time monitoring (press ``q`` to quit)::
157161

158162
python -m profiling.sampling run --live script.py
@@ -173,8 +177,9 @@ Enable opcode-level profiling to see which bytecode instructions are executing::
173177
Commands
174178
========
175179

176-
Tachyon operates through two subcommands that determine how to obtain the
177-
target process.
180+
Tachyon operates through several subcommands. ``run`` and ``attach`` collect
181+
samples over time; ``dump`` captures a single snapshot; ``replay`` converts
182+
binary profiles to other formats.
178183

179184

180185
The ``run`` command
@@ -217,6 +222,78 @@ On most systems, attaching to another process requires appropriate permissions.
217222
See :ref:`profiling-permissions` for platform-specific requirements.
218223

219224

225+
.. _dump-command:
226+
227+
The ``dump`` command
228+
--------------------
229+
230+
The ``dump`` command prints a single snapshot of a running process's Python
231+
stack and exits, similar to a traceback::
232+
233+
python -m profiling.sampling dump 12345
234+
235+
Unlike ``attach``, ``dump`` does not run a sampling loop: it reads the
236+
stack once. This is useful for investigating hung or unresponsive
237+
processes, or for answering "what is this process doing right now?".
238+
239+
The output mirrors a traceback (most recent call last) and annotates each
240+
thread with its current state (main thread, has GIL, on CPU, waiting for
241+
GIL, has exception, or idle):
242+
243+
.. code-block:: text
244+
245+
Stack dump for PID 12345, thread 140735 (main thread, has GIL, on CPU; most recent call last):
246+
File "server.py", line 28, in serve
247+
await handle_request(req)
248+
File "handler.py", line 91, in handle_request
249+
result = expensive_call(req)
250+
251+
When the target's source files are readable, ``dump`` prints the source
252+
line for each frame and highlights the executing expression.
253+
254+
Like ``attach``, ``dump`` requires permission to read the target process's
255+
memory. See :ref:`profiling-permissions`.
256+
257+
The ``dump`` command supports the following options:
258+
259+
``-a``, ``--all-threads``
260+
Dump every thread in the target process. Without this flag only the main
261+
thread is shown.
262+
263+
``--native``
264+
Include synthetic ``<native>`` frames marking transitions into C
265+
extensions or other non-Python code.
266+
267+
``--no-gc``
268+
Hide the synthetic ``<GC>`` frames that mark active garbage collection.
269+
270+
``--opcodes``
271+
Annotate each frame with the bytecode opcode the thread is currently
272+
executing (for example, ``opcode=CALL_KW``). Useful for
273+
instruction-level investigation, including identifying specializations
274+
chosen by the adaptive interpreter.
275+
276+
``--async-aware``
277+
Reconstruct stacks across ``await`` boundaries. ``dump`` walks the task
278+
graph and emits one section per task, with ``<task>`` markers separating
279+
coroutines awaiting each other.
280+
281+
``--async-mode {running,all}``
282+
Controls which tasks are included when ``--async-aware`` is enabled.
283+
``running`` shows only the task currently executing on each thread;
284+
``all`` (the default for ``dump``) also includes tasks suspended on a
285+
wait. ``attach``'s default for this flag is ``running``; ``dump``
286+
defaults to ``all`` because a single snapshot is most useful when it
287+
shows the full task graph.
288+
289+
``--blocking``
290+
Pause every thread in the target while reading its stack and resume
291+
them after. Guarantees a fully consistent snapshot at the cost of
292+
briefly stopping the target. Without it, ``dump`` reads memory while
293+
the target keeps running, which is faster but can occasionally produce
294+
a torn stack.
295+
296+
220297
.. _replay-command:
221298

222299
The ``replay`` command
@@ -1441,11 +1518,52 @@ Global options
14411518

14421519
Attach to and profile a running process by PID.
14431520

1521+
.. option:: dump
1522+
1523+
Print a single one-shot snapshot of a running process's Python stack.
1524+
14441525
.. option:: replay
14451526

14461527
Convert a binary profile file to another output format.
14471528

14481529

1530+
Dump options
1531+
------------
1532+
1533+
The following options apply to the ``dump`` subcommand:
1534+
1535+
.. option:: -a, --all-threads
1536+
1537+
Dump all threads in the target process instead of just the main thread.
1538+
1539+
.. option:: --native
1540+
1541+
Include ``<native>`` frames for non-Python code.
1542+
1543+
.. option:: --no-gc
1544+
1545+
Exclude ``<GC>`` frames for active garbage collection.
1546+
1547+
.. option:: --opcodes
1548+
1549+
Show bytecode opcode names when available.
1550+
1551+
.. option:: --async-aware
1552+
1553+
Reconstruct the stack across ``await`` boundaries for asyncio
1554+
applications.
1555+
1556+
.. option:: --async-mode <mode>
1557+
1558+
Async stack mode: ``running`` (only the running task) or ``all``
1559+
(all tasks including waiting). Defaults to ``all`` for ``dump``.
1560+
Requires :option:`--async-aware`.
1561+
1562+
.. option:: --blocking
1563+
1564+
Pause all threads in the target process while reading the stack.
1565+
1566+
14491567
Sampling options
14501568
----------------
14511569

Doc/whatsnew/3.15.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,9 @@ Key features include:
322322
* Profile running processes by PID (``attach``) - attach to already-running applications
323323
* Run and profile scripts directly (``run``) - profile from the very start of execution
324324
* Execute and profile modules (``run -m``) - profile packages run as ``python -m module``
325+
* Capture a one-shot snapshot of a running process (``dump``) - print a
326+
traceback-style stack of every thread (or all asyncio tasks with
327+
``--async-aware``). Useful for investigating hung processes.
325328

326329
* **Multiple profiling modes**: Choose what to measure based on your performance investigation:
327330

Lib/_colorize.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,23 @@ class LiveProfiler(ThemeSection):
359359
)
360360

361361

362+
@dataclass(frozen=True, kw_only=True)
363+
class ProfilerDump(ThemeSection):
364+
header: str = ANSIColors.BOLD_BLUE
365+
interpreter: str = ANSIColors.GREY
366+
thread: str = ANSIColors.BOLD_CYAN
367+
status: str = ANSIColors.YELLOW
368+
frame_index: str = ANSIColors.GREY
369+
frame: str = ANSIColors.BOLD_GREEN
370+
filename: str = ANSIColors.CYAN
371+
line_no: str = ANSIColors.YELLOW
372+
source: str = ANSIColors.WHITE
373+
source_highlight: str = ANSIColors.BOLD_YELLOW
374+
opcode: str = ANSIColors.GREY
375+
warning: str = ANSIColors.YELLOW
376+
reset: str = ANSIColors.RESET
377+
378+
362379
@dataclass(frozen=True, kw_only=True)
363380
class Pickletools(ThemeSection):
364381
annotation: str = ANSIColors.GREY
@@ -447,6 +464,7 @@ class Theme:
447464
http_server: HttpServer = field(default_factory=HttpServer)
448465
live_profiler: LiveProfiler = field(default_factory=LiveProfiler)
449466
pickletools: Pickletools = field(default_factory=Pickletools)
467+
profiler_dump: ProfilerDump = field(default_factory=ProfilerDump)
450468
syntax: Syntax = field(default_factory=Syntax)
451469
timeit: Timeit = field(default_factory=Timeit)
452470
tokenize: Tokenize = field(default_factory=Tokenize)
@@ -463,6 +481,7 @@ def copy_with(
463481
http_server: HttpServer | None = None,
464482
live_profiler: LiveProfiler | None = None,
465483
pickletools: Pickletools | None = None,
484+
profiler_dump: ProfilerDump | None = None,
466485
syntax: Syntax | None = None,
467486
timeit: Timeit | None = None,
468487
tokenize: Tokenize | None = None,
@@ -482,6 +501,7 @@ def copy_with(
482501
http_server=http_server or self.http_server,
483502
live_profiler=live_profiler or self.live_profiler,
484503
pickletools=pickletools or self.pickletools,
504+
profiler_dump=profiler_dump or self.profiler_dump,
485505
syntax=syntax or self.syntax,
486506
timeit=timeit or self.timeit,
487507
tokenize=tokenize or self.tokenize,
@@ -505,6 +525,7 @@ def no_colors(cls) -> Self:
505525
http_server=HttpServer.no_colors(),
506526
live_profiler=LiveProfiler.no_colors(),
507527
pickletools=Pickletools.no_colors(),
528+
profiler_dump=ProfilerDump.no_colors(),
508529
syntax=Syntax.no_colors(),
509530
timeit=Timeit.no_colors(),
510531
tokenize=Tokenize.no_colors(),

0 commit comments

Comments
 (0)