🌐 AI搜索 & 代理 主页
Skip to content

Commit 32facd8

Browse files
committed
Fix frame cache miss handling in RemoteUnwinder to return complete stacks
When the frame walker stopped at a cached frame boundary but the cache lookup failed to find the continuation (e.g., when a new unwinder instance encounters stale last_profiled_frame values from a previous profiler), the code would silently return an incomplete stack. Now when a cache lookup misses, the walker continues from that point to collect the remaining frames, ensuring complete stack traces are always returned.
1 parent 9c64ae1 commit 32facd8

File tree

2 files changed

+61
-0
lines changed

2 files changed

+61
-0
lines changed

Lib/test/test_external_inspection.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2559,6 +2559,59 @@ def get_thread_frames(target_funcs):
25592559
# No cross-contamination
25602560
self.assertNotIn("blech1", t2_blech)
25612561

2562+
@skip_if_not_supported
2563+
@unittest.skipIf(
2564+
sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED,
2565+
"Test only runs on Linux with process_vm_readv support",
2566+
)
2567+
def test_new_unwinder_with_stale_last_profiled_frame(self):
2568+
"""Test that a new unwinder returns complete stack when cache lookup misses."""
2569+
script_body = """\
2570+
def level4():
2571+
sock.sendall(b"sync1")
2572+
sock.recv(16)
2573+
sock.sendall(b"sync2")
2574+
sock.recv(16)
2575+
2576+
def level3():
2577+
level4()
2578+
2579+
def level2():
2580+
level3()
2581+
2582+
def level1():
2583+
level2()
2584+
2585+
level1()
2586+
"""
2587+
2588+
with self._target_process(script_body) as (p, client_socket, make_unwinder):
2589+
expected = {"level1", "level2", "level3", "level4"}
2590+
2591+
# First unwinder samples - this sets last_profiled_frame in target
2592+
unwinder1 = make_unwinder(cache_frames=True)
2593+
frames1 = self._sample_frames(client_socket, unwinder1, b"sync1", b"ack", expected)
2594+
2595+
# Create NEW unwinder (empty cache) and sample
2596+
# The target still has last_profiled_frame set from unwinder1
2597+
unwinder2 = make_unwinder(cache_frames=True)
2598+
frames2 = self._sample_frames(client_socket, unwinder2, b"sync2", b"done", expected)
2599+
2600+
self.assertIsNotNone(frames1)
2601+
self.assertIsNotNone(frames2)
2602+
2603+
funcs1 = [f.funcname for f in frames1]
2604+
funcs2 = [f.funcname for f in frames2]
2605+
2606+
# Both should have all levels
2607+
for level in ["level1", "level2", "level3", "level4"]:
2608+
self.assertIn(level, funcs1, f"{level} missing from first sample")
2609+
self.assertIn(level, funcs2, f"{level} missing from second sample")
2610+
2611+
# Should have same stack depth
2612+
self.assertEqual(len(frames1), len(frames2),
2613+
"New unwinder should return complete stack despite stale last_profiled_frame")
2614+
25622615

25632616
if __name__ == "__main__":
25642617
unittest.main()

Modules/_remote_debugging/frames.c

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -770,6 +770,14 @@ collect_frames_with_cache(
770770
Py_DECREF(frame_addresses);
771771
return -1;
772772
}
773+
if (cache_result == 0) {
774+
// Cache miss - continue walking from last_profiled_frame to get the rest
775+
if (process_frame_chain(unwinder, last_profiled_frame, chunks, frame_info, gc_frame,
776+
0, NULL, frame_addresses) < 0) {
777+
Py_DECREF(frame_addresses);
778+
return -1;
779+
}
780+
}
773781
}
774782

775783
// Convert frame_addresses (list of PyLong) to C array for efficient cache storage

0 commit comments

Comments
 (0)