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

Commit 1944a02

Browse files
committed
Fixes for small edge cases, docs and cache stats
1 parent 8df2106 commit 1944a02

File tree

9 files changed

+107
-15
lines changed

9 files changed

+107
-15
lines changed

InternalDocs/frames.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,9 @@ and can be retrieved from a cache. This significantly reduces the amount of remo
126126
memory reads needed when call stacks are deep and stable at their base.
127127

128128
The update in `_PyEval_FrameClearAndPop` is guarded: it only writes when
129-
`last_profiled_frame` is non-NULL, avoiding any overhead when profiling is inactive.
129+
`last_profiled_frame` is non-NULL AND matches the frame being popped. This
130+
prevents transient frames (called and returned between profiler samples) from
131+
corrupting the cache pointer, while avoiding any overhead when profiling is inactive.
130132

131133

132134
### The Instruction Pointer

Lib/profiling/sampling/sample.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,18 @@ def _print_unwinder_stats(self):
255255
print(f" Hits: {code_hits:,} ({ANSIColors.GREEN}{code_hits_pct:.1f}%{ANSIColors.RESET})")
256256
print(f" Misses: {code_misses:,} ({ANSIColors.RED}{code_misses_pct:.1f}%{ANSIColors.RESET})")
257257

258+
# Memory operations
259+
memory_reads = stats.get('memory_reads', 0)
260+
memory_bytes = stats.get('memory_bytes_read', 0)
261+
if memory_bytes >= 1024 * 1024:
262+
memory_str = f"{memory_bytes / (1024 * 1024):.1f} MB"
263+
elif memory_bytes >= 1024:
264+
memory_str = f"{memory_bytes / 1024:.1f} KB"
265+
else:
266+
memory_str = f"{memory_bytes} B"
267+
print(f" {ANSIColors.CYAN}Memory:{ANSIColors.RESET}")
268+
print(f" Read operations: {memory_reads:,} ({memory_str})")
269+
258270
# Stale invalidations
259271
stale_invalidations = stats.get('stale_cache_invalidations', 0)
260272
if stale_invalidations > 0:

Lib/test/test_external_inspection.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2745,6 +2745,60 @@ def recurse(n):
27452745
self.assertEqual(len(frames_cached), len(frames_no_cache),
27462746
"Cache exhaustion should not affect stack completeness")
27472747

2748+
@skip_if_not_supported
2749+
@unittest.skipIf(
2750+
sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED,
2751+
"Test only runs on Linux with process_vm_readv support",
2752+
)
2753+
def test_get_stats(self):
2754+
"""Test that get_stats() returns statistics when stats=True."""
2755+
script_body = """\
2756+
sock.sendall(b"ready")
2757+
sock.recv(16)
2758+
"""
2759+
2760+
with self._target_process(script_body) as (p, client_socket, _):
2761+
unwinder = RemoteUnwinder(p.pid, all_threads=True, stats=True)
2762+
self._wait_for_signal(client_socket, b"ready")
2763+
2764+
# Take a sample
2765+
unwinder.get_stack_trace()
2766+
2767+
stats = unwinder.get_stats()
2768+
client_socket.sendall(b"done")
2769+
2770+
# Verify expected keys exist
2771+
expected_keys = [
2772+
'total_samples', 'frame_cache_hits', 'frame_cache_misses',
2773+
'frame_cache_partial_hits', 'frames_read_from_cache',
2774+
'frames_read_from_memory', 'frame_cache_hit_rate'
2775+
]
2776+
for key in expected_keys:
2777+
self.assertIn(key, stats)
2778+
2779+
self.assertEqual(stats['total_samples'], 1)
2780+
2781+
@skip_if_not_supported
2782+
@unittest.skipIf(
2783+
sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED,
2784+
"Test only runs on Linux with process_vm_readv support",
2785+
)
2786+
def test_get_stats_disabled_raises(self):
2787+
"""Test that get_stats() raises RuntimeError when stats=False."""
2788+
script_body = """\
2789+
sock.sendall(b"ready")
2790+
sock.recv(16)
2791+
"""
2792+
2793+
with self._target_process(script_body) as (p, client_socket, _):
2794+
unwinder = RemoteUnwinder(p.pid, all_threads=True) # stats=False by default
2795+
self._wait_for_signal(client_socket, b"ready")
2796+
2797+
with self.assertRaises(RuntimeError):
2798+
unwinder.get_stats()
2799+
2800+
client_socket.sendall(b"done")
2801+
27482802

27492803
if __name__ == "__main__":
27502804
unittest.main()

Modules/_remote_debugging/_remote_debugging.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ typedef struct {
174174
uint64_t frames_read_from_cache; // Total frames retrieved from cache
175175
uint64_t frames_read_from_memory; // Total frames read from remote memory
176176
uint64_t memory_reads; // Total remote memory read operations
177+
uint64_t memory_bytes_read; // Total bytes read from remote memory
177178
uint64_t code_object_cache_hits; // Code object cache hits
178179
uint64_t code_object_cache_misses; // Code object cache misses
179180
uint64_t stale_cache_invalidations; // Times stale entries were cleared
@@ -422,6 +423,7 @@ extern int frame_cache_lookup_and_extend(
422423
uintptr_t *frame_addrs,
423424
Py_ssize_t *num_addrs,
424425
Py_ssize_t max_addrs);
426+
// Returns: 1 = stored, 0 = not stored (graceful), -1 = error
425427
extern int frame_cache_store(
426428
RemoteUnwinderObject *unwinder,
427429
uint64_t thread_id,

Modules/_remote_debugging/clinic/module.c.h

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Modules/_remote_debugging/frame_cache.c

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ frame_cache_lookup_and_extend(
194194
}
195195

196196
// Store frame list with addresses in cache
197+
// Returns: 1 = stored successfully, 0 = not stored (graceful degradation), -1 = error
197198
int
198199
frame_cache_store(
199200
RemoteUnwinderObject *unwinder,
@@ -220,11 +221,14 @@ frame_cache_store(
220221
// Clear old frame_list if replacing
221222
Py_CLEAR(entry->frame_list);
222223

223-
// Store data
224+
// Store data - truncate frame_list to match num_addrs
225+
entry->frame_list = PyList_GetSlice(frame_list, 0, num_addrs);
226+
if (!entry->frame_list) {
227+
return -1;
228+
}
224229
entry->thread_id = thread_id;
225230
memcpy(entry->addrs, addrs, num_addrs * sizeof(uintptr_t));
226231
entry->num_addrs = num_addrs;
227-
entry->frame_list = Py_NewRef(frame_list);
228232

229-
return 0;
233+
return 1;
230234
}

Modules/_remote_debugging/frames.c

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,8 @@ parse_frame_object(
189189
set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read interpreter frame");
190190
return -1;
191191
}
192+
STATS_INC(unwinder, memory_reads);
193+
STATS_ADD(unwinder, memory_bytes_read, SIZEOF_INTERP_FRAME);
192194

193195
*previous_frame = GET_MEMBER(uintptr_t, frame, unwinder->debug_offsets.interpreter_frame.previous);
194196
uintptr_t code_object = GET_MEMBER_NO_TAG(uintptr_t, frame, unwinder->debug_offsets.interpreter_frame.executable);
@@ -477,23 +479,30 @@ try_full_cache_hit(
477479
return -1;
478480
}
479481

480-
// Add current frame if valid
482+
// Get cached parent frames first (before modifying frame_info)
483+
Py_ssize_t cached_size = PyList_GET_SIZE(entry->frame_list);
484+
PyObject *parent_slice = NULL;
485+
if (cached_size > 1) {
486+
parent_slice = PyList_GetSlice(entry->frame_list, 1, cached_size);
487+
if (!parent_slice) {
488+
Py_XDECREF(current_frame);
489+
return -1;
490+
}
491+
}
492+
493+
// Now safe to modify frame_info - add current frame if valid
481494
if (current_frame != NULL) {
482495
if (PyList_Append(frame_info, current_frame) < 0) {
483496
Py_DECREF(current_frame);
497+
Py_XDECREF(parent_slice);
484498
return -1;
485499
}
486500
Py_DECREF(current_frame);
487501
STATS_ADD(unwinder, frames_read_from_memory, 1);
488502
}
489503

490-
// Extend with cached parent frames (from index 1 onwards, skipping the current frame)
491-
Py_ssize_t cached_size = PyList_GET_SIZE(entry->frame_list);
492-
if (cached_size > 1) {
493-
PyObject *parent_slice = PyList_GetSlice(entry->frame_list, 1, cached_size);
494-
if (!parent_slice) {
495-
return -1;
496-
}
504+
// Extend with cached parent frames
505+
if (parent_slice) {
497506
Py_ssize_t cur_size = PyList_GET_SIZE(frame_info);
498507
int result = PyList_SetSlice(frame_info, cur_size, cur_size, parent_slice);
499508
Py_DECREF(parent_slice);
@@ -569,7 +578,9 @@ collect_frames_with_cache(
569578
}
570579

571580
// Store in cache (frame_cache_store handles truncation if num_addrs > FRAME_CACHE_MAX_FRAMES)
572-
frame_cache_store(unwinder, thread_id, frame_info, addrs, num_addrs);
581+
if (frame_cache_store(unwinder, thread_id, frame_info, addrs, num_addrs) < 0) {
582+
return -1;
583+
}
573584

574585
return 0;
575586
}

Modules/_remote_debugging/module.c

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -814,6 +814,7 @@ RemoteUnwinder was created with stats=True.
814814
- frames_read_from_cache: Total frames retrieved from cache
815815
- frames_read_from_memory: Total frames read from remote memory
816816
- memory_reads: Total remote memory read operations
817+
- memory_bytes_read: Total bytes read from remote memory
817818
- code_object_cache_hits: Code object cache hits
818819
- code_object_cache_misses: Code object cache misses
819820
- stale_cache_invalidations: Times stale cache entries were cleared
@@ -826,7 +827,7 @@ RemoteUnwinder was created with stats=True.
826827

827828
static PyObject *
828829
_remote_debugging_RemoteUnwinder_get_stats_impl(RemoteUnwinderObject *self)
829-
/*[clinic end generated code: output=21e36477122be2a0 input=0a037cbf1c572d2b]*/
830+
/*[clinic end generated code: output=21e36477122be2a0 input=75fef4134c12a8c9]*/
830831
{
831832
if (!self->collect_stats) {
832833
PyErr_SetString(PyExc_RuntimeError,
@@ -857,6 +858,7 @@ _remote_debugging_RemoteUnwinder_get_stats_impl(RemoteUnwinderObject *self)
857858
ADD_STAT(frames_read_from_cache);
858859
ADD_STAT(frames_read_from_memory);
859860
ADD_STAT(memory_reads);
861+
ADD_STAT(memory_bytes_read);
860862
ADD_STAT(code_object_cache_hits);
861863
ADD_STAT(code_object_cache_misses);
862864
ADD_STAT(stale_cache_invalidations);

Modules/_remote_debugging/threads.c

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,8 @@ unwind_stack_for_thread(
296296
set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read thread state");
297297
goto error;
298298
}
299+
STATS_INC(unwinder, memory_reads);
300+
STATS_ADD(unwinder, memory_bytes_read, unwinder->debug_offsets.thread_state.size);
299301

300302
long tid = GET_MEMBER(long, ts, unwinder->debug_offsets.thread_state.native_thread_id);
301303

@@ -309,6 +311,8 @@ unwind_stack_for_thread(
309311
set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read GC state");
310312
goto error;
311313
}
314+
STATS_INC(unwinder, memory_reads);
315+
STATS_ADD(unwinder, memory_bytes_read, unwinder->debug_offsets.gc.size);
312316

313317
// Calculate thread status using flags (always)
314318
int status_flags = 0;

0 commit comments

Comments
 (0)