|
27 | 27 |
|
28 | 28 |
|
29 | 29 | 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): |
31 | 31 | self.pid = pid |
32 | 32 | self.sample_interval_usec = sample_interval_usec |
33 | 33 | self.all_threads = all_threads |
34 | 34 | self.mode = mode # Store mode for later use |
| 35 | + self.collect_stats = collect_stats |
35 | 36 | if _FREE_THREADED_BUILD: |
36 | 37 | self.unwinder = _remote_debugging.RemoteUnwinder( |
37 | 38 | self.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 |
39 | 41 | ) |
40 | 42 | else: |
41 | 43 | only_active_threads = bool(self.all_threads) |
42 | 44 | self.unwinder = _remote_debugging.RemoteUnwinder( |
43 | 45 | self.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 |
45 | 48 | ) |
46 | 49 | # Track sample intervals and total sample count |
47 | 50 | self.sample_intervals = deque(maxlen=100) |
@@ -129,6 +132,10 @@ def sample(self, collector, duration_sec=10, *, async_aware=False): |
129 | 132 | print(f"Sample rate: {sample_rate:.2f} samples/sec") |
130 | 133 | print(f"Error rate: {error_rate:.2f}%") |
131 | 134 |
|
| 135 | + # Print unwinder stats if stats collection is enabled |
| 136 | + if self.collect_stats: |
| 137 | + self._print_unwinder_stats() |
| 138 | + |
132 | 139 | # Pass stats to flamegraph collector if it's the right type |
133 | 140 | if hasattr(collector, 'set_stats'): |
134 | 141 | collector.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): |
176 | 183 | (1.0 / min_hz) * 1_000_000 if min_hz > 0 else 0 |
177 | 184 | ) # Max time = Min Hz |
178 | 185 |
|
| 186 | + # Build cache stats string if stats collection is enabled |
| 187 | + cache_stats_str = "" |
| 188 | + if self.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 | + if total > 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 | + except RuntimeError: |
| 199 | + pass |
| 200 | + |
179 | 201 | # Clear line and print stats |
180 | 202 | print( |
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}", |
186 | 209 | end="", |
187 | 210 | flush=True, |
188 | 211 | ) |
189 | 212 |
|
| 213 | + def _print_unwinder_stats(self): |
| 214 | + """Print unwinder statistics including cache performance.""" |
| 215 | + try: |
| 216 | + stats = self.unwinder.get_stats() |
| 217 | + except RuntimeError: |
| 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) if total_lookups > 0 else 0 |
| 232 | + partial_pct = (frame_cache_partial_hits / total_lookups * 100) if total_lookups > 0 else 0 |
| 233 | + misses_pct = (frame_cache_misses / total_lookups * 100) if total_lookups > 0 else 0 |
| 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) if total_frames > 0 else 0 |
| 246 | + memory_frame_pct = (frames_from_memory / total_frames * 100) if total_frames > 0 else 0 |
| 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) if total_code > 0 else 0 |
| 257 | + code_misses_pct = (code_misses / total_code * 100) if total_code > 0 else 0 |
| 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 | + if memory_bytes >= 1024 * 1024: |
| 267 | + memory_str = f"{memory_bytes / (1024 * 1024):.1f} MB" |
| 268 | + elif memory_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 | + if stale_invalidations > 0: |
| 278 | + print(f" {ANSIColors.YELLOW}Stale cache invalidations: {stale_invalidations}{ANSIColors.RESET}") |
| 279 | + |
190 | 280 |
|
191 | 281 | def sample( |
192 | 282 | pid, |
@@ -234,7 +324,8 @@ def sample( |
234 | 324 | mode=mode, |
235 | 325 | native=native, |
236 | 326 | gc=gc, |
237 | | - skip_non_matching_threads=skip_non_matching_threads |
| 327 | + skip_non_matching_threads=skip_non_matching_threads, |
| 328 | + collect_stats=realtime_stats, |
238 | 329 | ) |
239 | 330 | profiler.realtime_stats = realtime_stats |
240 | 331 |
|
@@ -290,7 +381,8 @@ def sample_live( |
290 | 381 | mode=mode, |
291 | 382 | native=native, |
292 | 383 | gc=gc, |
293 | | - skip_non_matching_threads=skip_non_matching_threads |
| 384 | + skip_non_matching_threads=skip_non_matching_threads, |
| 385 | + collect_stats=realtime_stats, |
294 | 386 | ) |
295 | 387 | profiler.realtime_stats = realtime_stats |
296 | 388 |
|
|
0 commit comments