Skip to content

Commit 7ecbe13

Browse files
committed
Add stats
1 parent 5ba13c5 commit 7ecbe13

File tree

7 files changed

+318
-22
lines changed

7 files changed

+318
-22
lines changed

Lib/profiling/sampling/sample.py

Lines changed: 90 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,21 +27,24 @@
2727

2828

2929
class SampleProfiler:
30-
def __init__(self, pid, sample_interval_usec, all_threads, *, mode=PROFILING_MODE_WALL, native=False, gc=True, skip_non_matching_threads=True):
30+
def __init__(self, pid, sample_interval_usec, all_threads, *, mode=PROFILING_MODE_WALL, native=False, gc=True, skip_non_matching_threads=True, collect_stats=False):
3131
self.pid = pid
3232
self.sample_interval_usec = sample_interval_usec
3333
self.all_threads = all_threads
3434
self.mode = mode # Store mode for later use
35+
self.collect_stats = collect_stats
3536
if _FREE_THREADED_BUILD:
3637
self.unwinder = _remote_debugging.RemoteUnwinder(
3738
self.pid, all_threads=self.all_threads, mode=mode, native=native, gc=gc,
38-
skip_non_matching_threads=skip_non_matching_threads, cache_frames=True
39+
skip_non_matching_threads=skip_non_matching_threads, cache_frames=True,
40+
stats=collect_stats
3941
)
4042
else:
4143
only_active_threads = bool(self.all_threads)
4244
self.unwinder = _remote_debugging.RemoteUnwinder(
4345
self.pid, only_active_thread=only_active_threads, mode=mode, native=native, gc=gc,
44-
skip_non_matching_threads=skip_non_matching_threads, cache_frames=True
46+
skip_non_matching_threads=skip_non_matching_threads, cache_frames=True,
47+
stats=collect_stats
4548
)
4649
# Track sample intervals and total sample count
4750
self.sample_intervals = deque(maxlen=100)
@@ -124,6 +127,10 @@ def sample(self, collector, duration_sec=10):
124127
print(f"Sample rate: {sample_rate:.2f} samples/sec")
125128
print(f"Error rate: {error_rate:.2f}%")
126129

130+
# Print unwinder stats if stats collection is enabled
131+
if self.collect_stats:
132+
self._print_unwinder_stats()
133+
127134
# Pass stats to flamegraph collector if it's the right type
128135
if hasattr(collector, 'set_stats'):
129136
collector.set_stats(self.sample_interval_usec, running_time, sample_rate, error_rate, missed_samples, mode=self.mode)
@@ -171,17 +178,88 @@ def _print_realtime_stats(self):
171178
(1.0 / min_hz) * 1_000_000 if min_hz > 0 else 0
172179
) # Max time = Min Hz
173180

181+
# Build cache stats string if stats collection is enabled
182+
cache_stats_str = ""
183+
if self.collect_stats:
184+
try:
185+
stats = self.unwinder.get_stats()
186+
hits = stats.get('frame_cache_hits', 0)
187+
partial = stats.get('frame_cache_partial_hits', 0)
188+
misses = stats.get('frame_cache_misses', 0)
189+
total = hits + partial + misses
190+
if total > 0:
191+
hit_pct = (hits + partial) / total * 100
192+
cache_stats_str = f" {ANSIColors.MAGENTA}Cache: {hit_pct:.1f}% ({hits}+{partial}/{misses}){ANSIColors.RESET}"
193+
except RuntimeError:
194+
pass
195+
174196
# Clear line and print stats
175197
print(
176-
f"\r\033[K{ANSIColors.BOLD_BLUE}Real-time sampling stats:{ANSIColors.RESET} "
177-
f"{ANSIColors.YELLOW}Mean: {mean_hz:.1f}Hz ({mean_us_per_sample:.2f}µs){ANSIColors.RESET} "
178-
f"{ANSIColors.GREEN}Min: {min_hz:.1f}Hz ({max_us_per_sample:.2f}µs){ANSIColors.RESET} "
179-
f"{ANSIColors.RED}Max: {max_hz:.1f}Hz ({min_us_per_sample:.2f}µs){ANSIColors.RESET} "
180-
f"{ANSIColors.CYAN}Samples: {self.total_samples}{ANSIColors.RESET}",
198+
f"\r\033[K{ANSIColors.BOLD_BLUE}Stats:{ANSIColors.RESET} "
199+
f"{ANSIColors.YELLOW}{mean_hz:.1f}Hz ({mean_us_per_sample:.1f}µs){ANSIColors.RESET} "
200+
f"{ANSIColors.GREEN}Min: {min_hz:.1f}Hz{ANSIColors.RESET} "
201+
f"{ANSIColors.RED}Max: {max_hz:.1f}Hz{ANSIColors.RESET} "
202+
f"{ANSIColors.CYAN}N={self.total_samples}{ANSIColors.RESET}"
203+
f"{cache_stats_str}",
181204
end="",
182205
flush=True,
183206
)
184207

208+
def _print_unwinder_stats(self):
209+
"""Print unwinder statistics including cache performance."""
210+
try:
211+
stats = self.unwinder.get_stats()
212+
except RuntimeError:
213+
return # Stats not enabled
214+
215+
print(f"\n{ANSIColors.BOLD_BLUE}{'='*50}{ANSIColors.RESET}")
216+
print(f"{ANSIColors.BOLD_BLUE}Unwinder Statistics:{ANSIColors.RESET}")
217+
218+
# Frame cache stats
219+
total_samples = stats.get('total_samples', 0)
220+
frame_cache_hits = stats.get('frame_cache_hits', 0)
221+
frame_cache_partial_hits = stats.get('frame_cache_partial_hits', 0)
222+
frame_cache_misses = stats.get('frame_cache_misses', 0)
223+
total_lookups = frame_cache_hits + frame_cache_partial_hits + frame_cache_misses
224+
225+
# Calculate percentages
226+
hits_pct = (frame_cache_hits / total_lookups * 100) if total_lookups > 0 else 0
227+
partial_pct = (frame_cache_partial_hits / total_lookups * 100) if total_lookups > 0 else 0
228+
misses_pct = (frame_cache_misses / total_lookups * 100) if total_lookups > 0 else 0
229+
230+
print(f" {ANSIColors.CYAN}Frame Cache:{ANSIColors.RESET}")
231+
print(f" Total samples: {total_samples:,}")
232+
print(f" Full hits: {frame_cache_hits:,} ({ANSIColors.GREEN}{hits_pct:.1f}%{ANSIColors.RESET})")
233+
print(f" Partial hits: {frame_cache_partial_hits:,} ({ANSIColors.YELLOW}{partial_pct:.1f}%{ANSIColors.RESET})")
234+
print(f" Misses: {frame_cache_misses:,} ({ANSIColors.RED}{misses_pct:.1f}%{ANSIColors.RESET})")
235+
236+
# Frame read stats
237+
frames_from_cache = stats.get('frames_read_from_cache', 0)
238+
frames_from_memory = stats.get('frames_read_from_memory', 0)
239+
total_frames = frames_from_cache + frames_from_memory
240+
cache_frame_pct = (frames_from_cache / total_frames * 100) if total_frames > 0 else 0
241+
memory_frame_pct = (frames_from_memory / total_frames * 100) if total_frames > 0 else 0
242+
243+
print(f" {ANSIColors.CYAN}Frame Reads:{ANSIColors.RESET}")
244+
print(f" From cache: {frames_from_cache:,} ({ANSIColors.GREEN}{cache_frame_pct:.1f}%{ANSIColors.RESET})")
245+
print(f" From memory: {frames_from_memory:,} ({ANSIColors.RED}{memory_frame_pct:.1f}%{ANSIColors.RESET})")
246+
247+
# Code object cache stats
248+
code_hits = stats.get('code_object_cache_hits', 0)
249+
code_misses = stats.get('code_object_cache_misses', 0)
250+
total_code = code_hits + code_misses
251+
code_hits_pct = (code_hits / total_code * 100) if total_code > 0 else 0
252+
code_misses_pct = (code_misses / total_code * 100) if total_code > 0 else 0
253+
254+
print(f" {ANSIColors.CYAN}Code Object Cache:{ANSIColors.RESET}")
255+
print(f" Hits: {code_hits:,} ({ANSIColors.GREEN}{code_hits_pct:.1f}%{ANSIColors.RESET})")
256+
print(f" Misses: {code_misses:,} ({ANSIColors.RED}{code_misses_pct:.1f}%{ANSIColors.RESET})")
257+
258+
# Stale invalidations
259+
stale_invalidations = stats.get('stale_cache_invalidations', 0)
260+
if stale_invalidations > 0:
261+
print(f" {ANSIColors.YELLOW}Stale cache invalidations: {stale_invalidations}{ANSIColors.RESET}")
262+
185263

186264
def sample(
187265
pid,
@@ -228,7 +306,8 @@ def sample(
228306
mode=mode,
229307
native=native,
230308
gc=gc,
231-
skip_non_matching_threads=skip_non_matching_threads
309+
skip_non_matching_threads=skip_non_matching_threads,
310+
collect_stats=realtime_stats # Collect stats when realtime_stats is enabled
232311
)
233312
profiler.realtime_stats = realtime_stats
234313

@@ -283,7 +362,8 @@ def sample_live(
283362
mode=mode,
284363
native=native,
285364
gc=gc,
286-
skip_non_matching_threads=skip_non_matching_threads
365+
skip_non_matching_threads=skip_non_matching_threads,
366+
collect_stats=realtime_stats # Collect stats when realtime_stats is enabled
287367
)
288368
profiler.realtime_stats = realtime_stats
289369

Modules/_remote_debugging/_remote_debugging.h

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,27 @@ typedef struct {
165165
PyObject *frame_list; // owned reference, NULL if empty
166166
} FrameCacheEntry;
167167

168+
/* Statistics for profiling performance analysis */
169+
typedef struct {
170+
uint64_t total_samples; // Total number of get_stack_trace calls
171+
uint64_t frame_cache_hits; // Full cache hits (entire stack unchanged)
172+
uint64_t frame_cache_misses; // Cache misses requiring full walk
173+
uint64_t frame_cache_partial_hits; // Partial hits (stopped at cached frame)
174+
uint64_t frames_read_from_cache; // Total frames retrieved from cache
175+
uint64_t frames_read_from_memory; // Total frames read from remote memory
176+
uint64_t memory_reads; // Total remote memory read operations
177+
uint64_t code_object_cache_hits; // Code object cache hits
178+
uint64_t code_object_cache_misses; // Code object cache misses
179+
uint64_t stale_cache_invalidations; // Times stale entries were cleared
180+
} UnwinderStats;
181+
182+
/* Stats tracking macros - no-op when stats collection is disabled */
183+
#define STATS_INC(unwinder, field) \
184+
do { if ((unwinder)->collect_stats) (unwinder)->stats.field++; } while(0)
185+
186+
#define STATS_ADD(unwinder, field, val) \
187+
do { if ((unwinder)->collect_stats) (unwinder)->stats.field += (val); } while(0)
188+
168189
typedef struct {
169190
PyTypeObject *RemoteDebugging_Type;
170191
PyTypeObject *TaskInfo_Type;
@@ -207,9 +228,11 @@ typedef struct {
207228
int native;
208229
int gc;
209230
int cache_frames;
231+
int collect_stats; // whether to collect statistics
210232
uint32_t stale_invalidation_counter; // counter for throttling frame_cache_invalidate_stale
211233
RemoteDebuggingState *cached_state;
212234
FrameCacheEntry *frame_cache; // preallocated array of FRAME_CACHE_MAX_THREADS entries
235+
UnwinderStats stats; // statistics for performance analysis
213236
#ifdef Py_GIL_DISABLED
214237
uint32_t tlbc_generation;
215238
_Py_hashtable_t *tlbc_cache;

Modules/_remote_debugging/clinic/module.c.h

Lines changed: 68 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Modules/_remote_debugging/code_objects.c

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,11 @@ parse_code_object(RemoteUnwinderObject *unwinder,
257257

258258
if (unwinder && unwinder->code_object_cache != NULL) {
259259
meta = _Py_hashtable_get(unwinder->code_object_cache, key);
260+
if (meta) {
261+
STATS_INC(unwinder, code_object_cache_hits);
262+
} else {
263+
STATS_INC(unwinder, code_object_cache_misses);
264+
}
260265
}
261266

262267
if (meta == NULL) {

Modules/_remote_debugging/frame_cache.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ frame_cache_invalidate_stale(RemoteUnwinderObject *unwinder, PyObject *result)
124124
Py_CLEAR(unwinder->frame_cache[i].frame_list);
125125
unwinder->frame_cache[i].thread_id = 0;
126126
unwinder->frame_cache[i].num_addrs = 0;
127+
STATS_INC(unwinder, stale_cache_invalidations);
127128
}
128129
}
129130
}

0 commit comments

Comments
 (0)