Skip to content

Commit ea51e74

Browse files
gh-138122: Add thread status statistics to flamegraph profiler (#141900)
Co-authored-by: ivonastojanovic <[email protected]>
1 parent db098a4 commit ea51e74

File tree

8 files changed

+777
-21
lines changed

8 files changed

+777
-21
lines changed

Lib/profiling/sampling/collector.py

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ def export(self, filename):
1919
"""Export collected data to a file."""
2020

2121
def _iter_all_frames(self, stack_frames, skip_idle=False):
22-
"""Iterate over all frame stacks from all interpreters and threads."""
2322
for interpreter_info in stack_frames:
2423
for thread_info in interpreter_info.threads:
2524
# skip_idle now means: skip if thread is not actively running
@@ -33,3 +32,83 @@ def _iter_all_frames(self, stack_frames, skip_idle=False):
3332
frames = thread_info.frame_info
3433
if frames:
3534
yield frames, thread_info.thread_id
35+
36+
def _is_gc_frame(self, frame):
37+
if isinstance(frame, tuple):
38+
funcname = frame[2] if len(frame) >= 3 else ""
39+
else:
40+
funcname = getattr(frame, "funcname", "")
41+
42+
return "<GC>" in funcname or "gc_collect" in funcname
43+
44+
def _collect_thread_status_stats(self, stack_frames):
45+
"""Collect aggregate and per-thread status statistics from a sample.
46+
47+
Returns:
48+
tuple: (aggregate_status_counts, has_gc_frame, per_thread_stats)
49+
- aggregate_status_counts: dict with has_gil, on_cpu, etc.
50+
- has_gc_frame: bool indicating if any thread has GC frames
51+
- per_thread_stats: dict mapping thread_id to per-thread counts
52+
"""
53+
status_counts = {
54+
"has_gil": 0,
55+
"on_cpu": 0,
56+
"gil_requested": 0,
57+
"unknown": 0,
58+
"total": 0,
59+
}
60+
has_gc_frame = False
61+
per_thread_stats = {}
62+
63+
for interpreter_info in stack_frames:
64+
threads = getattr(interpreter_info, "threads", [])
65+
for thread_info in threads:
66+
status_counts["total"] += 1
67+
68+
# Track thread status using bit flags
69+
status_flags = getattr(thread_info, "status", 0)
70+
71+
if status_flags & THREAD_STATUS_HAS_GIL:
72+
status_counts["has_gil"] += 1
73+
if status_flags & THREAD_STATUS_ON_CPU:
74+
status_counts["on_cpu"] += 1
75+
if status_flags & THREAD_STATUS_GIL_REQUESTED:
76+
status_counts["gil_requested"] += 1
77+
if status_flags & THREAD_STATUS_UNKNOWN:
78+
status_counts["unknown"] += 1
79+
80+
# Track per-thread statistics
81+
thread_id = getattr(thread_info, "thread_id", None)
82+
if thread_id is not None:
83+
if thread_id not in per_thread_stats:
84+
per_thread_stats[thread_id] = {
85+
"has_gil": 0,
86+
"on_cpu": 0,
87+
"gil_requested": 0,
88+
"unknown": 0,
89+
"total": 0,
90+
"gc_samples": 0,
91+
}
92+
93+
thread_stats = per_thread_stats[thread_id]
94+
thread_stats["total"] += 1
95+
96+
if status_flags & THREAD_STATUS_HAS_GIL:
97+
thread_stats["has_gil"] += 1
98+
if status_flags & THREAD_STATUS_ON_CPU:
99+
thread_stats["on_cpu"] += 1
100+
if status_flags & THREAD_STATUS_GIL_REQUESTED:
101+
thread_stats["gil_requested"] += 1
102+
if status_flags & THREAD_STATUS_UNKNOWN:
103+
thread_stats["unknown"] += 1
104+
105+
# Check for GC frames in this thread
106+
frames = getattr(thread_info, "frame_info", None)
107+
if frames:
108+
for frame in frames:
109+
if self._is_gc_frame(frame):
110+
thread_stats["gc_samples"] += 1
111+
has_gc_frame = True
112+
break
113+
114+
return status_counts, has_gc_frame, per_thread_stats

Lib/profiling/sampling/flamegraph.css

Lines changed: 174 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,143 @@ body {
108108
gap: 20px;
109109
}
110110

111+
/* Compact Thread Stats Bar - Colorful Square Design */
112+
.thread-stats-bar {
113+
background: rgba(255, 255, 255, 0.95);
114+
padding: 12px 24px;
115+
display: flex;
116+
align-items: center;
117+
justify-content: center;
118+
gap: 16px;
119+
font-size: 13px;
120+
box-shadow: 0 2px 8px rgba(55, 118, 171, 0.2);
121+
}
122+
123+
.thread-stat-item {
124+
display: inline-flex;
125+
align-items: center;
126+
gap: 8px;
127+
background: white;
128+
padding: 6px 14px;
129+
border-radius: 4px;
130+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
131+
transition: all 0.3s ease;
132+
border: 2px solid;
133+
min-width: 115px;
134+
justify-content: center;
135+
animation: fadeIn 0.5s ease-out backwards;
136+
}
137+
138+
.thread-stat-item:nth-child(1) { animation-delay: 0s; }
139+
.thread-stat-item:nth-child(3) { animation-delay: 0.1s; }
140+
.thread-stat-item:nth-child(5) { animation-delay: 0.2s; }
141+
.thread-stat-item:nth-child(7) { animation-delay: 0.3s; }
142+
143+
@keyframes fadeIn {
144+
from {
145+
opacity: 0;
146+
}
147+
to {
148+
opacity: 1;
149+
}
150+
}
151+
152+
@keyframes slideUp {
153+
from {
154+
opacity: 0;
155+
transform: translateY(15px);
156+
}
157+
to {
158+
opacity: 1;
159+
transform: translateY(0);
160+
}
161+
}
162+
163+
@keyframes gentlePulse {
164+
0%, 100% { box-shadow: 0 2px 8px rgba(55, 118, 171, 0.15); }
165+
50% { box-shadow: 0 2px 16px rgba(55, 118, 171, 0.4); }
166+
}
167+
168+
/* Color-coded borders and subtle glow on hover */
169+
#gil-held-stat {
170+
--stat-color: 40, 167, 69;
171+
border-color: rgb(var(--stat-color));
172+
background: linear-gradient(135deg, rgba(var(--stat-color), 0.06) 0%, #ffffff 100%);
173+
}
174+
175+
#gil-released-stat {
176+
--stat-color: 220, 53, 69;
177+
border-color: rgb(var(--stat-color));
178+
background: linear-gradient(135deg, rgba(var(--stat-color), 0.06) 0%, #ffffff 100%);
179+
}
180+
181+
#gil-waiting-stat {
182+
--stat-color: 255, 193, 7;
183+
border-color: rgb(var(--stat-color));
184+
background: linear-gradient(135deg, rgba(var(--stat-color), 0.06) 0%, #ffffff 100%);
185+
}
186+
187+
#gc-stat {
188+
--stat-color: 111, 66, 193;
189+
border-color: rgb(var(--stat-color));
190+
background: linear-gradient(135deg, rgba(var(--stat-color), 0.06) 0%, #ffffff 100%);
191+
}
192+
193+
#gil-held-stat:hover,
194+
#gil-released-stat:hover,
195+
#gil-waiting-stat:hover,
196+
#gc-stat:hover {
197+
box-shadow: 0 0 12px rgba(var(--stat-color), 0.4), 0 1px 3px rgba(0, 0, 0, 0.08);
198+
}
199+
200+
.thread-stat-item .stat-label {
201+
color: #5a6c7d;
202+
font-weight: 600;
203+
font-size: 11px;
204+
letter-spacing: 0.3px;
205+
}
206+
207+
.thread-stat-item .stat-value {
208+
color: #2e3338;
209+
font-weight: 800;
210+
font-size: 14px;
211+
font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
212+
}
213+
214+
.thread-stat-separator {
215+
color: rgba(0, 0, 0, 0.15);
216+
font-weight: 300;
217+
font-size: 16px;
218+
position: relative;
219+
z-index: 1;
220+
}
221+
222+
/* Responsive - stack on small screens */
223+
@media (max-width: 768px) {
224+
.thread-stats-bar {
225+
flex-wrap: wrap;
226+
gap: 8px;
227+
font-size: 11px;
228+
padding: 10px 16px;
229+
}
230+
231+
.thread-stat-item {
232+
padding: 4px 10px;
233+
}
234+
235+
.thread-stat-item .stat-label {
236+
font-size: 11px;
237+
}
238+
239+
.thread-stat-item .stat-value {
240+
font-size: 12px;
241+
}
242+
243+
.thread-stat-separator {
244+
display: none;
245+
}
246+
}
247+
111248
.stat-card {
112249
background: #ffffff;
113250
border: 1px solid #e9ecef;
@@ -119,8 +256,13 @@ body {
119256
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
120257
transition: all 0.2s ease;
121258
min-height: 120px;
259+
animation: slideUp 0.4s ease-out backwards;
122260
}
123261

262+
.stat-card:nth-child(1) { animation-delay: 0.1s; }
263+
.stat-card:nth-child(2) { animation-delay: 0.2s; }
264+
.stat-card:nth-child(3) { animation-delay: 0.3s; }
265+
124266
.stat-card:hover {
125267
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
126268
transform: translateY(-2px);
@@ -218,6 +360,11 @@ body {
218360
box-shadow: 0 4px 8px rgba(55, 118, 171, 0.3);
219361
}
220362

363+
.controls button:active {
364+
transform: translateY(1px);
365+
box-shadow: 0 1px 2px rgba(55, 118, 171, 0.2);
366+
}
367+
221368
.controls button.secondary {
222369
background: #ffd43b;
223370
color: #2e3338;
@@ -227,6 +374,10 @@ body {
227374
background: #ffcd02;
228375
}
229376

377+
.controls button.secondary:active {
378+
background: #e6b800;
379+
}
380+
230381
.thread-filter-wrapper {
231382
display: none;
232383
align-items: center;
@@ -368,11 +519,14 @@ body {
368519
display: flex;
369520
align-items: center;
370521
justify-content: center;
371-
transition: background 0.2s;
522+
transition: background 0.2s, transform 0.2s;
523+
animation: gentlePulse 3s ease-in-out infinite;
372524
}
373525

374526
#show-info-btn:hover {
375527
background: #2d5aa0;
528+
animation: none;
529+
transform: scale(1.05);
376530
}
377531

378532
#close-info-btn {
@@ -486,3 +640,22 @@ body {
486640
font-size: 12px !important;
487641
}
488642
}
643+
644+
/* Accessibility: visible focus states */
645+
button:focus-visible,
646+
select:focus-visible,
647+
input:focus-visible {
648+
outline: 2px solid #ffd43b;
649+
outline-offset: 2px;
650+
}
651+
652+
/* Smooth panel transitions */
653+
.legend-panel,
654+
.info-panel {
655+
transition: opacity 0.2s ease, transform 0.2s ease;
656+
}
657+
658+
.legend-panel[style*="block"],
659+
.info-panel[style*="block"] {
660+
animation: slideUp 0.2s ease-out;
661+
}

0 commit comments

Comments
 (0)