Skip to content

Commit b338308

Browse files
authored
gh-141645: Add a TUI mode to the new tachyon profiler (#141646)
1 parent e90061f commit b338308

File tree

17 files changed

+5521
-62
lines changed

17 files changed

+5521
-62
lines changed

Lib/profiling/sampling/collector.py

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,19 @@
11
from abc import ABC, abstractmethod
2-
3-
# Thread status flags
4-
try:
5-
from _remote_debugging import THREAD_STATUS_HAS_GIL, THREAD_STATUS_ON_CPU, THREAD_STATUS_UNKNOWN, THREAD_STATUS_GIL_REQUESTED
6-
except ImportError:
7-
# Fallback for tests or when module is not available
8-
THREAD_STATUS_HAS_GIL = (1 << 0)
9-
THREAD_STATUS_ON_CPU = (1 << 1)
10-
THREAD_STATUS_UNKNOWN = (1 << 2)
11-
THREAD_STATUS_GIL_REQUESTED = (1 << 3)
2+
from .constants import (
3+
THREAD_STATUS_HAS_GIL,
4+
THREAD_STATUS_ON_CPU,
5+
THREAD_STATUS_UNKNOWN,
6+
THREAD_STATUS_GIL_REQUESTED,
7+
)
128

139
class Collector(ABC):
1410
@abstractmethod
1511
def collect(self, stack_frames):
1612
"""Collect profiling data from stack frames."""
1713

14+
def collect_failed_sample(self):
15+
"""Collect data about a failed sample attempt."""
16+
1817
@abstractmethod
1918
def export(self, filename):
2019
"""Export collected data to a file."""
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
"""Constants for the sampling profiler."""
2+
3+
# Profiling mode constants
4+
PROFILING_MODE_WALL = 0
5+
PROFILING_MODE_CPU = 1
6+
PROFILING_MODE_GIL = 2
7+
PROFILING_MODE_ALL = 3 # Combines GIL + CPU checks
8+
9+
# Sort mode constants
10+
SORT_MODE_NSAMPLES = 0
11+
SORT_MODE_TOTTIME = 1
12+
SORT_MODE_CUMTIME = 2
13+
SORT_MODE_SAMPLE_PCT = 3
14+
SORT_MODE_CUMUL_PCT = 4
15+
SORT_MODE_NSAMPLES_CUMUL = 5
16+
17+
# Thread status flags
18+
try:
19+
from _remote_debugging import (
20+
THREAD_STATUS_HAS_GIL,
21+
THREAD_STATUS_ON_CPU,
22+
THREAD_STATUS_UNKNOWN,
23+
THREAD_STATUS_GIL_REQUESTED,
24+
)
25+
except ImportError:
26+
# Fallback for tests or when module is not available
27+
THREAD_STATUS_HAS_GIL = (1 << 0)
28+
THREAD_STATUS_ON_CPU = (1 << 1)
29+
THREAD_STATUS_UNKNOWN = (1 << 2)
30+
THREAD_STATUS_GIL_REQUESTED = (1 << 3)
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
"""Live profiling collector that displays top-like statistics using curses.
2+
3+
┌─────────────────────────────┐
4+
│ Target Python Process │
5+
│ (being profiled) │
6+
└──────────────┬──────────────┘
7+
│ Stack sampling at
8+
│ configured interval
9+
│ (e.g., 10000µs)
10+
11+
┌─────────────────────────────┐
12+
│ LiveStatsCollector │
13+
│ ┌───────────────────────┐ │
14+
│ │ collect() │ │ Aggregates samples
15+
│ │ - Iterates frames │ │ into statistics
16+
│ │ - Updates counters │ │
17+
│ └───────────┬───────────┘ │
18+
│ │ │
19+
│ ▼ │
20+
│ ┌───────────────────────┐ │
21+
│ │ Data Storage │ │
22+
│ │ - result dict │ │ Tracks per-function:
23+
│ │ - direct_calls │ │ • Direct samples
24+
│ │ - cumulative_calls │ │ • Cumulative samples
25+
│ └───────────┬───────────┘ │ • Derived time stats
26+
│ │ │
27+
│ ▼ │
28+
│ ┌───────────────────────┐ │
29+
│ │ Display Update │ │
30+
│ │ (10Hz by default) │ │ Rate-limited refresh
31+
│ └───────────┬───────────┘ │
32+
└──────────────┼──────────────┘
33+
34+
35+
┌─────────────────────────────┐
36+
│ DisplayInterface │
37+
│ (Abstract layer) │
38+
└──────────────┬──────────────┘
39+
┌───────┴────────┐
40+
│ │
41+
┌──────────▼────────┐ ┌───▼──────────┐
42+
│ CursesDisplay │ │ MockDisplay │
43+
│ - Real terminal │ │ - Testing │
44+
│ - ncurses backend │ │ - No UI │
45+
└─────────┬─────────┘ └──────────────┘
46+
47+
48+
┌─────────────────────────────────────┐
49+
│ Widget-Based Rendering │
50+
│ ┌─────────────────────────────────┐ │
51+
│ │ HeaderWidget │ │
52+
│ │ • PID, uptime, time, interval │ │
53+
│ │ • Sample stats & progress bar │ │
54+
│ │ • Efficiency bar │ │
55+
│ │ • Thread status & GC stats │ │
56+
│ │ • Function summary │ │
57+
│ │ • Top 3 hottest functions │ │
58+
│ ├─────────────────────────────────┤ │
59+
│ │ TableWidget │ │
60+
│ │ • Column headers (sortable) │ │ Interactive display
61+
│ │ • Stats rows (scrolling) │ │ with keyboard controls:
62+
│ │ - nsamples % time │ │ s: sort, p: pause
63+
│ │ - function file:line │ │ r: reset, /: filter
64+
│ ├─────────────────────────────────┤ │ q: quit, h: help
65+
│ │ FooterWidget │ │
66+
│ │ • Legend and status │ │
67+
│ │ • Filter input prompt │ │
68+
│ └─────────────────────────────────┘ │
69+
└─────────────────────────────────────┘
70+
71+
Architecture:
72+
73+
The live collector is organized into four layers. The data collection layer
74+
(LiveStatsCollector) aggregates stack samples into per-function statistics without
75+
any knowledge of how they will be presented. The display abstraction layer
76+
(DisplayInterface) defines rendering operations without coupling to curses or any
77+
specific UI framework. The widget layer (Widget, HeaderWidget, TableWidget,
78+
FooterWidget, HelpWidget, ProgressBarWidget) encapsulates individual UI components
79+
with their own rendering logic, promoting modularity and reusability. The
80+
presentation layer (CursesDisplay/MockDisplay) implements the actual rendering for
81+
terminal output and testing.
82+
83+
The system runs two independent update loops. The sampling loop is driven by the
84+
profiler at the configured interval (e.g., 10000µs) and continuously collects
85+
stack frames and updates statistics. The display loop runs at a fixed refresh rate
86+
(default 10Hz) and updates the terminal independently of sampling frequency. This
87+
separation allows high-frequency sampling without overwhelming the terminal with
88+
constant redraws.
89+
90+
Statistics are computed incrementally as samples arrive. The collector maintains
91+
running counters (direct calls and cumulative calls) in a dictionary keyed by
92+
function location. Derived metrics like time estimates and percentages are computed
93+
on-demand during display updates rather than being stored, which minimizes memory
94+
overhead as the number of tracked functions grows.
95+
96+
User input is processed asynchronously during display updates using non-blocking I/O.
97+
This allows interactive controls (sorting, filtering, pausing) without interrupting
98+
the data collection pipeline. The collector maintains mode flags (paused,
99+
filter_input_mode) that affect what gets displayed but not what gets collected.
100+
101+
"""
102+
103+
# Re-export all public classes and constants for backward compatibility
104+
from .collector import LiveStatsCollector
105+
from .display import DisplayInterface, CursesDisplay, MockDisplay
106+
from .widgets import (
107+
Widget,
108+
ProgressBarWidget,
109+
HeaderWidget,
110+
TableWidget,
111+
FooterWidget,
112+
HelpWidget,
113+
)
114+
from .constants import (
115+
MICROSECONDS_PER_SECOND,
116+
DISPLAY_UPDATE_HZ,
117+
DISPLAY_UPDATE_INTERVAL,
118+
MIN_TERMINAL_WIDTH,
119+
MIN_TERMINAL_HEIGHT,
120+
WIDTH_THRESHOLD_SAMPLE_PCT,
121+
WIDTH_THRESHOLD_TOTTIME,
122+
WIDTH_THRESHOLD_CUMUL_PCT,
123+
WIDTH_THRESHOLD_CUMTIME,
124+
HEADER_LINES,
125+
FOOTER_LINES,
126+
SAFETY_MARGIN,
127+
TOP_FUNCTIONS_DISPLAY_COUNT,
128+
COL_WIDTH_NSAMPLES,
129+
COL_SPACING,
130+
COL_WIDTH_SAMPLE_PCT,
131+
COL_WIDTH_TIME,
132+
MIN_FUNC_NAME_WIDTH,
133+
MAX_FUNC_NAME_WIDTH,
134+
MIN_AVAILABLE_SPACE,
135+
MIN_BAR_WIDTH,
136+
MAX_SAMPLE_RATE_BAR_WIDTH,
137+
MAX_EFFICIENCY_BAR_WIDTH,
138+
MIN_SAMPLE_RATE_FOR_SCALING,
139+
FINISHED_BANNER_EXTRA_LINES,
140+
COLOR_PAIR_HEADER_BG,
141+
COLOR_PAIR_CYAN,
142+
COLOR_PAIR_YELLOW,
143+
COLOR_PAIR_GREEN,
144+
COLOR_PAIR_MAGENTA,
145+
COLOR_PAIR_RED,
146+
COLOR_PAIR_SORTED_HEADER,
147+
DEFAULT_SORT_BY,
148+
DEFAULT_DISPLAY_LIMIT,
149+
)
150+
151+
__all__ = [
152+
# Main collector
153+
"LiveStatsCollector",
154+
# Display interfaces
155+
"DisplayInterface",
156+
"CursesDisplay",
157+
"MockDisplay",
158+
# Widgets
159+
"Widget",
160+
"ProgressBarWidget",
161+
"HeaderWidget",
162+
"TableWidget",
163+
"FooterWidget",
164+
"HelpWidget",
165+
# Constants
166+
"MICROSECONDS_PER_SECOND",
167+
"DISPLAY_UPDATE_HZ",
168+
"DISPLAY_UPDATE_INTERVAL",
169+
"MIN_TERMINAL_WIDTH",
170+
"MIN_TERMINAL_HEIGHT",
171+
"WIDTH_THRESHOLD_SAMPLE_PCT",
172+
"WIDTH_THRESHOLD_TOTTIME",
173+
"WIDTH_THRESHOLD_CUMUL_PCT",
174+
"WIDTH_THRESHOLD_CUMTIME",
175+
"HEADER_LINES",
176+
"FOOTER_LINES",
177+
"SAFETY_MARGIN",
178+
"TOP_FUNCTIONS_DISPLAY_COUNT",
179+
"COL_WIDTH_NSAMPLES",
180+
"COL_SPACING",
181+
"COL_WIDTH_SAMPLE_PCT",
182+
"COL_WIDTH_TIME",
183+
"MIN_FUNC_NAME_WIDTH",
184+
"MAX_FUNC_NAME_WIDTH",
185+
"MIN_AVAILABLE_SPACE",
186+
"MIN_BAR_WIDTH",
187+
"MAX_SAMPLE_RATE_BAR_WIDTH",
188+
"MAX_EFFICIENCY_BAR_WIDTH",
189+
"MIN_SAMPLE_RATE_FOR_SCALING",
190+
"FINISHED_BANNER_EXTRA_LINES",
191+
"COLOR_PAIR_HEADER_BG",
192+
"COLOR_PAIR_CYAN",
193+
"COLOR_PAIR_YELLOW",
194+
"COLOR_PAIR_GREEN",
195+
"COLOR_PAIR_MAGENTA",
196+
"COLOR_PAIR_RED",
197+
"COLOR_PAIR_SORTED_HEADER",
198+
"DEFAULT_SORT_BY",
199+
"DEFAULT_DISPLAY_LIMIT",
200+
]

0 commit comments

Comments
 (0)