|
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, cache_frames=True |
| 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, cache_frames=True |
| 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) |
@@ -124,6 +127,10 @@ def sample(self, collector, duration_sec=10): |
124 | 127 | print(f"Sample rate: {sample_rate:.2f} samples/sec") |
125 | 128 | print(f"Error rate: {error_rate:.2f}%") |
126 | 129 |
|
| 130 | + # Print unwinder stats if stats collection is enabled |
| 131 | + if self.collect_stats: |
| 132 | + self._print_unwinder_stats() |
| 133 | + |
127 | 134 | # Pass stats to flamegraph collector if it's the right type |
128 | 135 | if hasattr(collector, 'set_stats'): |
129 | 136 | 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): |
171 | 178 | (1.0 / min_hz) * 1_000_000 if min_hz > 0 else 0 |
172 | 179 | ) # Max time = Min Hz |
173 | 180 |
|
| 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 | + |
174 | 196 | # Clear line and print stats |
175 | 197 | 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}", |
181 | 204 | end="", |
182 | 205 | flush=True, |
183 | 206 | ) |
184 | 207 |
|
| 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 | + |
185 | 263 |
|
186 | 264 | def sample( |
187 | 265 | pid, |
@@ -228,7 +306,8 @@ def sample( |
228 | 306 | mode=mode, |
229 | 307 | native=native, |
230 | 308 | 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 |
232 | 311 | ) |
233 | 312 | profiler.realtime_stats = realtime_stats |
234 | 313 |
|
@@ -283,7 +362,8 @@ def sample_live( |
283 | 362 | mode=mode, |
284 | 363 | native=native, |
285 | 364 | 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 |
287 | 367 | ) |
288 | 368 | profiler.realtime_stats = realtime_stats |
289 | 369 |
|
|
0 commit comments