Skip to content

Commit 04db7b0

Browse files
committed
Implement frame caching in RemoteUnwinder to reduce memory reads
When sampling deep call stacks repeatedly, most of the stack is typically unchanged between samples. This implements a per-thread frame cache that stores (address, frame_info) pairs. On each sample, if the current_frame has reached the last_profiled_frame pointer (indicating unchanged stack below), we stop walking and append cached frames instead of re-reading them from the remote process. The cache is keyed by thread ID and stores the full frame list from the previous sample. When process_frame_chain hits the cached boundary, frame_cache_lookup_and_extend appends the cached continuation. After each sample we update last_profiled_frame in the target process so the eval loop can maintain the invariant. This is opt-in via cache_frames=True on RemoteUnwinder to avoid changing behavior for existing users.
1 parent 6c5e662 commit 04db7b0

File tree

5 files changed

+507
-30
lines changed

5 files changed

+507
-30
lines changed

Modules/_remote_debugging/_remote_debugging.h

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,9 @@ typedef struct {
195195
int skip_non_matching_threads;
196196
int native;
197197
int gc;
198+
int cache_frames;
198199
RemoteDebuggingState *cached_state;
200+
PyObject *frame_cache; // dict: thread_id -> list of (addr, frame_info)
199201
#ifdef Py_GIL_DISABLED
200202
uint32_t tlbc_generation;
201203
_Py_hashtable_t *tlbc_cache;
@@ -363,9 +365,38 @@ extern int process_frame_chain(
363365
uintptr_t initial_frame_addr,
364366
StackChunkList *chunks,
365367
PyObject *frame_info,
366-
uintptr_t gc_frame
368+
uintptr_t gc_frame,
369+
uintptr_t last_profiled_frame,
370+
int *stopped_at_cached_frame,
371+
PyObject *frame_addresses // optional: list to receive frame addresses
367372
);
368373

374+
/* Frame cache functions */
375+
extern int frame_cache_init(RemoteUnwinderObject *unwinder);
376+
extern void frame_cache_cleanup(RemoteUnwinderObject *unwinder);
377+
extern void frame_cache_invalidate_stale(RemoteUnwinderObject *unwinder, PyObject *result);
378+
extern int frame_cache_lookup_and_extend(
379+
RemoteUnwinderObject *unwinder,
380+
uint64_t thread_id,
381+
uintptr_t last_profiled_frame,
382+
PyObject *frame_info,
383+
PyObject *frame_addresses); // optional: list to extend with cached addresses
384+
extern int frame_cache_store(
385+
RemoteUnwinderObject *unwinder,
386+
uint64_t thread_id,
387+
PyObject *frame_list,
388+
const uintptr_t *addrs,
389+
Py_ssize_t num_addrs);
390+
391+
extern int collect_frames_with_cache(
392+
RemoteUnwinderObject *unwinder,
393+
uintptr_t frame_addr,
394+
StackChunkList *chunks,
395+
PyObject *frame_info,
396+
uintptr_t gc_frame,
397+
uintptr_t last_profiled_frame,
398+
uint64_t thread_id);
399+
369400
/* ============================================================================
370401
* THREAD FUNCTION DECLARATIONS
371402
* ============================================================================ */

Modules/_remote_debugging/clinic/module.c.h

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

0 commit comments

Comments
 (0)