Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 80 additions & 1 deletion Lib/profiling/sampling/collector.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ def export(self, filename):
"""Export collected data to a file."""

def _iter_all_frames(self, stack_frames, skip_idle=False):
"""Iterate over all frame stacks from all interpreters and threads."""
for interpreter_info in stack_frames:
for thread_info in interpreter_info.threads:
# skip_idle now means: skip if thread is not actively running
Expand All @@ -33,3 +32,83 @@ def _iter_all_frames(self, stack_frames, skip_idle=False):
frames = thread_info.frame_info
if frames:
yield frames, thread_info.thread_id

def _is_gc_frame(self, frame):
if isinstance(frame, tuple):
funcname = frame[2] if len(frame) >= 3 else ""
else:
funcname = getattr(frame, "funcname", "")

return "<GC>" in funcname or "gc_collect" in funcname

def _collect_thread_status_stats(self, stack_frames):
"""Collect aggregate and per-thread status statistics from a sample.

Returns:
tuple: (aggregate_status_counts, has_gc_frame, per_thread_stats)
- aggregate_status_counts: dict with has_gil, on_cpu, etc.
- has_gc_frame: bool indicating if any thread has GC frames
- per_thread_stats: dict mapping thread_id to per-thread counts
"""
status_counts = {
"has_gil": 0,
"on_cpu": 0,
"gil_requested": 0,
"unknown": 0,
"total": 0,
}
has_gc_frame = False
per_thread_stats = {}

for interpreter_info in stack_frames:
threads = getattr(interpreter_info, "threads", [])
for thread_info in threads:
status_counts["total"] += 1

# Track thread status using bit flags
status_flags = getattr(thread_info, "status", 0)

if status_flags & THREAD_STATUS_HAS_GIL:
status_counts["has_gil"] += 1
if status_flags & THREAD_STATUS_ON_CPU:
status_counts["on_cpu"] += 1
if status_flags & THREAD_STATUS_GIL_REQUESTED:
status_counts["gil_requested"] += 1
if status_flags & THREAD_STATUS_UNKNOWN:
status_counts["unknown"] += 1

# Track per-thread statistics
thread_id = getattr(thread_info, "thread_id", None)
if thread_id is not None:
if thread_id not in per_thread_stats:
per_thread_stats[thread_id] = {
"has_gil": 0,
"on_cpu": 0,
"gil_requested": 0,
"unknown": 0,
"total": 0,
"gc_samples": 0,
}

thread_stats = per_thread_stats[thread_id]
thread_stats["total"] += 1

if status_flags & THREAD_STATUS_HAS_GIL:
thread_stats["has_gil"] += 1
if status_flags & THREAD_STATUS_ON_CPU:
thread_stats["on_cpu"] += 1
if status_flags & THREAD_STATUS_GIL_REQUESTED:
thread_stats["gil_requested"] += 1
if status_flags & THREAD_STATUS_UNKNOWN:
thread_stats["unknown"] += 1

# Check for GC frames in this thread
frames = getattr(thread_info, "frame_info", None)
if frames:
for frame in frames:
if self._is_gc_frame(frame):
thread_stats["gc_samples"] += 1
has_gc_frame = True
break

return status_counts, has_gc_frame, per_thread_stats
175 changes: 174 additions & 1 deletion Lib/profiling/sampling/flamegraph.css
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,143 @@ body {
gap: 20px;
}

/* Compact Thread Stats Bar - Colorful Square Design */
.thread-stats-bar {
background: rgba(255, 255, 255, 0.95);
padding: 12px 24px;
display: flex;
align-items: center;
justify-content: center;
gap: 16px;
font-size: 13px;
box-shadow: 0 2px 8px rgba(55, 118, 171, 0.2);
}

.thread-stat-item {
display: inline-flex;
align-items: center;
gap: 8px;
background: white;
padding: 6px 14px;
border-radius: 4px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
transition: all 0.3s ease;
border: 2px solid;
min-width: 115px;
justify-content: center;
animation: fadeIn 0.5s ease-out backwards;
}

.thread-stat-item:nth-child(1) { animation-delay: 0s; }
.thread-stat-item:nth-child(3) { animation-delay: 0.1s; }
.thread-stat-item:nth-child(5) { animation-delay: 0.2s; }
.thread-stat-item:nth-child(7) { animation-delay: 0.3s; }

@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}

@keyframes slideUp {
from {
opacity: 0;
transform: translateY(15px);
}
to {
opacity: 1;
transform: translateY(0);
}
}

@keyframes gentlePulse {
0%, 100% { box-shadow: 0 2px 8px rgba(55, 118, 171, 0.15); }
50% { box-shadow: 0 2px 16px rgba(55, 118, 171, 0.4); }
}

/* Color-coded borders and subtle glow on hover */
#gil-held-stat {
--stat-color: 40, 167, 69;
border-color: rgb(var(--stat-color));
background: linear-gradient(135deg, rgba(var(--stat-color), 0.06) 0%, #ffffff 100%);
}

#gil-released-stat {
--stat-color: 220, 53, 69;
border-color: rgb(var(--stat-color));
background: linear-gradient(135deg, rgba(var(--stat-color), 0.06) 0%, #ffffff 100%);
}

#gil-waiting-stat {
--stat-color: 255, 193, 7;
border-color: rgb(var(--stat-color));
background: linear-gradient(135deg, rgba(var(--stat-color), 0.06) 0%, #ffffff 100%);
}

#gc-stat {
--stat-color: 111, 66, 193;
border-color: rgb(var(--stat-color));
background: linear-gradient(135deg, rgba(var(--stat-color), 0.06) 0%, #ffffff 100%);
}

#gil-held-stat:hover,
#gil-released-stat:hover,
#gil-waiting-stat:hover,
#gc-stat:hover {
box-shadow: 0 0 12px rgba(var(--stat-color), 0.4), 0 1px 3px rgba(0, 0, 0, 0.08);
}

.thread-stat-item .stat-label {
color: #5a6c7d;
font-weight: 600;
font-size: 11px;
letter-spacing: 0.3px;
}

.thread-stat-item .stat-value {
color: #2e3338;
font-weight: 800;
font-size: 14px;
font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
}

.thread-stat-separator {
color: rgba(0, 0, 0, 0.15);
font-weight: 300;
font-size: 16px;
position: relative;
z-index: 1;
}

/* Responsive - stack on small screens */
@media (max-width: 768px) {
.thread-stats-bar {
flex-wrap: wrap;
gap: 8px;
font-size: 11px;
padding: 10px 16px;
}

.thread-stat-item {
padding: 4px 10px;
}

.thread-stat-item .stat-label {
font-size: 11px;
}

.thread-stat-item .stat-value {
font-size: 12px;
}

.thread-stat-separator {
display: none;
}
}

.stat-card {
background: #ffffff;
border: 1px solid #e9ecef;
Expand All @@ -119,8 +256,13 @@ body {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
transition: all 0.2s ease;
min-height: 120px;
animation: slideUp 0.4s ease-out backwards;
}

.stat-card:nth-child(1) { animation-delay: 0.1s; }
.stat-card:nth-child(2) { animation-delay: 0.2s; }
.stat-card:nth-child(3) { animation-delay: 0.3s; }

.stat-card:hover {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
Expand Down Expand Up @@ -218,6 +360,11 @@ body {
box-shadow: 0 4px 8px rgba(55, 118, 171, 0.3);
}

.controls button:active {
transform: translateY(1px);
box-shadow: 0 1px 2px rgba(55, 118, 171, 0.2);
}

.controls button.secondary {
background: #ffd43b;
color: #2e3338;
Expand All @@ -227,6 +374,10 @@ body {
background: #ffcd02;
}

.controls button.secondary:active {
background: #e6b800;
}

.thread-filter-wrapper {
display: none;
align-items: center;
Expand Down Expand Up @@ -368,11 +519,14 @@ body {
display: flex;
align-items: center;
justify-content: center;
transition: background 0.2s;
transition: background 0.2s, transform 0.2s;
animation: gentlePulse 3s ease-in-out infinite;
}

#show-info-btn:hover {
background: #2d5aa0;
animation: none;
transform: scale(1.05);
}

#close-info-btn {
Expand Down Expand Up @@ -486,3 +640,22 @@ body {
font-size: 12px !important;
}
}

/* Accessibility: visible focus states */
button:focus-visible,
select:focus-visible,
input:focus-visible {
outline: 2px solid #ffd43b;
outline-offset: 2px;
}

/* Smooth panel transitions */
.legend-panel,
.info-panel {
transition: opacity 0.2s ease, transform 0.2s ease;
}

.legend-panel[style*="block"],
.info-panel[style*="block"] {
animation: slideUp 0.2s ease-out;
}
Loading
Loading