gh-138122: Don't sample partial frame chains (#141912) · python/cpython@d6d850d

GitHub

GitHub CopilotWrite better code with AI | MCP RegistryIntegrate external tools | ActionsAutomate any workflow | CodespacesInstant dev environments | IssuesPlan and track work | Code ReviewManage code changes | Code QualityEnforce quality at merge | Why GitHub | Marketplace | View all features | Enterprises | Small and medium teams | Startups | View all use cases | View all industries | View all solutions | AI | Software Development | DevOps | Security | View all topics | Customer stories | Events & webinars | Ebooks & reports | Business insights | Trust center | Partners | View all resources

@@ -1,7 +1,7 @@

1-importcontextlib

21importunittest

32importos

43importtextwrap

4+importcontextlib

55importimportlib

66importsys

77importsocket

@@ -216,33 +216,13 @@ def requires_subinterpreters(meth):

216216# Simple wrapper functions for RemoteUnwinder

217217# ============================================================================

218218219-# Errors that can occur transiently when reading process memory without synchronization

220-RETRIABLE_ERRORS= (

221-"Task list appears corrupted",

222-"Invalid linked list structure reading remote memory",

223-"Unknown error reading memory",

224-"Unhandled frame owner",

225-"Failed to parse initial frame",

226-"Failed to process frame chain",

227-"Failed to unwind stack",

228-)

229-230-231-def_is_retriable_error(exc):

232-"""Check if an exception is a transient error that should be retried."""

233-msg=str(exc)

234-returnany(msg.startswith(err) orerrinmsgforerrinRETRIABLE_ERRORS)

235-236-237219defget_stack_trace(pid):

238220for_inbusy_retry(SHORT_TIMEOUT):

239221try:

240222unwinder=RemoteUnwinder(pid, all_threads=True, debug=True)

241223returnunwinder.get_stack_trace()

242224exceptRuntimeErrorase:

243-if_is_retriable_error(e):

244-continue

245-raise

225+continue

246226raiseRuntimeError("Failed to get stack trace after retries")

247227248228@@ -252,9 +232,7 @@ def get_async_stack_trace(pid):

252232unwinder=RemoteUnwinder(pid, debug=True)

253233returnunwinder.get_async_stack_trace()

254234exceptRuntimeErrorase:

255-if_is_retriable_error(e):

256-continue

257-raise

235+continue

258236raiseRuntimeError("Failed to get async stack trace after retries")

259237260238@@ -264,9 +242,7 @@ def get_all_awaited_by(pid):

264242unwinder=RemoteUnwinder(pid, debug=True)

265243returnunwinder.get_all_awaited_by()

266244exceptRuntimeErrorase:

267-if_is_retriable_error(e):

268-continue

269-raise

245+continue

270246raiseRuntimeError("Failed to get all awaited_by after retries")

271247272248@@ -2268,18 +2244,13 @@ def make_unwinder(cache_frames=True):

22682244def_get_frames_with_retry(self, unwinder, required_funcs):

22692245"""Get frames containing required_funcs, with retry for transient errors."""

22702246for_inrange(MAX_TRIES):

2271-try:

2247+withcontextlib.suppress(OSError, RuntimeError):

22722248traces=unwinder.get_stack_trace()

22732249forinterpintraces:

22742250forthreadininterp.threads:

22752251funcs= {f.funcnameforfinthread.frame_info}

22762252ifrequired_funcs.issubset(funcs):

22772253returnthread.frame_info

2278-exceptRuntimeErrorase:

2279-if_is_retriable_error(e):

2280-pass

2281-else:

2282-raise

22832254time.sleep(0.1)

22842255returnNone

22852256@@ -2802,70 +2773,39 @@ def foo2():

28022773make_unwinder,

28032774 ):

28042775unwinder=make_unwinder(cache_frames=True)

2805-buffer=b""

2806-2807-defrecv_msg():

2808-"""Receive a single message from socket."""

2809-nonlocalbuffer

2810-whileb"\n"notinbuffer:

2811-chunk=client_socket.recv(256)

2812-ifnotchunk:

2813-returnNone

2814-buffer+=chunk

2815-msg, buffer=buffer.split(b"\n", 1)

2816-returnmsg

2817-2818-defget_thread_frames(target_funcs):

2819-"""Get frames for thread matching target functions."""

2820-retries=0

2821-for_inbusy_retry(SHORT_TIMEOUT):

2822-ifretries>=5:

2823-break

2824-retries+=1

2825-# On Windows, ReadProcessMemory can fail with OSError

2826-# (WinError 299) when frame pointers are in flux

2827-withcontextlib.suppress(RuntimeError, OSError):

2828-traces=unwinder.get_stack_trace()

2829-forinterpintraces:

2830-forthreadininterp.threads:

2831-funcs= [f.funcnameforfinthread.frame_info]

2832-ifany(finfuncsforfintarget_funcs):

2833-returnfuncs

2834-returnNone

2776+2777+# Message dispatch table: signal -> required functions for that thread

2778+dispatch= {

2779+b"t1:baz1": {"baz1", "bar1", "foo1"},

2780+b"t2:baz2": {"baz2", "bar2", "foo2"},

2781+b"t1:blech1": {"blech1", "foo1"},

2782+b"t2:blech2": {"blech2", "foo2"},

2783+ }

2835278428362785# Track results for each sync point

28372786results= {}

283827872839-# Process 4 sync points: baz1, baz2, blech1, blech2

2840-# With the lock, threads are serialized - handle one at a time

2841-for_inrange(4):

2842-msg=recv_msg()

2843-self.assertIsNotNone(msg, "Expected message from subprocess")

2844-2845-# Determine which thread/function and take snapshot

2846-ifmsg==b"t1:baz1":

2847-funcs=get_thread_frames(["baz1", "bar1", "foo1"])

2848-self.assertIsNotNone(funcs, "Thread 1 not found at baz1")

2849-results["t1:baz1"] =funcs

2850-elifmsg==b"t2:baz2":

2851-funcs=get_thread_frames(["baz2", "bar2", "foo2"])

2852-self.assertIsNotNone(funcs, "Thread 2 not found at baz2")

2853-results["t2:baz2"] =funcs

2854-elifmsg==b"t1:blech1":

2855-funcs=get_thread_frames(["blech1", "foo1"])

2856-self.assertIsNotNone(funcs, "Thread 1 not found at blech1")

2857-results["t1:blech1"] =funcs

2858-elifmsg==b"t2:blech2":

2859-funcs=get_thread_frames(["blech2", "foo2"])

2860-self.assertIsNotNone(funcs, "Thread 2 not found at blech2")

2861-results["t2:blech2"] =funcs

2862-2863-# Release thread to continue

2788+# Process 4 sync points (order depends on thread scheduling)

2789+buffer=_wait_for_signal(client_socket, b"\n")

2790+foriinrange(4):

2791+# Extract first message from buffer

2792+msg, sep, buffer=buffer.partition(b"\n")

2793+self.assertIn(msg, dispatch, f"Unexpected message: {msg!r}")

2794+2795+# Sample frames for the thread at this sync point

2796+required_funcs=dispatch[msg]

2797+frames=self._get_frames_with_retry(unwinder, required_funcs)

2798+self.assertIsNotNone(frames, f"Thread not found for {msg!r}")

2799+results[msg] = [f.funcnameforfinframes]

2800+2801+# Release thread and wait for next message (if not last)

28642802client_socket.sendall(b"k")

2803+ifi<3:

2804+buffer+=_wait_for_signal(client_socket, b"\n")

2865280528662806# Validate Phase 1: baz snapshots

2867-t1_baz=results.get("t1:baz1")

2868-t2_baz=results.get("t2:baz2")

2807+t1_baz=results.get(b"t1:baz1")

2808+t2_baz=results.get(b"t2:baz2")

28692809self.assertIsNotNone(t1_baz, "Missing t1:baz1 snapshot")

28702810self.assertIsNotNone(t2_baz, "Missing t2:baz2 snapshot")

28712811@@ -2890,8 +2830,8 @@ def get_thread_frames(target_funcs):

28902830self.assertNotIn("foo1", t2_baz)

2891283128922832# Validate Phase 2: blech snapshots (cache invalidation test)

2893-t1_blech=results.get("t1:blech1")

2894-t2_blech=results.get("t2:blech2")

2833+t1_blech=results.get(b"t1:blech1")

2834+t2_blech=results.get(b"t2:blech2")

28952835self.assertIsNotNone(t1_blech, "Missing t1:blech1 snapshot")

28962836self.assertIsNotNone(t2_blech, "Missing t2:blech2 snapshot")

28972837