mirror of
https://github.com/python/cpython.git
synced 2026-10-29 10:55:16 +08:00
+2








4ceb3faf2c
* [3.15] gh-153364: Make frame, coroutine, and task-waiter chain walks iterative and bounded (GH-153365) (#158813) gh-153364: Make frame, coroutine, and task-waiter chain walks iterative and bounded (GH-153365) * let me declare single limit * use our new limit in process_frame_chain() * add it in parse_async_frame_chain() * parse_coro_chain() * NEWS * async in the message? * test * no race * process_task_awaited_by * process_task_awaited_by limit test * NEWS * MAX_TASK_WAITER_CHAIN_DEPTH * TASK_WAITER_CHAIN_DEPTH in test * TASK_WAITER_CHAIN_DEPTH 256 * prevent the drift with the comment * better naming, better style * MAX_TASK_WAITER_CHAIN_DEPTH comment * task-waiter iterative bfs walk * iterative coro-walk * nicer news * 1 << 14 * comment * unused read_Py_ssize_t * fix tombstones * simplify * correct msg * better test * news for tombstones * left-over from when testing buggy version * redundant new line (cherry picked from commite0861c6ae7) Co-authored-by: Maurycy Pawłowski-Wieroński <maurycy@maurycy.com> (cherry picked from commit9e401cf7f5) * [3.15] gh-155811: Add a seqcount to `gc_stats` to prevent torn reads (GH-155828) (#158829) * update_seq * no need for XCHGL, MOVL is enough? * gh-155811: Retry an inconsistent GC snapshot once --------- (cherry picked from commit5fecd448bb) Co-authored-by: Pablo Galindo Salgado <Pablogsal@gmail.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> (cherry picked from commit028124039c) * [3.15] gh-151292: `_remote_debugging`: Do not corrupt the binary file when hitting `OverflowError` (GH-152892) (#158830) gh-151292: `_remote_debugging`: Do not corrupt the binary file when hitting `OverflowError` (GH-152892) * the kolektor * test * better test * news * =Q, move const to the base, not self.running * gh-151292: Track binary writer finalization state --------- (cherry picked from commitf839c061d3) Co-authored-by: Maurycy Pawłowski-Wieroński <maurycy@maurycy.com> Co-authored-by: Pablo Galindo Salgado <Pablogsal@gmail.com> (cherry picked from commit8a7c23f316) * [3.15] gh-158583: Fix uninitialized memory read in bytes.fromhex() (GH-158584) (#158691) gh-158583: Fix uninitialized memory read in bytes.fromhex() (GH-158584) (cherry picked from commit9d22a5334b) Co-authored-by: Victor Stinner <vstinner@python.org> (cherry picked from commit4f7af46e25) * [3.15] gh-154194: Degrade frames in Tachyon instead of failing the sample (GH-154195) (#158831) * gh-154194: Degrade frames in Tachyon instead of failing the sample (#154195) * degrade gracefully * news * better NEWS wording * do not raise on MAX_REMOTE_STR_READ * bye MAX_REMOTE_STR_READ * fix -m asyncio ps|pstree * test truncation and linetable sentinel * simpler * simpler * redundant now * respect #157790 in the news --------- Co-authored-by: Pablo Galindo Salgado <Pablogsal@gmail.com> (cherry picked from commit7d25916b41) * Preserve the stable ABI when creating fallback frame names --------- Co-authored-by: Maurycy Pawłowski-Wieroński <maurycy@maurycy.com> (cherry picked from commitc27f494061) * [3.15] Add MSan to CI (GH-158625) (#158832) Run a job that the test suite with MSan to the CI (#158625) * Run the test suite with MSan in CI * Additional fixes * Add `_Py_MSAN_UNPOISON_STRING` * Apply Victor's suggestions * Apply Victor's suggestions --------- (cherry picked from commitb93fb19a6e) Co-authored-by: Stan Ulbrych <stan@python.org> Co-authored-by: Victor Stinner <victor.stinner@gmail.com> (cherry picked from commit1bc78dea18) * [3.15] gh-156810: Write the profiler's collapsed-stack export as UTF-8 (GH-156811) (#156814) gh-156810: Write the profiler's collapsed-stack export as UTF-8 (GH-156811) (cherry picked from commitc3706f4c68) Co-authored-by: tonghuaroot (童话) <tonghuaroot@gmail.com> (cherry picked from commitfce28da9b7) * [3.15] gh-158552: Wait for Windows threads to suspend before blocking sampling (GH-158802) (#158845) gh-158552: Wait for Windows threads to suspend before blocking sampling (GH-158802) * gh-158552: Wait for Windows threads to suspend before blocking sampling * Use a named Windows thread enumeration status constant (cherry picked from commit1643525f0f) Co-authored-by: Pablo Galindo Salgado <Pablogsal@gmail.com> (cherry picked from commit31288da21d) * [3.15] gh-152721: Fix quadratic RLE replay time in the profiling binary reader (GH-152722) (#158850) Backport of GH-152722. Co-authored-by: tonghuaroot <tonghuaroot@gmail.com> (cherry picked from commitebaca2ac2d) * [3.15] gh-156545: Fix flamegraph export RecursionError on deeply recursive programs (GH-156546) (#158851) Backport of GH-156546. Co-authored-by: tonghuaroot (童话) <tonghuaroot@gmail.com> (cherry picked from commit0ac7217040) * [3.15] gh-158540: Add the profiled script's directory to sys.path (GH-158548) (#158844) gh-158540: Add the profiled script's directory to sys.path (GH-158548) * gh-158540: Add the profiled script's directory to sys.path When a script is profiled with ``python -m profiling.sampling run script.py`` from another directory, the script cannot import modules placed next to it, because ``_sync_coordinator._execute_script()`` executes it with the working directory (added by ``_setup_environment()`` for the module case) as ``sys.path[0]`` instead of the script's own directory. Make the script's directory importable in ``_execute_script()``, matching the behavior of ``python script.py``. Add a regression test that runs the coordinator on a script importing a sibling module. * Update _sync_coordinator.py Comment simplified * gh-158540: Resolve symlinks when adding the script directory to sys.path ``python script.py`` resolves symlinks when computing ``sys.path[0]``, so a script reached through a symlink (``link.py -> sub/where.py``) imports modules from the real script's directory. Apply ``os.path.realpath()`` before taking the directory name, and make sure the result is placed at the front of ``sys.path`` even if it was already listed. Add a regression test for a symlinked script. --------- (cherry picked from commit3f02aab9fa) Co-authored-by: he_tao <53343436+hetaozdh@users.noreply.github.com> Co-authored-by: Eduardo Villalpando Mello <eduardo.villalpando.mello@gmail.com> (cherry picked from commitf52d831397) * [3.15] gh-153838: Skip non-regular source files in the heatmap exporter (GH-153839) (#158853) Backport of GH-153839. Co-authored-by: tonghuaroot <tonghuaroot@gmail.com> (cherry picked from commit48998df251) * [3.15] gh-158539: Fix exception mode missing handlers in generators/coroutines (GH-158581) (#158852) * [3.15] gh-158539: Fix exception mode missing handlers in generators/coroutines (GH-158581) Backport of GH-158581. Co-authored-by: LucasZhou <donghao.zhou@outlook.com> * [3.15] gh-158539: Use portable static assertion messages * [3.15] gh-158539: Keep layout assertions with debug-offset validation * [3.15] gh-158539: Use the platform guard for in-process inspection tests --------- Co-authored-by: LucasZhou <donghao.zhou@outlook.com> (cherry picked from commitd625ecbb8d) * [3.15] gh-158522: Fix truncated stack for a task whose coroutine recurses (GH-158526) (#158870) Co-authored-by: Timofei Ivankov <128279579+deadlovelll@users.noreply.github.com> (cherry picked from commita904b397ed) * gh-156545, gh-158539: Fix deep flamegraph export on small C stacks and macOS runtime lookup (#158874) (cherry picked from commit114de198c0) Include the C-stack test helper from main, introduced byce5ae29ef9, which the regression test requires but 3.15 does not yet provide. --------- Co-authored-by: Miss Islington (bot) <31488909+miss-islington@users.noreply.github.com> Co-authored-by: Maurycy Pawłowski-Wieroński <maurycy@maurycy.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> Co-authored-by: Victor Stinner <vstinner@python.org> Co-authored-by: Stan Ulbrych <stan@python.org> Co-authored-by: Victor Stinner <victor.stinner@gmail.com> Co-authored-by: tonghuaroot (童话) <tonghuaroot@gmail.com> Co-authored-by: he_tao <53343436+hetaozdh@users.noreply.github.com> Co-authored-by: Eduardo Villalpando Mello <eduardo.villalpando.mello@gmail.com> Co-authored-by: LucasZhou <donghao.zhou@outlook.com> Co-authored-by: Timofei Ivankov <128279579+deadlovelll@users.noreply.github.com>
671 lines
24 KiB
C
671 lines
24 KiB
C
/******************************************************************************
|
|
* Remote Debugging Module - Frame Functions
|
|
*
|
|
* This file contains functions for parsing interpreter frames and
|
|
* managing stack chunks from remote process memory.
|
|
******************************************************************************/
|
|
|
|
#include "_remote_debugging.h"
|
|
|
|
/* ============================================================================
|
|
* STACK CHUNK MANAGEMENT FUNCTIONS
|
|
* ============================================================================ */
|
|
|
|
void
|
|
cleanup_stack_chunks(StackChunkList *chunks)
|
|
{
|
|
for (size_t i = 0; i < chunks->count; ++i) {
|
|
PyMem_RawFree(chunks->chunks[i].local_copy);
|
|
}
|
|
PyMem_RawFree(chunks->chunks);
|
|
}
|
|
|
|
static int
|
|
process_single_stack_chunk(
|
|
RemoteUnwinderObject *unwinder,
|
|
uintptr_t chunk_addr,
|
|
StackChunkInfo *chunk_info
|
|
) {
|
|
// Start with default size assumption
|
|
size_t current_size = _PY_DATA_STACK_CHUNK_SIZE;
|
|
|
|
char *this_chunk = PyMem_RawMalloc(current_size);
|
|
if (!this_chunk) {
|
|
PyErr_NoMemory();
|
|
set_exception_cause(unwinder, PyExc_MemoryError, "Failed to allocate stack chunk buffer");
|
|
return -1;
|
|
}
|
|
|
|
if (_Py_RemoteDebug_PagedReadRemoteMemory(&unwinder->handle, chunk_addr, current_size, this_chunk) < 0) {
|
|
PyMem_RawFree(this_chunk);
|
|
set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read stack chunk");
|
|
return -1;
|
|
}
|
|
|
|
// Check actual size and reread if necessary
|
|
size_t actual_size = GET_MEMBER(size_t, this_chunk, offsetof(_PyStackChunk, size));
|
|
if (actual_size != current_size) {
|
|
// Validate size: reject garbage (too small or unreasonably large)
|
|
// Size must be at least enough for the header and reasonably bounded
|
|
if (actual_size <= offsetof(_PyStackChunk, data) || actual_size > MAX_STACK_CHUNK_SIZE) {
|
|
PyMem_RawFree(this_chunk);
|
|
PyErr_Format(PyExc_RuntimeError,
|
|
"Invalid stack chunk size %zu (corrupted remote memory)", actual_size);
|
|
set_exception_cause(unwinder, PyExc_RuntimeError,
|
|
"Invalid stack chunk size (corrupted remote memory)");
|
|
return -1;
|
|
}
|
|
|
|
char *tmp = PyMem_RawRealloc(this_chunk, actual_size);
|
|
if (!tmp) {
|
|
PyMem_RawFree(this_chunk);
|
|
PyErr_NoMemory();
|
|
set_exception_cause(unwinder, PyExc_MemoryError, "Failed to reallocate stack chunk buffer");
|
|
return -1;
|
|
}
|
|
this_chunk = tmp;
|
|
|
|
if (_Py_RemoteDebug_PagedReadRemoteMemory(&unwinder->handle, chunk_addr, actual_size, this_chunk) < 0) {
|
|
PyMem_RawFree(this_chunk);
|
|
set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to reread stack chunk with correct size");
|
|
return -1;
|
|
}
|
|
current_size = actual_size;
|
|
}
|
|
|
|
chunk_info->remote_addr = chunk_addr;
|
|
chunk_info->size = current_size;
|
|
chunk_info->local_copy = this_chunk;
|
|
return 0;
|
|
}
|
|
|
|
int
|
|
copy_stack_chunks(RemoteUnwinderObject *unwinder,
|
|
uintptr_t tstate_addr,
|
|
StackChunkList *out_chunks)
|
|
{
|
|
uintptr_t chunk_addr;
|
|
StackChunkInfo *chunks = NULL;
|
|
size_t count = 0;
|
|
size_t max_chunks = 16;
|
|
|
|
if (read_ptr(unwinder, tstate_addr + (uintptr_t)unwinder->debug_offsets.thread_state.datastack_chunk, &chunk_addr)) {
|
|
set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read initial stack chunk address");
|
|
return -1;
|
|
}
|
|
|
|
chunks = PyMem_RawMalloc(max_chunks * sizeof(StackChunkInfo));
|
|
if (!chunks) {
|
|
PyErr_NoMemory();
|
|
set_exception_cause(unwinder, PyExc_MemoryError, "Failed to allocate stack chunks array");
|
|
return -1;
|
|
}
|
|
|
|
const size_t MAX_STACK_CHUNKS = 4096;
|
|
while (chunk_addr != 0 && count < MAX_STACK_CHUNKS) {
|
|
// Grow array if needed
|
|
if (count >= max_chunks) {
|
|
max_chunks *= 2;
|
|
StackChunkInfo *new_chunks = PyMem_RawRealloc(chunks, max_chunks * sizeof(StackChunkInfo));
|
|
if (!new_chunks) {
|
|
PyErr_NoMemory();
|
|
set_exception_cause(unwinder, PyExc_MemoryError, "Failed to grow stack chunks array");
|
|
goto error;
|
|
}
|
|
chunks = new_chunks;
|
|
}
|
|
|
|
// Process this chunk
|
|
if (process_single_stack_chunk(unwinder, chunk_addr, &chunks[count]) < 0) {
|
|
set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to process stack chunk");
|
|
goto error;
|
|
}
|
|
|
|
// Get next chunk address and increment count
|
|
chunk_addr = GET_MEMBER(uintptr_t, chunks[count].local_copy, offsetof(_PyStackChunk, previous));
|
|
count++;
|
|
}
|
|
|
|
out_chunks->chunks = chunks;
|
|
out_chunks->count = count;
|
|
return 0;
|
|
|
|
error:
|
|
for (size_t i = 0; i < count; ++i) {
|
|
PyMem_RawFree(chunks[i].local_copy);
|
|
}
|
|
PyMem_RawFree(chunks);
|
|
return -1;
|
|
}
|
|
|
|
void *
|
|
find_frame_in_chunks(StackChunkList *chunks, uintptr_t remote_ptr)
|
|
{
|
|
for (size_t i = 0; i < chunks->count; ++i) {
|
|
// Validate size: reject garbage that would cause underflow
|
|
if (chunks->chunks[i].size <= offsetof(_PyStackChunk, data)) {
|
|
// Skip this chunk - corrupted size from remote memory
|
|
continue;
|
|
}
|
|
uintptr_t base = chunks->chunks[i].remote_addr + offsetof(_PyStackChunk, data);
|
|
size_t payload = chunks->chunks[i].size - offsetof(_PyStackChunk, data);
|
|
|
|
if (payload >= SIZEOF_INTERP_FRAME &&
|
|
remote_ptr >= base &&
|
|
remote_ptr <= base + payload - SIZEOF_INTERP_FRAME) {
|
|
return (char *)chunks->chunks[i].local_copy + (remote_ptr - chunks->chunks[i].remote_addr);
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/* ============================================================================
|
|
* FRAME PARSING FUNCTIONS
|
|
* ============================================================================ */
|
|
|
|
int
|
|
is_frame_valid(
|
|
RemoteUnwinderObject *unwinder,
|
|
uintptr_t frame_addr,
|
|
uintptr_t code_object_addr
|
|
) {
|
|
if ((void*)code_object_addr == NULL) {
|
|
return 0;
|
|
}
|
|
|
|
void* frame = (void*)frame_addr;
|
|
|
|
char owner = GET_MEMBER(char, frame, unwinder->debug_offsets.interpreter_frame.owner);
|
|
if (owner == FRAME_OWNED_BY_INTERPRETER) {
|
|
return 0; // C frame or sentinel base frame
|
|
}
|
|
|
|
if (owner != FRAME_OWNED_BY_GENERATOR && owner != FRAME_OWNED_BY_THREAD) {
|
|
PyErr_Format(PyExc_RuntimeError, "Unhandled frame owner %d.\n", owner);
|
|
set_exception_cause(unwinder, PyExc_RuntimeError, "Unhandled frame owner type in async frame");
|
|
return -1;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
static int
|
|
parse_frame_buffer(
|
|
RemoteUnwinderObject *unwinder,
|
|
PyObject** result,
|
|
const char *frame,
|
|
uintptr_t* address_of_code_object,
|
|
uintptr_t* previous_frame
|
|
) {
|
|
*address_of_code_object = 0;
|
|
|
|
*previous_frame = GET_MEMBER(uintptr_t, frame, unwinder->debug_offsets.interpreter_frame.previous);
|
|
uintptr_t code_object = GET_MEMBER_NO_TAG(uintptr_t, frame, unwinder->debug_offsets.interpreter_frame.executable);
|
|
int frame_valid = is_frame_valid(unwinder, (uintptr_t)frame, code_object);
|
|
if (frame_valid != 1) {
|
|
return frame_valid;
|
|
}
|
|
|
|
uintptr_t instruction_pointer = GET_MEMBER(uintptr_t, frame, unwinder->debug_offsets.interpreter_frame.instr_ptr);
|
|
|
|
// Get tlbc_index for free threading builds
|
|
int32_t tlbc_index = 0;
|
|
#ifdef Py_GIL_DISABLED
|
|
if (unwinder->debug_offsets.interpreter_frame.tlbc_index != 0) {
|
|
tlbc_index = GET_MEMBER(int32_t, frame, unwinder->debug_offsets.interpreter_frame.tlbc_index);
|
|
}
|
|
#endif
|
|
|
|
*address_of_code_object = code_object;
|
|
|
|
CodeObjectContext code_ctx = {
|
|
.code_addr = code_object,
|
|
.instruction_pointer = instruction_pointer,
|
|
.tlbc_index = tlbc_index,
|
|
};
|
|
return parse_code_object(unwinder, result, &code_ctx);
|
|
}
|
|
|
|
int
|
|
parse_frame_object(
|
|
RemoteUnwinderObject *unwinder,
|
|
PyObject** result,
|
|
uintptr_t address,
|
|
uintptr_t* address_of_code_object,
|
|
uintptr_t* previous_frame
|
|
) {
|
|
char frame[SIZEOF_INTERP_FRAME];
|
|
Py_ssize_t bytes_read = _Py_RemoteDebug_ReadRemoteMemory(
|
|
&unwinder->handle,
|
|
address,
|
|
SIZEOF_INTERP_FRAME,
|
|
frame
|
|
);
|
|
if (bytes_read < 0) {
|
|
set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read interpreter frame");
|
|
return -1;
|
|
}
|
|
STATS_INC(unwinder, memory_reads);
|
|
STATS_ADD(unwinder, memory_bytes_read, SIZEOF_INTERP_FRAME);
|
|
|
|
return parse_frame_buffer(unwinder, result, frame, address_of_code_object, previous_frame);
|
|
}
|
|
|
|
int
|
|
parse_frame_from_chunks(
|
|
RemoteUnwinderObject *unwinder,
|
|
PyObject **result,
|
|
uintptr_t address,
|
|
uintptr_t *previous_frame,
|
|
uintptr_t *stackpointer,
|
|
StackChunkList *chunks
|
|
) {
|
|
void *frame_ptr = find_frame_in_chunks(chunks, address);
|
|
if (!frame_ptr) {
|
|
PyErr_Format(PyExc_RuntimeError, "Frame at address 0x%lx not found in stack chunks", address);
|
|
set_exception_cause(unwinder, PyExc_RuntimeError, "Frame not found in stack chunks");
|
|
return -1;
|
|
}
|
|
|
|
char *frame = (char *)frame_ptr;
|
|
*previous_frame = GET_MEMBER(uintptr_t, frame, unwinder->debug_offsets.interpreter_frame.previous);
|
|
*stackpointer = GET_MEMBER(uintptr_t, frame, unwinder->debug_offsets.interpreter_frame.stackpointer);
|
|
uintptr_t code_object = GET_MEMBER_NO_TAG(uintptr_t, frame_ptr, unwinder->debug_offsets.interpreter_frame.executable);
|
|
int frame_valid = is_frame_valid(unwinder, (uintptr_t)frame, code_object);
|
|
if (frame_valid != 1) {
|
|
return frame_valid;
|
|
}
|
|
|
|
uintptr_t instruction_pointer = GET_MEMBER(uintptr_t, frame, unwinder->debug_offsets.interpreter_frame.instr_ptr);
|
|
|
|
// Get tlbc_index for free threading builds
|
|
int32_t tlbc_index = 0;
|
|
#ifdef Py_GIL_DISABLED
|
|
if (unwinder->debug_offsets.interpreter_frame.tlbc_index != 0) {
|
|
tlbc_index = GET_MEMBER(int32_t, frame, unwinder->debug_offsets.interpreter_frame.tlbc_index);
|
|
}
|
|
#endif
|
|
|
|
CodeObjectContext code_ctx = {
|
|
.code_addr = code_object,
|
|
.instruction_pointer = instruction_pointer,
|
|
.tlbc_index = tlbc_index,
|
|
};
|
|
return parse_code_object(unwinder, result, &code_ctx);
|
|
}
|
|
|
|
/* ============================================================================
|
|
* FRAME CHAIN PROCESSING
|
|
* ============================================================================ */
|
|
|
|
int
|
|
process_frame_chain(
|
|
RemoteUnwinderObject *unwinder,
|
|
FrameWalkContext *ctx)
|
|
{
|
|
uintptr_t frame_addr = ctx->frame_addr;
|
|
uintptr_t prev_frame_addr = 0;
|
|
uintptr_t last_frame_addr = 0;
|
|
size_t frame_count = 0;
|
|
|
|
ctx->stopped_at_cached_frame = 0;
|
|
ctx->last_frame_visited = 0;
|
|
|
|
while ((void*)frame_addr != NULL) {
|
|
PyObject *frame = NULL;
|
|
uintptr_t next_frame_addr = 0;
|
|
uintptr_t stackpointer = 0;
|
|
last_frame_addr = frame_addr;
|
|
|
|
if (++frame_count > MAX_FRAME_CHAIN_DEPTH) {
|
|
PyErr_SetString(PyExc_RuntimeError, "Too many stack frames (possible infinite loop)");
|
|
set_exception_cause(unwinder, PyExc_RuntimeError, "Frame chain iteration limit exceeded");
|
|
return -1;
|
|
}
|
|
assert(frame_count <= MAX_FRAME_CHAIN_DEPTH);
|
|
|
|
if (ctx->chunks && ctx->chunks->count > 0) {
|
|
if (parse_frame_from_chunks(unwinder, &frame, frame_addr, &next_frame_addr, &stackpointer, ctx->chunks) == 0) {
|
|
goto parsed_frame;
|
|
}
|
|
PyErr_Clear();
|
|
}
|
|
{
|
|
uintptr_t address_of_code_object = 0;
|
|
int parse_result;
|
|
if (ctx->prefetch.frame && ctx->prefetch.frame_addr == frame_addr) {
|
|
parse_result = parse_frame_buffer(
|
|
unwinder, &frame, ctx->prefetch.frame,
|
|
&address_of_code_object, &next_frame_addr);
|
|
}
|
|
else {
|
|
parse_result = parse_frame_object(
|
|
unwinder, &frame, frame_addr,
|
|
&address_of_code_object, &next_frame_addr);
|
|
}
|
|
if (parse_result < 0) {
|
|
set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to parse frame object in chain");
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
parsed_frame:
|
|
// Skip first frame if requested (used for cache miss continuation)
|
|
if (ctx->skip_first_frame && frame_count == 1) {
|
|
Py_XDECREF(frame);
|
|
frame_addr = next_frame_addr;
|
|
continue;
|
|
}
|
|
|
|
if (frame == NULL && PyList_GET_SIZE(ctx->frame_info) == 0) {
|
|
const char *e = "Failed to parse initial frame in chain";
|
|
PyErr_SetString(PyExc_RuntimeError, e);
|
|
return -1;
|
|
}
|
|
PyObject *extra_frame = NULL;
|
|
if (unwinder->gc && frame_addr == ctx->gc_frame) {
|
|
_Py_DECLARE_STR(gc, "<GC>");
|
|
extra_frame = &_Py_STR(gc);
|
|
}
|
|
else if (unwinder->native &&
|
|
frame == NULL &&
|
|
next_frame_addr &&
|
|
!(unwinder->gc && next_frame_addr == ctx->gc_frame))
|
|
{
|
|
_Py_DECLARE_STR(native, "<native>");
|
|
extra_frame = &_Py_STR(native);
|
|
}
|
|
if (extra_frame) {
|
|
PyObject *extra_frame_info = make_frame_info(
|
|
unwinder, _Py_LATIN1_CHR('~'), Py_None, extra_frame, Py_None);
|
|
if (extra_frame_info == NULL) {
|
|
Py_XDECREF(frame);
|
|
return -1;
|
|
}
|
|
if (PyList_Append(ctx->frame_info, extra_frame_info) < 0) {
|
|
Py_DECREF(extra_frame_info);
|
|
Py_XDECREF(frame);
|
|
set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to append extra frame");
|
|
return -1;
|
|
}
|
|
if (ctx->frame_addrs && ctx->num_addrs < ctx->max_addrs) {
|
|
assert(ctx->num_addrs >= 0);
|
|
ctx->frame_addrs[ctx->num_addrs++] = 0;
|
|
}
|
|
Py_DECREF(extra_frame_info);
|
|
}
|
|
if (frame) {
|
|
if (prev_frame_addr && frame_addr != prev_frame_addr) {
|
|
const char *f = "Broken frame chain: expected frame at 0x%lx, got 0x%lx";
|
|
PyErr_Format(PyExc_RuntimeError, f, prev_frame_addr, frame_addr);
|
|
Py_DECREF(frame);
|
|
set_exception_cause(unwinder, PyExc_RuntimeError, "Frame chain consistency check failed");
|
|
return -1;
|
|
}
|
|
|
|
if (PyList_Append(ctx->frame_info, frame) < 0) {
|
|
Py_DECREF(frame);
|
|
set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to append frame");
|
|
return -1;
|
|
}
|
|
if (ctx->frame_addrs && ctx->num_addrs < ctx->max_addrs) {
|
|
assert(ctx->num_addrs >= 0);
|
|
ctx->frame_addrs[ctx->num_addrs++] = frame_addr;
|
|
}
|
|
Py_DECREF(frame);
|
|
}
|
|
|
|
if (ctx->last_profiled.frame != 0 && frame_addr == ctx->last_profiled.frame) {
|
|
ctx->stopped_at_cached_frame = 1;
|
|
break;
|
|
}
|
|
|
|
prev_frame_addr = next_frame_addr;
|
|
frame_addr = next_frame_addr;
|
|
}
|
|
|
|
if (!ctx->stopped_at_cached_frame && ctx->base_frame_addr != 0 && last_frame_addr != ctx->base_frame_addr) {
|
|
PyErr_Format(PyExc_RuntimeError,
|
|
"Incomplete sample: did not reach base frame (expected 0x%lx, got 0x%lx)",
|
|
ctx->base_frame_addr, last_frame_addr);
|
|
return -1;
|
|
}
|
|
|
|
ctx->last_frame_visited = last_frame_addr;
|
|
|
|
return 0;
|
|
}
|
|
|
|
int
|
|
set_last_profiled_frame(RemoteUnwinderObject *unwinder, uintptr_t tstate_addr,
|
|
uintptr_t frame_addr)
|
|
{
|
|
uintptr_t lpf_addr = tstate_addr +
|
|
(uintptr_t)unwinder->debug_offsets.thread_state.last_profiled_frame;
|
|
return _Py_RemoteDebug_WriteRemoteMemory(&unwinder->handle, lpf_addr,
|
|
sizeof(uintptr_t), &frame_addr);
|
|
}
|
|
|
|
// Clear the profiler anchor frame for all threads in the target process. The
|
|
// sequence is intentionally preserved: a zero frame disables cache lookup, and
|
|
// the next profiler-owned anchor should use the target's current generation.
|
|
int
|
|
clear_last_profiled_frames(RemoteUnwinderObject *unwinder)
|
|
{
|
|
uintptr_t current_interp = unwinder->interpreter_addr;
|
|
const size_t MAX_INTERPRETERS = 256;
|
|
size_t interp_count = 0;
|
|
|
|
while (current_interp != 0 && interp_count < MAX_INTERPRETERS) {
|
|
interp_count++;
|
|
// Get first thread in this interpreter
|
|
uintptr_t tstate_addr;
|
|
if (_Py_RemoteDebug_PagedReadRemoteMemory(
|
|
&unwinder->handle,
|
|
current_interp + unwinder->debug_offsets.interpreter_state.threads_head,
|
|
sizeof(void*),
|
|
&tstate_addr) < 0) {
|
|
// Non-fatal: just skip clearing
|
|
PyErr_Clear();
|
|
return 0;
|
|
}
|
|
|
|
// Iterate all threads in this interpreter
|
|
const size_t MAX_THREADS_PER_INTERP = 8192;
|
|
size_t thread_count = 0;
|
|
while (tstate_addr != 0 && thread_count < MAX_THREADS_PER_INTERP) {
|
|
thread_count++;
|
|
uintptr_t no_frame = 0;
|
|
if (set_last_profiled_frame(unwinder, tstate_addr, no_frame) < 0) {
|
|
PyErr_Clear();
|
|
}
|
|
|
|
// Move to next thread
|
|
if (_Py_RemoteDebug_PagedReadRemoteMemory(
|
|
&unwinder->handle,
|
|
tstate_addr + unwinder->debug_offsets.thread_state.next,
|
|
sizeof(void*),
|
|
&tstate_addr) < 0) {
|
|
PyErr_Clear();
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Move to next interpreter
|
|
if (_Py_RemoteDebug_PagedReadRemoteMemory(
|
|
&unwinder->handle,
|
|
current_interp + unwinder->debug_offsets.interpreter_state.next,
|
|
sizeof(void*),
|
|
¤t_interp) < 0) {
|
|
PyErr_Clear();
|
|
break;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
// Fast path: check if we have a full cache hit (parent stack unchanged)
|
|
// A "full hit" means current frame == last profiled frame, so we can reuse
|
|
// cached parent frames. We always read the current frame from memory to get
|
|
// updated line numbers (the line within a frame can change between samples).
|
|
// Returns: 1 if full hit (frame_info populated with current frame + cached parents),
|
|
// 0 if miss, -1 on error
|
|
static int
|
|
try_full_cache_hit(
|
|
RemoteUnwinderObject *unwinder,
|
|
const FrameWalkContext *ctx,
|
|
uint64_t thread_id)
|
|
{
|
|
if (!unwinder->frame_cache || ctx->last_profiled.frame == 0) {
|
|
return 0;
|
|
}
|
|
if (ctx->frame_addr != ctx->last_profiled.frame) {
|
|
return 0;
|
|
}
|
|
|
|
FrameCacheEntry *entry = frame_cache_find(unwinder, thread_id);
|
|
if (!entry || !entry->frame_list) {
|
|
return 0;
|
|
}
|
|
if (entry->thread_state_addr != ctx->thread_state_addr) {
|
|
return 0;
|
|
}
|
|
|
|
if (entry->num_addrs == 0 || entry->addrs[0] != ctx->frame_addr) {
|
|
return 0;
|
|
}
|
|
if (entry->last_profiled_frame_seq != ctx->last_profiled.seq) {
|
|
return 0;
|
|
}
|
|
|
|
PyObject *current_frame = NULL;
|
|
uintptr_t code_object_addr = 0;
|
|
uintptr_t previous_frame = 0;
|
|
int parse_result;
|
|
if (ctx->prefetch.frame && ctx->prefetch.frame_addr == ctx->frame_addr) {
|
|
parse_result = parse_frame_buffer(unwinder, ¤t_frame,
|
|
ctx->prefetch.frame,
|
|
&code_object_addr, &previous_frame);
|
|
}
|
|
else {
|
|
parse_result = parse_frame_object(unwinder, ¤t_frame, ctx->frame_addr,
|
|
&code_object_addr, &previous_frame);
|
|
}
|
|
if (parse_result < 0) {
|
|
return -1;
|
|
}
|
|
if (!frame_cache_anchor_matches(unwinder, ctx->thread_state_addr,
|
|
ctx->last_profiled)) {
|
|
Py_XDECREF(current_frame);
|
|
return 0;
|
|
}
|
|
|
|
if (current_frame != NULL) {
|
|
if (PyList_Append(ctx->frame_info, current_frame) < 0) {
|
|
Py_DECREF(current_frame);
|
|
return -1;
|
|
}
|
|
Py_DECREF(current_frame);
|
|
STATS_ADD(unwinder, frames_read_from_memory, 1);
|
|
}
|
|
|
|
Py_ssize_t cached_size = PyList_GET_SIZE(entry->frame_list);
|
|
for (Py_ssize_t i = 1; i < cached_size; i++) {
|
|
PyObject *cached_frame = PyList_GET_ITEM(entry->frame_list, i);
|
|
if (PyList_Append(ctx->frame_info, cached_frame) < 0) {
|
|
return -1;
|
|
}
|
|
}
|
|
STATS_ADD(unwinder, frames_read_from_cache, cached_size > 1 ? cached_size - 1 : 0);
|
|
|
|
STATS_INC(unwinder, frame_cache_hits);
|
|
return 1;
|
|
}
|
|
|
|
// High-level helper: collect frames with cache optimization
|
|
// Returns complete frame_info list, handling all cache logic internally
|
|
int
|
|
collect_frames_with_cache(
|
|
RemoteUnwinderObject *unwinder,
|
|
FrameWalkContext *ctx,
|
|
uint64_t thread_id)
|
|
{
|
|
int full_hit = try_full_cache_hit(unwinder, ctx, thread_id);
|
|
if (full_hit != 0) {
|
|
return full_hit < 0 ? -1 : 0;
|
|
}
|
|
|
|
assert(ctx->chunks != NULL);
|
|
|
|
// Cache misses copy stack chunks before walking. Frames found there are
|
|
// parsed from a stable snapshot, which keeps moving stacks from seeding the
|
|
// cache with an impossible parent chain.
|
|
if (ctx->chunks->count == 0) {
|
|
if (copy_stack_chunks(unwinder, ctx->thread_state_addr, ctx->chunks) < 0) {
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
Py_ssize_t frames_before = PyList_GET_SIZE(ctx->frame_info);
|
|
|
|
if (process_frame_chain(unwinder, ctx) < 0) {
|
|
return -1;
|
|
}
|
|
|
|
STATS_ADD(unwinder, frames_read_from_memory, PyList_GET_SIZE(ctx->frame_info) - frames_before);
|
|
|
|
if (ctx->stopped_at_cached_frame) {
|
|
Py_ssize_t frames_before_cache = PyList_GET_SIZE(ctx->frame_info);
|
|
int cache_result = frame_cache_lookup_and_extend(unwinder, thread_id,
|
|
ctx->thread_state_addr,
|
|
ctx->last_profiled,
|
|
ctx->frame_info, ctx->frame_addrs, &ctx->num_addrs,
|
|
ctx->max_addrs);
|
|
if (cache_result < 0) {
|
|
return -1;
|
|
}
|
|
if (cache_result == 0) {
|
|
STATS_INC(unwinder, frame_cache_misses);
|
|
|
|
// Continue walking from last_profiled_frame, skipping it (already processed)
|
|
Py_ssize_t frames_before_walk = PyList_GET_SIZE(ctx->frame_info);
|
|
FrameWalkContext continue_ctx = {
|
|
.frame_addr = ctx->last_profiled.frame,
|
|
.base_frame_addr = ctx->base_frame_addr,
|
|
.gc_frame = ctx->gc_frame,
|
|
.chunks = ctx->chunks,
|
|
.skip_first_frame = 1,
|
|
.frame_info = ctx->frame_info,
|
|
.frame_addrs = ctx->frame_addrs,
|
|
.num_addrs = ctx->num_addrs,
|
|
.max_addrs = ctx->max_addrs,
|
|
};
|
|
if (process_frame_chain(unwinder, &continue_ctx) < 0) {
|
|
return -1;
|
|
}
|
|
ctx->num_addrs = continue_ctx.num_addrs;
|
|
ctx->last_frame_visited = continue_ctx.last_frame_visited;
|
|
STATS_ADD(unwinder, frames_read_from_memory, PyList_GET_SIZE(ctx->frame_info) - frames_before_walk);
|
|
} else {
|
|
// Partial cache hit - cached stack was validated as complete when stored,
|
|
// so set last_frame_visited to base_frame_addr for validation in frame_cache_store
|
|
ctx->last_frame_visited = ctx->base_frame_addr;
|
|
STATS_INC(unwinder, frame_cache_partial_hits);
|
|
STATS_ADD(unwinder, frames_read_from_cache, PyList_GET_SIZE(ctx->frame_info) - frames_before_cache);
|
|
}
|
|
} else {
|
|
if (ctx->last_profiled.frame == 0) {
|
|
STATS_INC(unwinder, frame_cache_misses);
|
|
}
|
|
}
|
|
|
|
if (frame_cache_store(unwinder, thread_id, ctx->frame_info, ctx->frame_addrs,
|
|
ctx->num_addrs, ctx->thread_state_addr,
|
|
ctx->last_profiled.seq, ctx->base_frame_addr,
|
|
ctx->last_frame_visited) < 0) {
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|