Skip to content

Commit 7d98020

Browse files
committed
metadata
1 parent 6692aec commit 7d98020

3 files changed

Lines changed: 252 additions & 1 deletion

File tree

Lib/test/test_external_inspection.py

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3804,5 +3804,170 @@ def test_get_stats_disabled_raises(self):
38043804
client_socket.sendall(b"done")
38053805

38063806

3807+
@requires_remote_subprocess_debugging()
3808+
class TestMetadataDegradation(RemoteInspectionTestBase):
3809+
"""Tests for graceful degradation of oversized code-object metadata."""
3810+
3811+
@contextmanager
3812+
def _running_target(self, script_body):
3813+
"""Run a target script (socket handshake prepended), yield (process, socket)."""
3814+
port = find_unused_port()
3815+
script = (
3816+
textwrap.dedent(
3817+
f"""\
3818+
import time, socket
3819+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
3820+
sock.connect(('localhost', {port}))
3821+
"""
3822+
)
3823+
+ textwrap.dedent(script_body)
3824+
)
3825+
3826+
with os_helper.temp_dir() as work_dir:
3827+
script_dir = os.path.join(work_dir, "script_pkg")
3828+
os.mkdir(script_dir)
3829+
3830+
server_socket = _create_server_socket(port)
3831+
script_name = _make_test_script(script_dir, "script", script)
3832+
client_socket = None
3833+
3834+
try:
3835+
with _managed_subprocess([sys.executable, script_name]) as p:
3836+
client_socket, _ = server_socket.accept()
3837+
server_socket.close()
3838+
server_socket = None
3839+
yield p, client_socket
3840+
finally:
3841+
_cleanup_sockets(client_socket, server_socket)
3842+
3843+
def _sample_until_frame(self, pid, predicate):
3844+
"""Sample until a frame matching predicate appears; return that frame."""
3845+
unwinder = RemoteUnwinder(pid, all_threads=True)
3846+
traces = _get_stack_trace_with_retry(
3847+
unwinder,
3848+
condition=lambda t: self._find_frame_in_trace(t, predicate)
3849+
is not None,
3850+
)
3851+
return self._find_frame_in_trace(traces, predicate)
3852+
3853+
@skip_if_not_supported
3854+
@unittest.skipIf(
3855+
sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED,
3856+
"Test only runs on Linux with process_vm_readv support",
3857+
)
3858+
def test_long_qualname_truncated_not_dropped(self):
3859+
"""A qualname longer than 1024 chars is truncated with a marker
3860+
instead of failing the whole sample."""
3861+
script_body = """\
3862+
src = ("def " + "f" * 1100 + "():\\n"
3863+
" sock.sendall(b'ready')\\n"
3864+
" time.sleep(10_000)\\n")
3865+
ns = {"sock": sock, "time": time}
3866+
exec(src, ns)
3867+
ns["f" * 1100]()
3868+
"""
3869+
with self._running_target(script_body) as (p, client_socket):
3870+
_wait_for_signal(client_socket, b"ready")
3871+
frame = self._sample_until_frame(
3872+
p.pid, lambda f: f.funcname.startswith("fff")
3873+
)
3874+
self.assertEqual(
3875+
frame.funcname, "f" * 1024 + "…(len=1100)"
3876+
)
3877+
3878+
@skip_if_not_supported
3879+
@unittest.skipIf(
3880+
sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED,
3881+
"Test only runs on Linux with process_vm_readv support",
3882+
)
3883+
def test_long_filename_truncated(self):
3884+
"""A filename longer than 1024 chars is truncated with a marker
3885+
instead of failing the whole sample."""
3886+
script_body = """\
3887+
src = ("def g():\\n"
3888+
" sock.sendall(b'ready')\\n"
3889+
" time.sleep(10_000)\\n")
3890+
ns = {"sock": sock, "time": time}
3891+
exec(compile(src, "x" * 1500 + ".py", "exec"), ns)
3892+
ns["g"]()
3893+
"""
3894+
with self._running_target(script_body) as (p, client_socket):
3895+
_wait_for_signal(client_socket, b"ready")
3896+
frame = self._sample_until_frame(
3897+
p.pid, lambda f: f.funcname == "g"
3898+
)
3899+
self.assertEqual(
3900+
frame.filename, "x" * 1024 + "…(len=1503)"
3901+
)
3902+
3903+
@skip_if_not_supported
3904+
@unittest.skipIf(
3905+
sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED,
3906+
"Test only runs on Linux with process_vm_readv support",
3907+
)
3908+
def test_large_linetable_keeps_line_numbers(self):
3909+
"""A linetable larger than the old 4096-byte cap keeps line numbers."""
3910+
script_body = """\
3911+
body = "\\n".join(" x%d = %d" % (i, i) for i in range(4000))
3912+
src = ("def big():\\n" + body + "\\n"
3913+
" sock.sendall(b'ready')\\n"
3914+
" time.sleep(10_000)\\n")
3915+
ns = {"sock": sock, "time": time}
3916+
exec(src, ns)
3917+
sock.sendall(b"lt:%d\\n" % len(ns["big"].__code__.co_linetable))
3918+
ns["big"]()
3919+
"""
3920+
with self._running_target(script_body) as (p, client_socket):
3921+
buffer = _wait_for_signal(client_socket, [b"lt:", b"ready"])
3922+
linetable_size = int(
3923+
buffer.partition(b"lt:")[2].partition(b"\n")[0]
3924+
)
3925+
self.assertGreater(linetable_size, 4096)
3926+
3927+
frame = self._sample_until_frame(
3928+
p.pid, lambda f: f.funcname == "big"
3929+
)
3930+
self.assertIsNotNone(
3931+
frame.location, "line info degraded for large linetable"
3932+
)
3933+
self.assertGreater(frame.location.lineno, 4000)
3934+
3935+
@skip_if_not_supported
3936+
@unittest.skipIf(
3937+
sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED,
3938+
"Test only runs on Linux with process_vm_readv support",
3939+
)
3940+
@unittest.skipIf(
3941+
sys.platform == "win32",
3942+
"Process death maps to ProcessLookupError only on POSIX platforms",
3943+
)
3944+
def test_dead_process_raises_not_degrades(self):
3945+
"""Death of the target raises ProcessLookupError instead of
3946+
degrading to synthetic frames."""
3947+
script_body = """\
3948+
sock.sendall(b"ready")
3949+
time.sleep(10_000)
3950+
"""
3951+
with self._running_target(script_body) as (p, client_socket):
3952+
_wait_for_signal(client_socket, b"ready")
3953+
unwinder = RemoteUnwinder(p.pid, all_threads=True)
3954+
_get_stack_trace_with_retry(unwinder)
3955+
3956+
p.kill()
3957+
p.wait()
3958+
3959+
for _ in busy_retry(SHORT_TIMEOUT, error=False):
3960+
try:
3961+
unwinder.get_stack_trace()
3962+
except ProcessLookupError:
3963+
break
3964+
except RuntimeError:
3965+
continue
3966+
else:
3967+
self.fail(
3968+
"ProcessLookupError never raised for dead process"
3969+
)
3970+
3971+
38073972
if __name__ == "__main__":
38083973
unittest.main()

Lib/test/test_profiling/test_sampling_profiler/test_binary_format.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -628,6 +628,41 @@ def test_same_line_different_columns(self):
628628
collector, count = self.roundtrip(samples)
629629
self.assertEqual(count, 3)
630630

631+
def test_synthetic_frames_roundtrip(self):
632+
"""Degraded/sentinel frames (location=None) survive the binary format."""
633+
frames = [
634+
FrameInfo(("~", None, name, None))
635+
for name in _remote_debugging.SYNTHETIC_FRAME_NAMES
636+
]
637+
frames.append(FrameInfo(("app.py", None, "<unknown function>", None)))
638+
frames.append(FrameInfo(("<unknown file>", None, "real_func", None)))
639+
samples = [[make_interpreter(0, [make_thread(1, frames)])]]
640+
641+
collector, count = self.roundtrip(samples)
642+
self.assertEqual(count, 1)
643+
self.assert_samples_equal(samples, collector)
644+
645+
class RawFrameCollector:
646+
def __init__(self):
647+
self.frames = []
648+
649+
def collect(self, stack_frames, timestamps_us):
650+
for interp in stack_frames:
651+
for thread in interp.threads:
652+
self.frames.extend(thread.frame_info)
653+
654+
def export(self, filename):
655+
pass
656+
657+
raw = RawFrameCollector()
658+
with BinaryReader(self.create_binary_file(samples)) as reader:
659+
reader.replay_samples(raw)
660+
self.assertEqual(len(raw.frames), len(frames))
661+
for original, replayed in zip(frames, raw.frames):
662+
self.assertEqual(replayed.filename, original.filename)
663+
self.assertEqual(replayed.funcname, original.funcname)
664+
self.assertIsNone(replayed.location)
665+
631666

632667
class TestBinaryEdgeCases(BinaryFormatTestBase):
633668
"""Tests for edge cases in binary format."""

Lib/test/test_profiling/test_sampling_profiler/test_collectors.py

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,17 @@
2020
from profiling.sampling.jsonl_collector import JsonlCollector
2121
from profiling.sampling.gecko_collector import GeckoCollector
2222
from profiling.sampling.heatmap_collector import _TemplateLoader
23-
from profiling.sampling.collector import extract_lineno, normalize_location
23+
from profiling.sampling.collector import (
24+
extract_lineno,
25+
is_synthetic,
26+
normalize_location,
27+
)
2428
from profiling.sampling.opcode_utils import get_opcode_info, format_opcode
2529
from profiling.sampling.constants import (
2630
PROFILING_MODE_WALL,
2731
PROFILING_MODE_CPU,
2832
DEFAULT_LOCATION,
33+
SYNTHETIC_FRAME_NAMES,
2934
)
3035
from _remote_debugging import (
3136
THREAD_STATUS_HAS_GIL,
@@ -3204,3 +3209,49 @@ def test_jsonl_collector_filters_internal_frames(self):
32043209

32053210
for path in paths:
32063211
self.assertNotIn("_sync_coordinator", path)
3212+
3213+
3214+
class TestSyntheticFrames(unittest.TestCase):
3215+
"""Tests for SYNTHETIC_FRAME_NAMES and the is_synthetic predicate."""
3216+
3217+
def test_synthetic_frame_names_exact_contract(self):
3218+
expected = (
3219+
"<GC>",
3220+
"<native>",
3221+
"<unknown function>",
3222+
"<unknown file>",
3223+
"<unreadable frame>",
3224+
)
3225+
self.assertEqual(_remote_debugging.SYNTHETIC_FRAME_NAMES, expected)
3226+
3227+
def test_synthetic_frame_names_parity_with_fallback(self):
3228+
self.assertEqual(
3229+
frozenset(_remote_debugging.SYNTHETIC_FRAME_NAMES),
3230+
SYNTHETIC_FRAME_NAMES,
3231+
)
3232+
3233+
def test_is_synthetic_sentinel_funcnames(self):
3234+
for name in SYNTHETIC_FRAME_NAMES:
3235+
with self.subTest(name=name):
3236+
self.assertTrue(is_synthetic(("app.py", None, name, None)))
3237+
3238+
def test_is_synthetic_tilde_filename(self):
3239+
self.assertTrue(is_synthetic(("~", None, "anything", None)))
3240+
3241+
def test_is_synthetic_rejects_real_angle_names(self):
3242+
for name in ("<module>", "<lambda>", "<listcomp>", "<genexpr>"):
3243+
with self.subTest(name=name):
3244+
self.assertFalse(
3245+
is_synthetic(("app.py", (1, 1, 0, 0), name, None))
3246+
)
3247+
3248+
def test_is_synthetic_unknown_file_with_real_funcname(self):
3249+
self.assertFalse(
3250+
is_synthetic(("<unknown file>", None, "real_func", None))
3251+
)
3252+
3253+
def test_is_synthetic_frameinfo_object(self):
3254+
self.assertTrue(
3255+
is_synthetic(MockFrameInfo("app.py", 1, "<unknown function>"))
3256+
)
3257+
self.assertFalse(is_synthetic(MockFrameInfo("app.py", 1, "work")))

0 commit comments

Comments
 (0)