Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
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.
  • Loading branch information
pablogsal committed Dec 1, 2025
commit d847cf7bfdbb55ded9254224e881812ff9edad52
33 changes: 32 additions & 1 deletion Modules/_remote_debugging/_remote_debugging.h
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,9 @@ typedef struct {
int skip_non_matching_threads;
int native;
int gc;
int cache_frames;
RemoteDebuggingState *cached_state;
PyObject *frame_cache; // dict: thread_id -> list of (addr, frame_info)
#ifdef Py_GIL_DISABLED
uint32_t tlbc_generation;
_Py_hashtable_t *tlbc_cache;
Expand Down Expand Up @@ -363,9 +365,38 @@ extern int process_frame_chain(
uintptr_t initial_frame_addr,
StackChunkList *chunks,
PyObject *frame_info,
uintptr_t gc_frame
uintptr_t gc_frame,
uintptr_t last_profiled_frame,
int *stopped_at_cached_frame,
PyObject *frame_addresses // optional: list to receive frame addresses
);

/* Frame cache functions */
extern int frame_cache_init(RemoteUnwinderObject *unwinder);
extern void frame_cache_cleanup(RemoteUnwinderObject *unwinder);
extern void frame_cache_invalidate_stale(RemoteUnwinderObject *unwinder, PyObject *result);
extern int frame_cache_lookup_and_extend(
RemoteUnwinderObject *unwinder,
uint64_t thread_id,
uintptr_t last_profiled_frame,
PyObject *frame_info,
PyObject *frame_addresses); // optional: list to extend with cached addresses
extern int frame_cache_store(
RemoteUnwinderObject *unwinder,
uint64_t thread_id,
PyObject *frame_list,
const uintptr_t *addrs,
Py_ssize_t num_addrs);

extern int collect_frames_with_cache(
RemoteUnwinderObject *unwinder,
uintptr_t frame_addr,
StackChunkList *chunks,
PyObject *frame_info,
uintptr_t gc_frame,
uintptr_t last_profiled_frame,
uint64_t thread_id);

/* ============================================================================
* THREAD FUNCTION DECLARATIONS
* ============================================================================ */
Expand Down
33 changes: 23 additions & 10 deletions Modules/_remote_debugging/clinic/module.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading