Skip to content

Commit c0612fa

Browse files
committed
gh-149296: Add dump subcommand to sampling profiler for one-shot stack snapshots
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 a65611e commit c0612fa

11 files changed

Lines changed: 1473 additions & 116 deletions

File tree

Doc/library/profiling.sampling.rst

Lines changed: 123 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,81 @@ 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. If a source
253+
file's modification time is newer than the target process's start time,
254+
``dump`` replaces the line with ``[source file changed after process
255+
started]`` to avoid showing misleading code.
256+
257+
Like ``attach``, ``dump`` requires permission to read the target process's
258+
memory. See :ref:`profiling-permissions`.
259+
260+
The ``dump`` command supports the following options:
261+
262+
``-a``, ``--all-threads``
263+
Dump every thread in the target process. Without this flag only the main
264+
thread is shown.
265+
266+
``--native``
267+
Include synthetic ``<native>`` frames marking transitions into C
268+
extensions or other non-Python code.
269+
270+
``--no-gc``
271+
Hide the synthetic ``<GC>`` frames that mark active garbage collection.
272+
273+
``--opcodes``
274+
Annotate each frame with the bytecode opcode the thread is currently
275+
executing (for example, ``opcode=CALL_KW``). Useful for
276+
instruction-level investigation, including identifying specializations
277+
chosen by the adaptive interpreter.
278+
279+
``--async-aware``
280+
Reconstruct stacks across ``await`` boundaries. ``dump`` walks the task
281+
graph and emits one section per task, with ``<task>`` markers separating
282+
coroutines awaiting each other.
283+
284+
``--async-mode {running,all}``
285+
Controls which tasks are included when ``--async-aware`` is enabled.
286+
``running`` shows only the task currently executing on each thread;
287+
``all`` (the default for ``dump``) also includes tasks suspended on a
288+
wait. ``attach``'s default for this flag is ``running``; ``dump``
289+
defaults to ``all`` because a single snapshot is most useful when it
290+
shows the full task graph.
291+
292+
``--blocking``
293+
Pause every thread in the target while reading its stack and resume
294+
them after. Guarantees a fully consistent snapshot at the cost of
295+
briefly stopping the target. Without it, ``dump`` reads memory while
296+
the target keeps running, which is faster but can occasionally produce
297+
a torn stack.
298+
299+
220300
.. _replay-command:
221301

222302
The ``replay`` command
@@ -1441,11 +1521,52 @@ Global options
14411521

14421522
Attach to and profile a running process by PID.
14431523

1524+
.. option:: dump
1525+
1526+
Print a single one-shot snapshot of a running process's Python stack.
1527+
14441528
.. option:: replay
14451529

14461530
Convert a binary profile file to another output format.
14471531

14481532

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

Doc/whatsnew/3.15.rst

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

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

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)