gh-138122: Implement frame caching in RemoteUnwinder to reduce memory… · python/cpython@572c780

GitHub

@@ -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