@@ -27,21 +27,24 @@
272728282929classSampleProfiler:
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):
3131self.pid=pid
3232self.sample_interval_usec=sample_interval_usec
3333self.all_threads=all_threads
3434self.mode=mode# Store mode for later use
35+self.collect_stats=collect_stats
3536if_FREE_THREADED_BUILD:
3637self.unwinder=_remote_debugging.RemoteUnwinder(
3738self.pid, all_threads=self.all_threads, mode=mode, native=native, gc=gc,
38-skip_non_matching_threads=skip_non_matching_threads
39+skip_non_matching_threads=skip_non_matching_threads, cache_frames=True,
40+stats=collect_stats
3941 )
4042else:
4143only_active_threads=bool(self.all_threads)
4244self.unwinder=_remote_debugging.RemoteUnwinder(
4345self.pid, only_active_thread=only_active_threads, mode=mode, native=native, gc=gc,
44-skip_non_matching_threads=skip_non_matching_threads
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
4750self.sample_intervals=deque(maxlen=100)
@@ -129,6 +132,10 @@ def sample(self, collector, duration_sec=10, *, async_aware=False):
129132print(f"Sample rate: {sample_rate:.2f} samples/sec")
130133print(f"Error rate: {error_rate:.2f}%")
131134135+# Print unwinder stats if stats collection is enabled
136+ifself.collect_stats:
137+self._print_unwinder_stats()
138+132139# Pass stats to flamegraph collector if it's the right type
133140ifhasattr(collector, 'set_stats'):
134141collector.set_stats(self.sample_interval_usec, running_time, sample_rate, error_rate, missed_samples, mode=self.mode)
@@ -176,17 +183,100 @@ def _print_realtime_stats(self):
176183 (1.0/min_hz) *1_000_000ifmin_hz>0else0
177184 ) # Max time = Min Hz
178185186+# Build cache stats string if stats collection is enabled
187+cache_stats_str=""
188+ifself.collect_stats:
189+try:
190+stats=self.unwinder.get_stats()
191+hits=stats.get('frame_cache_hits', 0)
192+partial=stats.get('frame_cache_partial_hits', 0)
193+misses=stats.get('frame_cache_misses', 0)
194+total=hits+partial+misses
195+iftotal>0:
196+hit_pct= (hits+partial) /total*100
197+cache_stats_str=f" {ANSIColors.MAGENTA}Cache: {hit_pct:.1f}% ({hits}+{partial}/{misses}){ANSIColors.RESET}"
198+exceptRuntimeError:
199+pass
200+179201# Clear line and print stats
180202print(
181-f"\r\033[K{ANSIColors.BOLD_BLUE}Real-time sampling stats:{ANSIColors.RESET} "
182-f"{ANSIColors.YELLOW}Mean: {mean_hz:.1f}Hz ({mean_us_per_sample:.2f}µs){ANSIColors.RESET} "
183-f"{ANSIColors.GREEN}Min: {min_hz:.1f}Hz ({max_us_per_sample:.2f}µs){ANSIColors.RESET} "
184-f"{ANSIColors.RED}Max: {max_hz:.1f}Hz ({min_us_per_sample:.2f}µs){ANSIColors.RESET} "
185-f"{ANSIColors.CYAN}Samples: {self.total_samples}{ANSIColors.RESET}",
203+f"\r\033[K{ANSIColors.BOLD_BLUE}Stats:{ANSIColors.RESET} "
204+f"{ANSIColors.YELLOW}{mean_hz:.1f}Hz ({mean_us_per_sample:.1f}µs){ANSIColors.RESET} "
205+f"{ANSIColors.GREEN}Min: {min_hz:.1f}Hz{ANSIColors.RESET} "
206+f"{ANSIColors.RED}Max: {max_hz:.1f}Hz{ANSIColors.RESET} "
207+f"{ANSIColors.CYAN}N={self.total_samples}{ANSIColors.RESET}"
208+f"{cache_stats_str}",
186209end="",
187210flush=True,
188211 )
189212213+def_print_unwinder_stats(self):
214+"""Print unwinder statistics including cache performance."""
215+try:
216+stats=self.unwinder.get_stats()
217+exceptRuntimeError:
218+return# Stats not enabled
219+220+print(f"\n{ANSIColors.BOLD_BLUE}{'='*50}{ANSIColors.RESET}")
221+print(f"{ANSIColors.BOLD_BLUE}Unwinder Statistics:{ANSIColors.RESET}")
222+223+# Frame cache stats
224+total_samples=stats.get('total_samples', 0)
225+frame_cache_hits=stats.get('frame_cache_hits', 0)
226+frame_cache_partial_hits=stats.get('frame_cache_partial_hits', 0)
227+frame_cache_misses=stats.get('frame_cache_misses', 0)
228+total_lookups=frame_cache_hits+frame_cache_partial_hits+frame_cache_misses
229+230+# Calculate percentages
231+hits_pct= (frame_cache_hits/total_lookups*100) iftotal_lookups>0else0
232+partial_pct= (frame_cache_partial_hits/total_lookups*100) iftotal_lookups>0else0
233+misses_pct= (frame_cache_misses/total_lookups*100) iftotal_lookups>0else0
234+235+print(f" {ANSIColors.CYAN}Frame Cache:{ANSIColors.RESET}")
236+print(f" Total samples: {total_samples:,}")
237+print(f" Full hits: {frame_cache_hits:,} ({ANSIColors.GREEN}{hits_pct:.1f}%{ANSIColors.RESET})")
238+print(f" Partial hits: {frame_cache_partial_hits:,} ({ANSIColors.YELLOW}{partial_pct:.1f}%{ANSIColors.RESET})")
239+print(f" Misses: {frame_cache_misses:,} ({ANSIColors.RED}{misses_pct:.1f}%{ANSIColors.RESET})")
240+241+# Frame read stats
242+frames_from_cache=stats.get('frames_read_from_cache', 0)
243+frames_from_memory=stats.get('frames_read_from_memory', 0)
244+total_frames=frames_from_cache+frames_from_memory
245+cache_frame_pct= (frames_from_cache/total_frames*100) iftotal_frames>0else0
246+memory_frame_pct= (frames_from_memory/total_frames*100) iftotal_frames>0else0
247+248+print(f" {ANSIColors.CYAN}Frame Reads:{ANSIColors.RESET}")
249+print(f" From cache: {frames_from_cache:,} ({ANSIColors.GREEN}{cache_frame_pct:.1f}%{ANSIColors.RESET})")
250+print(f" From memory: {frames_from_memory:,} ({ANSIColors.RED}{memory_frame_pct:.1f}%{ANSIColors.RESET})")
251+252+# Code object cache stats
253+code_hits=stats.get('code_object_cache_hits', 0)
254+code_misses=stats.get('code_object_cache_misses', 0)
255+total_code=code_hits+code_misses
256+code_hits_pct= (code_hits/total_code*100) iftotal_code>0else0
257+code_misses_pct= (code_misses/total_code*100) iftotal_code>0else0
258+259+print(f" {ANSIColors.CYAN}Code Object Cache:{ANSIColors.RESET}")
260+print(f" Hits: {code_hits:,} ({ANSIColors.GREEN}{code_hits_pct:.1f}%{ANSIColors.RESET})")
261+print(f" Misses: {code_misses:,} ({ANSIColors.RED}{code_misses_pct:.1f}%{ANSIColors.RESET})")
262+263+# Memory operations
264+memory_reads=stats.get('memory_reads', 0)
265+memory_bytes=stats.get('memory_bytes_read', 0)
266+ifmemory_bytes>=1024*1024:
267+memory_str=f"{memory_bytes/ (1024*1024):.1f} MB"
268+elifmemory_bytes>=1024:
269+memory_str=f"{memory_bytes/1024:.1f} KB"
270+else:
271+memory_str=f"{memory_bytes} B"
272+print(f" {ANSIColors.CYAN}Memory:{ANSIColors.RESET}")
273+print(f" Read operations: {memory_reads:,} ({memory_str})")
274+275+# Stale invalidations
276+stale_invalidations=stats.get('stale_cache_invalidations', 0)
277+ifstale_invalidations>0:
278+print(f" {ANSIColors.YELLOW}Stale cache invalidations: {stale_invalidations}{ANSIColors.RESET}")
279+190280191281defsample(
192282pid,
@@ -234,7 +324,8 @@ def sample(
234324mode=mode,
235325native=native,
236326gc=gc,
237-skip_non_matching_threads=skip_non_matching_threads
327+skip_non_matching_threads=skip_non_matching_threads,
328+collect_stats=realtime_stats,
238329 )
239330profiler.realtime_stats=realtime_stats
240331@@ -290,7 +381,8 @@ def sample_live(
290381mode=mode,
291382native=native,
292383gc=gc,
293-skip_non_matching_threads=skip_non_matching_threads
384+skip_non_matching_threads=skip_non_matching_threads,
385+collect_stats=realtime_stats,
294386 )
295387profiler.realtime_stats=realtime_stats
296388