mirror of
				https://github.com/python/cpython.git
				synced 2025-10-31 13:41:24 +00:00 
			
		
		
		
	gh-138385: Sample all interpreters in the tachyon profiler (#138398)
This commit is contained in:
		
							parent
							
								
									01895d233b
								
							
						
					
					
						commit
						03ee060ec8
					
				
					 7 changed files with 884 additions and 320 deletions
				
			
		|  | @ -9,3 +9,11 @@ def collect(self, stack_frames): | ||||||
|     @abstractmethod |     @abstractmethod | ||||||
|     def export(self, filename): |     def export(self, filename): | ||||||
|         """Export collected data to a file.""" |         """Export collected data to a file.""" | ||||||
|  | 
 | ||||||
|  |     def _iter_all_frames(self, stack_frames): | ||||||
|  |         """Iterate over all frame stacks from all interpreters and threads.""" | ||||||
|  |         for interpreter_info in stack_frames: | ||||||
|  |             for thread_info in interpreter_info.threads: | ||||||
|  |                 frames = thread_info.frame_info | ||||||
|  |                 if frames: | ||||||
|  |                     yield frames | ||||||
|  |  | ||||||
|  | @ -15,10 +15,10 @@ def __init__(self, sample_interval_usec): | ||||||
|             lambda: collections.defaultdict(int) |             lambda: collections.defaultdict(int) | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|     def collect(self, stack_frames): |     def _process_frames(self, frames): | ||||||
|         for thread_id, frames in stack_frames: |         """Process a single thread's frame stack.""" | ||||||
|         if not frames: |         if not frames: | ||||||
|                 continue |             return | ||||||
| 
 | 
 | ||||||
|         # Process each frame in the stack to track cumulative calls |         # Process each frame in the stack to track cumulative calls | ||||||
|         for frame in frames: |         for frame in frames: | ||||||
|  | @ -26,13 +26,7 @@ def collect(self, stack_frames): | ||||||
|             self.result[location]["cumulative_calls"] += 1 |             self.result[location]["cumulative_calls"] += 1 | ||||||
| 
 | 
 | ||||||
|         # The top frame gets counted as an inline call (directly executing) |         # The top frame gets counted as an inline call (directly executing) | ||||||
|             top_frame = frames[0] |         top_location = (frames[0].filename, frames[0].lineno, frames[0].funcname) | ||||||
|             top_location = ( |  | ||||||
|                 top_frame.filename, |  | ||||||
|                 top_frame.lineno, |  | ||||||
|                 top_frame.funcname, |  | ||||||
|             ) |  | ||||||
| 
 |  | ||||||
|         self.result[top_location]["direct_calls"] += 1 |         self.result[top_location]["direct_calls"] += 1 | ||||||
| 
 | 
 | ||||||
|         # Track caller-callee relationships for call graph |         # Track caller-callee relationships for call graph | ||||||
|  | @ -40,19 +34,15 @@ def collect(self, stack_frames): | ||||||
|             callee_frame = frames[i - 1] |             callee_frame = frames[i - 1] | ||||||
|             caller_frame = frames[i] |             caller_frame = frames[i] | ||||||
| 
 | 
 | ||||||
|                 callee = ( |             callee = (callee_frame.filename, callee_frame.lineno, callee_frame.funcname) | ||||||
|                     callee_frame.filename, |             caller = (caller_frame.filename, caller_frame.lineno, caller_frame.funcname) | ||||||
|                     callee_frame.lineno, |  | ||||||
|                     callee_frame.funcname, |  | ||||||
|                 ) |  | ||||||
|                 caller = ( |  | ||||||
|                     caller_frame.filename, |  | ||||||
|                     caller_frame.lineno, |  | ||||||
|                     caller_frame.funcname, |  | ||||||
|                 ) |  | ||||||
| 
 | 
 | ||||||
|             self.callers[callee][caller] += 1 |             self.callers[callee][caller] += 1 | ||||||
| 
 | 
 | ||||||
|  |     def collect(self, stack_frames): | ||||||
|  |         for frames in self._iter_all_frames(stack_frames): | ||||||
|  |             self._process_frames(frames) | ||||||
|  | 
 | ||||||
|     def export(self, filename): |     def export(self, filename): | ||||||
|         self.create_stats() |         self.create_stats() | ||||||
|         self._dump_stats(filename) |         self._dump_stats(filename) | ||||||
|  |  | ||||||
|  | @ -9,9 +9,11 @@ def __init__(self): | ||||||
|         self.call_trees = [] |         self.call_trees = [] | ||||||
|         self.function_samples = collections.defaultdict(int) |         self.function_samples = collections.defaultdict(int) | ||||||
| 
 | 
 | ||||||
|     def collect(self, stack_frames): |     def _process_frames(self, frames): | ||||||
|         for thread_id, frames in stack_frames: |         """Process a single thread's frame stack.""" | ||||||
|             if frames: |         if not frames: | ||||||
|  |             return | ||||||
|  | 
 | ||||||
|         # Store the complete call stack (reverse order - root first) |         # Store the complete call stack (reverse order - root first) | ||||||
|         call_tree = list(reversed(frames)) |         call_tree = list(reversed(frames)) | ||||||
|         self.call_trees.append(call_tree) |         self.call_trees.append(call_tree) | ||||||
|  | @ -20,6 +22,10 @@ def collect(self, stack_frames): | ||||||
|         for frame in frames: |         for frame in frames: | ||||||
|             self.function_samples[frame] += 1 |             self.function_samples[frame] += 1 | ||||||
| 
 | 
 | ||||||
|  |     def collect(self, stack_frames): | ||||||
|  |         for frames in self._iter_all_frames(stack_frames): | ||||||
|  |             self._process_frames(frames) | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| class CollapsedStackCollector(StackTraceCollector): | class CollapsedStackCollector(StackTraceCollector): | ||||||
|     def export(self, filename): |     def export(self, filename): | ||||||
|  |  | ||||||
|  | @ -19,6 +19,11 @@ | ||||||
| 
 | 
 | ||||||
| import subprocess | import subprocess | ||||||
| 
 | 
 | ||||||
|  | try: | ||||||
|  |     from concurrent import interpreters | ||||||
|  | except ImportError: | ||||||
|  |     interpreters = None | ||||||
|  | 
 | ||||||
| PROCESS_VM_READV_SUPPORTED = False | PROCESS_VM_READV_SUPPORTED = False | ||||||
| 
 | 
 | ||||||
| try: | try: | ||||||
|  | @ -47,6 +52,12 @@ def _make_test_script(script_dir, script_basename, source): | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | def requires_subinterpreters(meth): | ||||||
|  |     """Decorator to skip a test if subinterpreters are not supported.""" | ||||||
|  |     return unittest.skipIf(interpreters is None, | ||||||
|  |                            'subinterpreters required')(meth) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| def get_stack_trace(pid): | def get_stack_trace(pid): | ||||||
|     unwinder = RemoteUnwinder(pid, all_threads=True, debug=True) |     unwinder = RemoteUnwinder(pid, all_threads=True, debug=True) | ||||||
|     return unwinder.get_stack_trace() |     return unwinder.get_stack_trace() | ||||||
|  | @ -140,15 +151,27 @@ def foo(): | ||||||
|             ] |             ] | ||||||
|             # Is possible that there are more threads, so we check that the |             # Is possible that there are more threads, so we check that the | ||||||
|             # expected stack traces are in the result (looking at you Windows!) |             # expected stack traces are in the result (looking at you Windows!) | ||||||
|             self.assertIn((ANY, thread_expected_stack_trace), stack_trace) |             found_expected_stack = False | ||||||
|  |             for interpreter_info in stack_trace: | ||||||
|  |                 for thread_info in interpreter_info.threads: | ||||||
|  |                     if thread_info.frame_info == thread_expected_stack_trace: | ||||||
|  |                         found_expected_stack = True | ||||||
|  |                         break | ||||||
|  |                 if found_expected_stack: | ||||||
|  |                     break | ||||||
|  |             self.assertTrue(found_expected_stack, "Expected thread stack trace not found") | ||||||
| 
 | 
 | ||||||
|             # Check that the main thread stack trace is in the result |             # Check that the main thread stack trace is in the result | ||||||
|             frame = FrameInfo([script_name, 19, "<module>"]) |             frame = FrameInfo([script_name, 19, "<module>"]) | ||||||
|             for _, stack in stack_trace: |             main_thread_found = False | ||||||
|                 if frame in stack: |             for interpreter_info in stack_trace: | ||||||
|  |                 for thread_info in interpreter_info.threads: | ||||||
|  |                     if frame in thread_info.frame_info: | ||||||
|  |                         main_thread_found = True | ||||||
|                         break |                         break | ||||||
|             else: |                 if main_thread_found: | ||||||
|                 self.fail("Main thread stack trace not found in result") |                     break | ||||||
|  |             self.assertTrue(main_thread_found, "Main thread stack trace not found in result") | ||||||
| 
 | 
 | ||||||
|     @skip_if_not_supported |     @skip_if_not_supported | ||||||
|     @unittest.skipIf( |     @unittest.skipIf( | ||||||
|  | @ -1086,13 +1109,17 @@ def test_self_trace(self): | ||||||
|         # Is possible that there are more threads, so we check that the |         # Is possible that there are more threads, so we check that the | ||||||
|         # expected stack traces are in the result (looking at you Windows!) |         # expected stack traces are in the result (looking at you Windows!) | ||||||
|         this_tread_stack = None |         this_tread_stack = None | ||||||
|         for thread_id, stack in stack_trace: |         # New format: [InterpreterInfo(interpreter_id, [ThreadInfo(...)])] | ||||||
|             if thread_id == threading.get_native_id(): |         for interpreter_info in stack_trace: | ||||||
|                 this_tread_stack = stack |             for thread_info in interpreter_info.threads: | ||||||
|  |                 if thread_info.thread_id == threading.get_native_id(): | ||||||
|  |                     this_tread_stack = thread_info.frame_info | ||||||
|  |                     break | ||||||
|  |             if this_tread_stack: | ||||||
|                 break |                 break | ||||||
|         self.assertIsNotNone(this_tread_stack) |         self.assertIsNotNone(this_tread_stack) | ||||||
|         self.assertEqual( |         self.assertEqual( | ||||||
|             stack[:2], |             this_tread_stack[:2], | ||||||
|             [ |             [ | ||||||
|                 FrameInfo( |                 FrameInfo( | ||||||
|                     [ |                     [ | ||||||
|  | @ -1111,6 +1138,360 @@ def test_self_trace(self): | ||||||
|             ], |             ], | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|  |     @skip_if_not_supported | ||||||
|  |     @unittest.skipIf( | ||||||
|  |         sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED, | ||||||
|  |         "Test only runs on Linux with process_vm_readv support", | ||||||
|  |     ) | ||||||
|  |     @requires_subinterpreters | ||||||
|  |     def test_subinterpreter_stack_trace(self): | ||||||
|  |         # Test that subinterpreters are correctly handled | ||||||
|  |         port = find_unused_port() | ||||||
|  | 
 | ||||||
|  |         # Calculate subinterpreter code separately and pickle it to avoid f-string issues | ||||||
|  |         import pickle | ||||||
|  |         subinterp_code = textwrap.dedent(f''' | ||||||
|  |             import socket | ||||||
|  |             import time | ||||||
|  | 
 | ||||||
|  |             def sub_worker(): | ||||||
|  |                 def nested_func(): | ||||||
|  |                     sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | ||||||
|  |                     sock.connect(('localhost', {port})) | ||||||
|  |                     sock.sendall(b"ready:sub\\n") | ||||||
|  |                     time.sleep(10_000) | ||||||
|  |                 nested_func() | ||||||
|  | 
 | ||||||
|  |             sub_worker() | ||||||
|  |         ''').strip() | ||||||
|  | 
 | ||||||
|  |         # Pickle the subinterpreter code | ||||||
|  |         pickled_code = pickle.dumps(subinterp_code) | ||||||
|  | 
 | ||||||
|  |         script = textwrap.dedent( | ||||||
|  |             f""" | ||||||
|  |             from concurrent import interpreters | ||||||
|  |             import time | ||||||
|  |             import sys | ||||||
|  |             import socket | ||||||
|  |             import threading | ||||||
|  | 
 | ||||||
|  |             # Connect to the test process | ||||||
|  |             sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | ||||||
|  |             sock.connect(('localhost', {port})) | ||||||
|  | 
 | ||||||
|  |             def main_worker(): | ||||||
|  |                 # Function running in main interpreter | ||||||
|  |                 sock.sendall(b"ready:main\\n") | ||||||
|  |                 time.sleep(10_000) | ||||||
|  | 
 | ||||||
|  |             def run_subinterp(): | ||||||
|  |                 # Create and run subinterpreter | ||||||
|  |                 subinterp = interpreters.create() | ||||||
|  | 
 | ||||||
|  |                 import pickle | ||||||
|  |                 pickled_code = {pickled_code!r} | ||||||
|  |                 subinterp_code = pickle.loads(pickled_code) | ||||||
|  |                 subinterp.exec(subinterp_code) | ||||||
|  | 
 | ||||||
|  |             # Start subinterpreter in thread | ||||||
|  |             sub_thread = threading.Thread(target=run_subinterp) | ||||||
|  |             sub_thread.start() | ||||||
|  | 
 | ||||||
|  |             # Start main thread work | ||||||
|  |             main_thread = threading.Thread(target=main_worker) | ||||||
|  |             main_thread.start() | ||||||
|  | 
 | ||||||
|  |             # Keep main thread alive | ||||||
|  |             main_thread.join() | ||||||
|  |             sub_thread.join() | ||||||
|  |             """ | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         with os_helper.temp_dir() as work_dir: | ||||||
|  |             script_dir = os.path.join(work_dir, "script_pkg") | ||||||
|  |             os.mkdir(script_dir) | ||||||
|  | 
 | ||||||
|  |             # Create a socket server to communicate with the target process | ||||||
|  |             server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | ||||||
|  |             server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) | ||||||
|  |             server_socket.bind(("localhost", port)) | ||||||
|  |             server_socket.settimeout(SHORT_TIMEOUT) | ||||||
|  |             server_socket.listen(1) | ||||||
|  | 
 | ||||||
|  |             script_name = _make_test_script(script_dir, "script", script) | ||||||
|  |             client_sockets = [] | ||||||
|  |             try: | ||||||
|  |                 p = subprocess.Popen([sys.executable, script_name]) | ||||||
|  | 
 | ||||||
|  |                 # Accept connections from both main and subinterpreter | ||||||
|  |                 responses = set() | ||||||
|  |                 while len(responses) < 2:  # Wait for both "ready:main" and "ready:sub" | ||||||
|  |                     try: | ||||||
|  |                         client_socket, _ = server_socket.accept() | ||||||
|  |                         client_sockets.append(client_socket) | ||||||
|  | 
 | ||||||
|  |                         # Read the response from this connection | ||||||
|  |                         response = client_socket.recv(1024) | ||||||
|  |                         if b"ready:main" in response: | ||||||
|  |                             responses.add("main") | ||||||
|  |                         if b"ready:sub" in response: | ||||||
|  |                             responses.add("sub") | ||||||
|  |                     except socket.timeout: | ||||||
|  |                         break | ||||||
|  | 
 | ||||||
|  |                 server_socket.close() | ||||||
|  |                 stack_trace = get_stack_trace(p.pid) | ||||||
|  |             except PermissionError: | ||||||
|  |                 self.skipTest( | ||||||
|  |                     "Insufficient permissions to read the stack trace" | ||||||
|  |                 ) | ||||||
|  |             finally: | ||||||
|  |                 for client_socket in client_sockets: | ||||||
|  |                     if client_socket is not None: | ||||||
|  |                         client_socket.close() | ||||||
|  |                 p.kill() | ||||||
|  |                 p.terminate() | ||||||
|  |                 p.wait(timeout=SHORT_TIMEOUT) | ||||||
|  | 
 | ||||||
|  |             # Verify we have multiple interpreters | ||||||
|  |             self.assertGreaterEqual(len(stack_trace), 1, "Should have at least one interpreter") | ||||||
|  | 
 | ||||||
|  |             # Look for main interpreter (ID 0) and subinterpreter (ID > 0) | ||||||
|  |             main_interp = None | ||||||
|  |             sub_interp = None | ||||||
|  | 
 | ||||||
|  |             for interpreter_info in stack_trace: | ||||||
|  |                 if interpreter_info.interpreter_id == 0: | ||||||
|  |                     main_interp = interpreter_info | ||||||
|  |                 elif interpreter_info.interpreter_id > 0: | ||||||
|  |                     sub_interp = interpreter_info | ||||||
|  | 
 | ||||||
|  |             self.assertIsNotNone(main_interp, "Main interpreter should be present") | ||||||
|  | 
 | ||||||
|  |             # Check main interpreter has expected stack trace | ||||||
|  |             main_found = False | ||||||
|  |             for thread_info in main_interp.threads: | ||||||
|  |                 for frame in thread_info.frame_info: | ||||||
|  |                     if frame.funcname == "main_worker": | ||||||
|  |                         main_found = True | ||||||
|  |                         break | ||||||
|  |                 if main_found: | ||||||
|  |                     break | ||||||
|  |             self.assertTrue(main_found, "Main interpreter should have main_worker in stack") | ||||||
|  | 
 | ||||||
|  |             # If subinterpreter is present, check its stack trace | ||||||
|  |             if sub_interp: | ||||||
|  |                 sub_found = False | ||||||
|  |                 for thread_info in sub_interp.threads: | ||||||
|  |                     for frame in thread_info.frame_info: | ||||||
|  |                         if frame.funcname in ("sub_worker", "nested_func"): | ||||||
|  |                             sub_found = True | ||||||
|  |                             break | ||||||
|  |                     if sub_found: | ||||||
|  |                         break | ||||||
|  |                 self.assertTrue(sub_found, "Subinterpreter should have sub_worker or nested_func in stack") | ||||||
|  | 
 | ||||||
|  |     @skip_if_not_supported | ||||||
|  |     @unittest.skipIf( | ||||||
|  |         sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED, | ||||||
|  |         "Test only runs on Linux with process_vm_readv support", | ||||||
|  |     ) | ||||||
|  |     @requires_subinterpreters | ||||||
|  |     def test_multiple_subinterpreters_with_threads(self): | ||||||
|  |         # Test multiple subinterpreters, each with multiple threads | ||||||
|  |         port = find_unused_port() | ||||||
|  | 
 | ||||||
|  |         # Calculate subinterpreter codes separately and pickle them | ||||||
|  |         import pickle | ||||||
|  | 
 | ||||||
|  |         # Code for first subinterpreter with 2 threads | ||||||
|  |         subinterp1_code = textwrap.dedent(f''' | ||||||
|  |             import socket | ||||||
|  |             import time | ||||||
|  |             import threading | ||||||
|  | 
 | ||||||
|  |             def worker1(): | ||||||
|  |                 def nested_func(): | ||||||
|  |                     sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | ||||||
|  |                     sock.connect(('localhost', {port})) | ||||||
|  |                     sock.sendall(b"ready:sub1-t1\\n") | ||||||
|  |                     time.sleep(10_000) | ||||||
|  |                 nested_func() | ||||||
|  | 
 | ||||||
|  |             def worker2(): | ||||||
|  |                 def nested_func(): | ||||||
|  |                     sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | ||||||
|  |                     sock.connect(('localhost', {port})) | ||||||
|  |                     sock.sendall(b"ready:sub1-t2\\n") | ||||||
|  |                     time.sleep(10_000) | ||||||
|  |                 nested_func() | ||||||
|  | 
 | ||||||
|  |             t1 = threading.Thread(target=worker1) | ||||||
|  |             t2 = threading.Thread(target=worker2) | ||||||
|  |             t1.start() | ||||||
|  |             t2.start() | ||||||
|  |             t1.join() | ||||||
|  |             t2.join() | ||||||
|  |         ''').strip() | ||||||
|  | 
 | ||||||
|  |         # Code for second subinterpreter with 2 threads | ||||||
|  |         subinterp2_code = textwrap.dedent(f''' | ||||||
|  |             import socket | ||||||
|  |             import time | ||||||
|  |             import threading | ||||||
|  | 
 | ||||||
|  |             def worker1(): | ||||||
|  |                 def nested_func(): | ||||||
|  |                     sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | ||||||
|  |                     sock.connect(('localhost', {port})) | ||||||
|  |                     sock.sendall(b"ready:sub2-t1\\n") | ||||||
|  |                     time.sleep(10_000) | ||||||
|  |                 nested_func() | ||||||
|  | 
 | ||||||
|  |             def worker2(): | ||||||
|  |                 def nested_func(): | ||||||
|  |                     sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | ||||||
|  |                     sock.connect(('localhost', {port})) | ||||||
|  |                     sock.sendall(b"ready:sub2-t2\\n") | ||||||
|  |                     time.sleep(10_000) | ||||||
|  |                 nested_func() | ||||||
|  | 
 | ||||||
|  |             t1 = threading.Thread(target=worker1) | ||||||
|  |             t2 = threading.Thread(target=worker2) | ||||||
|  |             t1.start() | ||||||
|  |             t2.start() | ||||||
|  |             t1.join() | ||||||
|  |             t2.join() | ||||||
|  |         ''').strip() | ||||||
|  | 
 | ||||||
|  |         # Pickle the subinterpreter codes | ||||||
|  |         pickled_code1 = pickle.dumps(subinterp1_code) | ||||||
|  |         pickled_code2 = pickle.dumps(subinterp2_code) | ||||||
|  | 
 | ||||||
|  |         script = textwrap.dedent( | ||||||
|  |             f""" | ||||||
|  |             from concurrent import interpreters | ||||||
|  |             import time | ||||||
|  |             import sys | ||||||
|  |             import socket | ||||||
|  |             import threading | ||||||
|  | 
 | ||||||
|  |             # Connect to the test process | ||||||
|  |             sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | ||||||
|  |             sock.connect(('localhost', {port})) | ||||||
|  | 
 | ||||||
|  |             def main_worker(): | ||||||
|  |                 # Function running in main interpreter | ||||||
|  |                 sock.sendall(b"ready:main\\n") | ||||||
|  |                 time.sleep(10_000) | ||||||
|  | 
 | ||||||
|  |             def run_subinterp1(): | ||||||
|  |                 # Create and run first subinterpreter | ||||||
|  |                 subinterp = interpreters.create() | ||||||
|  | 
 | ||||||
|  |                 import pickle | ||||||
|  |                 pickled_code = {pickled_code1!r} | ||||||
|  |                 subinterp_code = pickle.loads(pickled_code) | ||||||
|  |                 subinterp.exec(subinterp_code) | ||||||
|  | 
 | ||||||
|  |             def run_subinterp2(): | ||||||
|  |                 # Create and run second subinterpreter | ||||||
|  |                 subinterp = interpreters.create() | ||||||
|  | 
 | ||||||
|  |                 import pickle | ||||||
|  |                 pickled_code = {pickled_code2!r} | ||||||
|  |                 subinterp_code = pickle.loads(pickled_code) | ||||||
|  |                 subinterp.exec(subinterp_code) | ||||||
|  | 
 | ||||||
|  |             # Start subinterpreters in threads | ||||||
|  |             sub1_thread = threading.Thread(target=run_subinterp1) | ||||||
|  |             sub2_thread = threading.Thread(target=run_subinterp2) | ||||||
|  |             sub1_thread.start() | ||||||
|  |             sub2_thread.start() | ||||||
|  | 
 | ||||||
|  |             # Start main thread work | ||||||
|  |             main_thread = threading.Thread(target=main_worker) | ||||||
|  |             main_thread.start() | ||||||
|  | 
 | ||||||
|  |             # Keep main thread alive | ||||||
|  |             main_thread.join() | ||||||
|  |             sub1_thread.join() | ||||||
|  |             sub2_thread.join() | ||||||
|  |             """ | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         with os_helper.temp_dir() as work_dir: | ||||||
|  |             script_dir = os.path.join(work_dir, "script_pkg") | ||||||
|  |             os.mkdir(script_dir) | ||||||
|  | 
 | ||||||
|  |             # Create a socket server to communicate with the target process | ||||||
|  |             server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | ||||||
|  |             server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) | ||||||
|  |             server_socket.bind(("localhost", port)) | ||||||
|  |             server_socket.settimeout(SHORT_TIMEOUT) | ||||||
|  |             server_socket.listen(5)  # Allow multiple connections | ||||||
|  | 
 | ||||||
|  |             script_name = _make_test_script(script_dir, "script", script) | ||||||
|  |             client_sockets = [] | ||||||
|  |             try: | ||||||
|  |                 p = subprocess.Popen([sys.executable, script_name]) | ||||||
|  | 
 | ||||||
|  |                 # Accept connections from main and all subinterpreter threads | ||||||
|  |                 expected_responses = {"ready:main", "ready:sub1-t1", "ready:sub1-t2", "ready:sub2-t1", "ready:sub2-t2"} | ||||||
|  |                 responses = set() | ||||||
|  | 
 | ||||||
|  |                 while len(responses) < 5:  # Wait for all 5 ready signals | ||||||
|  |                     try: | ||||||
|  |                         client_socket, _ = server_socket.accept() | ||||||
|  |                         client_sockets.append(client_socket) | ||||||
|  | 
 | ||||||
|  |                         # Read the response from this connection | ||||||
|  |                         response = client_socket.recv(1024) | ||||||
|  |                         response_str = response.decode().strip() | ||||||
|  |                         if response_str in expected_responses: | ||||||
|  |                             responses.add(response_str) | ||||||
|  |                     except socket.timeout: | ||||||
|  |                         break | ||||||
|  | 
 | ||||||
|  |                 server_socket.close() | ||||||
|  |                 stack_trace = get_stack_trace(p.pid) | ||||||
|  |             except PermissionError: | ||||||
|  |                 self.skipTest( | ||||||
|  |                     "Insufficient permissions to read the stack trace" | ||||||
|  |                 ) | ||||||
|  |             finally: | ||||||
|  |                 for client_socket in client_sockets: | ||||||
|  |                     if client_socket is not None: | ||||||
|  |                         client_socket.close() | ||||||
|  |                 p.kill() | ||||||
|  |                 p.terminate() | ||||||
|  |                 p.wait(timeout=SHORT_TIMEOUT) | ||||||
|  | 
 | ||||||
|  |             # Verify we have multiple interpreters | ||||||
|  |             self.assertGreaterEqual(len(stack_trace), 2, "Should have at least two interpreters") | ||||||
|  | 
 | ||||||
|  |             # Count interpreters by ID | ||||||
|  |             interpreter_ids = {interp.interpreter_id for interp in stack_trace} | ||||||
|  |             self.assertIn(0, interpreter_ids, "Main interpreter should be present") | ||||||
|  |             self.assertGreaterEqual(len(interpreter_ids), 3, "Should have main + at least 2 subinterpreters") | ||||||
|  | 
 | ||||||
|  |             # Count total threads across all interpreters | ||||||
|  |             total_threads = sum(len(interp.threads) for interp in stack_trace) | ||||||
|  |             self.assertGreaterEqual(total_threads, 5, "Should have at least 5 threads total") | ||||||
|  | 
 | ||||||
|  |             # Look for expected function names in stack traces | ||||||
|  |             all_funcnames = set() | ||||||
|  |             for interpreter_info in stack_trace: | ||||||
|  |                 for thread_info in interpreter_info.threads: | ||||||
|  |                     for frame in thread_info.frame_info: | ||||||
|  |                         all_funcnames.add(frame.funcname) | ||||||
|  | 
 | ||||||
|  |             # Should find functions from different interpreters and threads | ||||||
|  |             expected_funcs = {"main_worker", "worker1", "worker2", "nested_func"} | ||||||
|  |             found_funcs = expected_funcs.intersection(all_funcnames) | ||||||
|  |             self.assertGreater(len(found_funcs), 0, f"Should find some expected functions, got: {all_funcnames}") | ||||||
|  | 
 | ||||||
|     @skip_if_not_supported |     @skip_if_not_supported | ||||||
|     @unittest.skipIf( |     @unittest.skipIf( | ||||||
|         sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED, |         sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED, | ||||||
|  | @ -1203,15 +1584,20 @@ def main_work(): | ||||||
|                     # Wait for the main thread to start its busy work |                     # Wait for the main thread to start its busy work | ||||||
|                     all_traces = unwinder_all.get_stack_trace() |                     all_traces = unwinder_all.get_stack_trace() | ||||||
|                     found = False |                     found = False | ||||||
|                     for thread_id, stack in all_traces: |                     # New format: [InterpreterInfo(interpreter_id, [ThreadInfo(...)])] | ||||||
|                         if not stack: |                     for interpreter_info in all_traces: | ||||||
|  |                         for thread_info in interpreter_info.threads: | ||||||
|  |                             if not thread_info.frame_info: | ||||||
|                                 continue |                                 continue | ||||||
|                         current_frame = stack[0] |                             current_frame = thread_info.frame_info[0] | ||||||
|                             if ( |                             if ( | ||||||
|                                 current_frame.funcname == "main_work" |                                 current_frame.funcname == "main_work" | ||||||
|                                 and current_frame.lineno > 15 |                                 and current_frame.lineno > 15 | ||||||
|                             ): |                             ): | ||||||
|                                 found = True |                                 found = True | ||||||
|  |                                 break | ||||||
|  |                         if found: | ||||||
|  |                             break | ||||||
| 
 | 
 | ||||||
|                     if found: |                     if found: | ||||||
|                         break |                         break | ||||||
|  | @ -1237,19 +1623,31 @@ def main_work(): | ||||||
|                 p.terminate() |                 p.terminate() | ||||||
|                 p.wait(timeout=SHORT_TIMEOUT) |                 p.wait(timeout=SHORT_TIMEOUT) | ||||||
| 
 | 
 | ||||||
|             # Verify we got multiple threads in all_traces |             # Count total threads across all interpreters in all_traces | ||||||
|  |             total_threads = sum(len(interpreter_info.threads) for interpreter_info in all_traces) | ||||||
|             self.assertGreater( |             self.assertGreater( | ||||||
|                 len(all_traces), 1, "Should have multiple threads" |                 total_threads, 1, "Should have multiple threads" | ||||||
|             ) |             ) | ||||||
| 
 | 
 | ||||||
|             # Verify we got exactly one thread in gil_traces |             # Count total threads across all interpreters in gil_traces | ||||||
|  |             total_gil_threads = sum(len(interpreter_info.threads) for interpreter_info in gil_traces) | ||||||
|             self.assertEqual( |             self.assertEqual( | ||||||
|                 len(gil_traces), 1, "Should have exactly one GIL holder" |                 total_gil_threads, 1, "Should have exactly one GIL holder" | ||||||
|             ) |             ) | ||||||
| 
 | 
 | ||||||
|             # The GIL holder should be in the all_traces list |             # Get the GIL holder thread ID | ||||||
|             gil_thread_id = gil_traces[0][0] |             gil_thread_id = None | ||||||
|             all_thread_ids = [trace[0] for trace in all_traces] |             for interpreter_info in gil_traces: | ||||||
|  |                 if interpreter_info.threads: | ||||||
|  |                     gil_thread_id = interpreter_info.threads[0].thread_id | ||||||
|  |                     break | ||||||
|  | 
 | ||||||
|  |             # Get all thread IDs from all_traces | ||||||
|  |             all_thread_ids = [] | ||||||
|  |             for interpreter_info in all_traces: | ||||||
|  |                 for thread_info in interpreter_info.threads: | ||||||
|  |                     all_thread_ids.append(thread_info.thread_id) | ||||||
|  | 
 | ||||||
|             self.assertIn( |             self.assertIn( | ||||||
|                 gil_thread_id, |                 gil_thread_id, | ||||||
|                 all_thread_ids, |                 all_thread_ids, | ||||||
|  |  | ||||||
|  | @ -49,6 +49,28 @@ def __repr__(self): | ||||||
|         return f"MockFrameInfo(filename='{self.filename}', lineno={self.lineno}, funcname='{self.funcname}')" |         return f"MockFrameInfo(filename='{self.filename}', lineno={self.lineno}, funcname='{self.funcname}')" | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | class MockThreadInfo: | ||||||
|  |     """Mock ThreadInfo for testing since the real one isn't accessible.""" | ||||||
|  | 
 | ||||||
|  |     def __init__(self, thread_id, frame_info): | ||||||
|  |         self.thread_id = thread_id | ||||||
|  |         self.frame_info = frame_info | ||||||
|  | 
 | ||||||
|  |     def __repr__(self): | ||||||
|  |         return f"MockThreadInfo(thread_id={self.thread_id}, frame_info={self.frame_info})" | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class MockInterpreterInfo: | ||||||
|  |     """Mock InterpreterInfo for testing since the real one isn't accessible.""" | ||||||
|  | 
 | ||||||
|  |     def __init__(self, interpreter_id, threads): | ||||||
|  |         self.interpreter_id = interpreter_id | ||||||
|  |         self.threads = threads | ||||||
|  | 
 | ||||||
|  |     def __repr__(self): | ||||||
|  |         return f"MockInterpreterInfo(interpreter_id={self.interpreter_id}, threads={self.threads})" | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| skip_if_not_supported = unittest.skipIf( | skip_if_not_supported = unittest.skipIf( | ||||||
|     ( |     ( | ||||||
|         sys.platform != "darwin" |         sys.platform != "darwin" | ||||||
|  | @ -152,19 +174,22 @@ def test_pstats_collector_with_extreme_intervals_and_empty_data(self): | ||||||
|         self.assertEqual(len(collector.result), 0) |         self.assertEqual(len(collector.result), 0) | ||||||
| 
 | 
 | ||||||
|         # Test collecting frames with None thread id |         # Test collecting frames with None thread id | ||||||
|         test_frames = [(None, [MockFrameInfo("file.py", 10, "func")])] |         test_frames = [MockInterpreterInfo(0, [MockThreadInfo(None, [MockFrameInfo("file.py", 10, "func")])])] | ||||||
|         collector.collect(test_frames) |         collector.collect(test_frames) | ||||||
|         # Should still process the frames |         # Should still process the frames | ||||||
|         self.assertEqual(len(collector.result), 1) |         self.assertEqual(len(collector.result), 1) | ||||||
| 
 | 
 | ||||||
|         # Test collecting duplicate frames in same sample |         # Test collecting duplicate frames in same sample | ||||||
|         test_frames = [ |         test_frames = [ | ||||||
|             ( |             MockInterpreterInfo( | ||||||
|  |                 0,  # interpreter_id | ||||||
|  |                 [MockThreadInfo( | ||||||
|                     1, |                     1, | ||||||
|                     [ |                     [ | ||||||
|                         MockFrameInfo("file.py", 10, "func1"), |                         MockFrameInfo("file.py", 10, "func1"), | ||||||
|                         MockFrameInfo("file.py", 10, "func1"),  # Duplicate |                         MockFrameInfo("file.py", 10, "func1"),  # Duplicate | ||||||
|                     ], |                     ], | ||||||
|  |                 )] | ||||||
|             ) |             ) | ||||||
|         ] |         ] | ||||||
|         collector = PstatsCollector(sample_interval_usec=1000) |         collector = PstatsCollector(sample_interval_usec=1000) | ||||||
|  | @ -179,7 +204,7 @@ def test_pstats_collector_single_frame_stacks(self): | ||||||
|         collector = PstatsCollector(sample_interval_usec=1000) |         collector = PstatsCollector(sample_interval_usec=1000) | ||||||
| 
 | 
 | ||||||
|         # Test with exactly one frame (should trigger the <= 1 condition) |         # Test with exactly one frame (should trigger the <= 1 condition) | ||||||
|         single_frame = [(1, [MockFrameInfo("single.py", 10, "single_func")])] |         single_frame = [MockInterpreterInfo(0, [MockThreadInfo(1, [MockFrameInfo("single.py", 10, "single_func")])])] | ||||||
|         collector.collect(single_frame) |         collector.collect(single_frame) | ||||||
| 
 | 
 | ||||||
|         # Should record the single frame with inline call |         # Should record the single frame with inline call | ||||||
|  | @ -190,7 +215,7 @@ def test_pstats_collector_single_frame_stacks(self): | ||||||
|         self.assertEqual(collector.result[single_key]["cumulative_calls"], 1) |         self.assertEqual(collector.result[single_key]["cumulative_calls"], 1) | ||||||
| 
 | 
 | ||||||
|         # Test with empty frames (should also trigger <= 1 condition) |         # Test with empty frames (should also trigger <= 1 condition) | ||||||
|         empty_frames = [(1, [])] |         empty_frames = [MockInterpreterInfo(0, [MockThreadInfo(1, [])])] | ||||||
|         collector.collect(empty_frames) |         collector.collect(empty_frames) | ||||||
| 
 | 
 | ||||||
|         # Should not add any new entries |         # Should not add any new entries | ||||||
|  | @ -200,11 +225,14 @@ def test_pstats_collector_single_frame_stacks(self): | ||||||
| 
 | 
 | ||||||
|         # Test mixed single and multi-frame stacks |         # Test mixed single and multi-frame stacks | ||||||
|         mixed_frames = [ |         mixed_frames = [ | ||||||
|             ( |             MockInterpreterInfo( | ||||||
|  |                 0, | ||||||
|  |                 [ | ||||||
|  |                     MockThreadInfo( | ||||||
|                         1, |                         1, | ||||||
|                         [MockFrameInfo("single2.py", 20, "single_func2")], |                         [MockFrameInfo("single2.py", 20, "single_func2")], | ||||||
|                     ),  # Single frame |                     ),  # Single frame | ||||||
|             ( |                     MockThreadInfo( | ||||||
|                         2, |                         2, | ||||||
|                         [  # Multi-frame stack |                         [  # Multi-frame stack | ||||||
|                             MockFrameInfo("multi.py", 30, "multi_func1"), |                             MockFrameInfo("multi.py", 30, "multi_func1"), | ||||||
|  | @ -212,6 +240,8 @@ def test_pstats_collector_single_frame_stacks(self): | ||||||
|                         ], |                         ], | ||||||
|                     ), |                     ), | ||||||
|                 ] |                 ] | ||||||
|  |             ), | ||||||
|  |         ] | ||||||
|         collector.collect(mixed_frames) |         collector.collect(mixed_frames) | ||||||
| 
 | 
 | ||||||
|         # Should have recorded all functions |         # Should have recorded all functions | ||||||
|  | @ -244,14 +274,14 @@ def test_collapsed_stack_collector_with_empty_and_deep_stacks(self): | ||||||
|         self.assertEqual(len(collector.call_trees), 0) |         self.assertEqual(len(collector.call_trees), 0) | ||||||
| 
 | 
 | ||||||
|         # Test with single frame stack |         # Test with single frame stack | ||||||
|         test_frames = [(1, [("file.py", 10, "func")])] |         test_frames = [MockInterpreterInfo(0, [MockThreadInfo(1, [("file.py", 10, "func")])])] | ||||||
|         collector.collect(test_frames) |         collector.collect(test_frames) | ||||||
|         self.assertEqual(len(collector.call_trees), 1) |         self.assertEqual(len(collector.call_trees), 1) | ||||||
|         self.assertEqual(collector.call_trees[0], [("file.py", 10, "func")]) |         self.assertEqual(collector.call_trees[0], [("file.py", 10, "func")]) | ||||||
| 
 | 
 | ||||||
|         # Test with very deep stack |         # Test with very deep stack | ||||||
|         deep_stack = [(f"file{i}.py", i, f"func{i}") for i in range(100)] |         deep_stack = [(f"file{i}.py", i, f"func{i}") for i in range(100)] | ||||||
|         test_frames = [(1, deep_stack)] |         test_frames = [MockInterpreterInfo(0, [MockThreadInfo(1, deep_stack)])] | ||||||
|         collector = CollapsedStackCollector() |         collector = CollapsedStackCollector() | ||||||
|         collector.collect(test_frames) |         collector.collect(test_frames) | ||||||
|         self.assertEqual(len(collector.call_trees[0]), 100) |         self.assertEqual(len(collector.call_trees[0]), 100) | ||||||
|  | @ -271,12 +301,15 @@ def test_pstats_collector_basic(self): | ||||||
| 
 | 
 | ||||||
|         # Test collecting sample data |         # Test collecting sample data | ||||||
|         test_frames = [ |         test_frames = [ | ||||||
|             ( |             MockInterpreterInfo( | ||||||
|  |                 0, | ||||||
|  |                 [MockThreadInfo( | ||||||
|                     1, |                     1, | ||||||
|                     [ |                     [ | ||||||
|                         MockFrameInfo("file.py", 10, "func1"), |                         MockFrameInfo("file.py", 10, "func1"), | ||||||
|                         MockFrameInfo("file.py", 20, "func2"), |                         MockFrameInfo("file.py", 20, "func2"), | ||||||
|                     ], |                     ], | ||||||
|  |                 )] | ||||||
|             ) |             ) | ||||||
|         ] |         ] | ||||||
|         collector.collect(test_frames) |         collector.collect(test_frames) | ||||||
|  | @ -309,12 +342,15 @@ def test_pstats_collector_create_stats(self): | ||||||
|         )  # 1 second intervals |         )  # 1 second intervals | ||||||
| 
 | 
 | ||||||
|         test_frames = [ |         test_frames = [ | ||||||
|             ( |             MockInterpreterInfo( | ||||||
|  |                 0, | ||||||
|  |                 [MockThreadInfo( | ||||||
|                     1, |                     1, | ||||||
|                     [ |                     [ | ||||||
|                         MockFrameInfo("file.py", 10, "func1"), |                         MockFrameInfo("file.py", 10, "func1"), | ||||||
|                         MockFrameInfo("file.py", 20, "func2"), |                         MockFrameInfo("file.py", 20, "func2"), | ||||||
|                     ], |                     ], | ||||||
|  |                 )] | ||||||
|             ) |             ) | ||||||
|         ] |         ] | ||||||
|         collector.collect(test_frames) |         collector.collect(test_frames) | ||||||
|  | @ -350,7 +386,7 @@ def test_collapsed_stack_collector_basic(self): | ||||||
| 
 | 
 | ||||||
|         # Test collecting sample data |         # Test collecting sample data | ||||||
|         test_frames = [ |         test_frames = [ | ||||||
|             (1, [("file.py", 10, "func1"), ("file.py", 20, "func2")]) |             MockInterpreterInfo(0, [MockThreadInfo(1, [("file.py", 10, "func1"), ("file.py", 20, "func2")])]) | ||||||
|         ] |         ] | ||||||
|         collector.collect(test_frames) |         collector.collect(test_frames) | ||||||
| 
 | 
 | ||||||
|  | @ -374,12 +410,12 @@ def test_collapsed_stack_collector_export(self): | ||||||
|         collector = CollapsedStackCollector() |         collector = CollapsedStackCollector() | ||||||
| 
 | 
 | ||||||
|         test_frames1 = [ |         test_frames1 = [ | ||||||
|             (1, [("file.py", 10, "func1"), ("file.py", 20, "func2")]) |             MockInterpreterInfo(0, [MockThreadInfo(1, [("file.py", 10, "func1"), ("file.py", 20, "func2")])]) | ||||||
|         ] |         ] | ||||||
|         test_frames2 = [ |         test_frames2 = [ | ||||||
|             (1, [("file.py", 10, "func1"), ("file.py", 20, "func2")]) |             MockInterpreterInfo(0, [MockThreadInfo(1, [("file.py", 10, "func1"), ("file.py", 20, "func2")])]) | ||||||
|         ]  # Same stack |         ]  # Same stack | ||||||
|         test_frames3 = [(1, [("other.py", 5, "other_func")])] |         test_frames3 = [MockInterpreterInfo(0, [MockThreadInfo(1, [("other.py", 5, "other_func")])])] | ||||||
| 
 | 
 | ||||||
|         collector.collect(test_frames1) |         collector.collect(test_frames1) | ||||||
|         collector.collect(test_frames2) |         collector.collect(test_frames2) | ||||||
|  | @ -406,24 +442,30 @@ def test_pstats_collector_export(self): | ||||||
|         )  # 1 second intervals |         )  # 1 second intervals | ||||||
| 
 | 
 | ||||||
|         test_frames1 = [ |         test_frames1 = [ | ||||||
|             ( |             MockInterpreterInfo( | ||||||
|  |                 0, | ||||||
|  |                 [MockThreadInfo( | ||||||
|                     1, |                     1, | ||||||
|                     [ |                     [ | ||||||
|                         MockFrameInfo("file.py", 10, "func1"), |                         MockFrameInfo("file.py", 10, "func1"), | ||||||
|                         MockFrameInfo("file.py", 20, "func2"), |                         MockFrameInfo("file.py", 20, "func2"), | ||||||
|                     ], |                     ], | ||||||
|  |                 )] | ||||||
|             ) |             ) | ||||||
|         ] |         ] | ||||||
|         test_frames2 = [ |         test_frames2 = [ | ||||||
|             ( |             MockInterpreterInfo( | ||||||
|  |                 0, | ||||||
|  |                 [MockThreadInfo( | ||||||
|                     1, |                     1, | ||||||
|                     [ |                     [ | ||||||
|                         MockFrameInfo("file.py", 10, "func1"), |                         MockFrameInfo("file.py", 10, "func1"), | ||||||
|                         MockFrameInfo("file.py", 20, "func2"), |                         MockFrameInfo("file.py", 20, "func2"), | ||||||
|                     ], |                     ], | ||||||
|  |                 )] | ||||||
|             ) |             ) | ||||||
|         ]  # Same stack |         ]  # Same stack | ||||||
|         test_frames3 = [(1, [MockFrameInfo("other.py", 5, "other_func")])] |         test_frames3 = [MockInterpreterInfo(0, [MockThreadInfo(1, [MockFrameInfo("other.py", 5, "other_func")])])] | ||||||
| 
 | 
 | ||||||
|         collector.collect(test_frames1) |         collector.collect(test_frames1) | ||||||
|         collector.collect(test_frames2) |         collector.collect(test_frames2) | ||||||
|  | @ -1138,7 +1180,9 @@ def test_recursive_function_call_counting(self): | ||||||
| 
 | 
 | ||||||
|         # Simulate a recursive call pattern: fibonacci(5) calling itself |         # Simulate a recursive call pattern: fibonacci(5) calling itself | ||||||
|         recursive_frames = [ |         recursive_frames = [ | ||||||
|             ( |             MockInterpreterInfo( | ||||||
|  |                 0, | ||||||
|  |                 [MockThreadInfo( | ||||||
|                     1, |                     1, | ||||||
|                     [  # First sample: deep in recursion |                     [  # First sample: deep in recursion | ||||||
|                         MockFrameInfo("fib.py", 10, "fibonacci"), |                         MockFrameInfo("fib.py", 10, "fibonacci"), | ||||||
|  | @ -1149,16 +1193,22 @@ def test_recursive_function_call_counting(self): | ||||||
|                         MockFrameInfo("fib.py", 10, "fibonacci"),  # even deeper |                         MockFrameInfo("fib.py", 10, "fibonacci"),  # even deeper | ||||||
|                         MockFrameInfo("main.py", 5, "main"),  # main caller |                         MockFrameInfo("main.py", 5, "main"),  # main caller | ||||||
|                     ], |                     ], | ||||||
|  |                 )] | ||||||
|             ), |             ), | ||||||
|             ( |             MockInterpreterInfo( | ||||||
|  |                 0, | ||||||
|  |                 [MockThreadInfo( | ||||||
|                     1, |                     1, | ||||||
|                     [  # Second sample: different recursion depth |                     [  # Second sample: different recursion depth | ||||||
|                         MockFrameInfo("fib.py", 10, "fibonacci"), |                         MockFrameInfo("fib.py", 10, "fibonacci"), | ||||||
|                         MockFrameInfo("fib.py", 10, "fibonacci"),  # recursive call |                         MockFrameInfo("fib.py", 10, "fibonacci"),  # recursive call | ||||||
|                         MockFrameInfo("main.py", 5, "main"),  # main caller |                         MockFrameInfo("main.py", 5, "main"),  # main caller | ||||||
|                     ], |                     ], | ||||||
|  |                 )] | ||||||
|             ), |             ), | ||||||
|             ( |             MockInterpreterInfo( | ||||||
|  |                 0, | ||||||
|  |                 [MockThreadInfo( | ||||||
|                     1, |                     1, | ||||||
|                     [  # Third sample: back to deeper recursion |                     [  # Third sample: back to deeper recursion | ||||||
|                         MockFrameInfo("fib.py", 10, "fibonacci"), |                         MockFrameInfo("fib.py", 10, "fibonacci"), | ||||||
|  | @ -1166,6 +1216,7 @@ def test_recursive_function_call_counting(self): | ||||||
|                         MockFrameInfo("fib.py", 10, "fibonacci"), |                         MockFrameInfo("fib.py", 10, "fibonacci"), | ||||||
|                         MockFrameInfo("main.py", 5, "main"), |                         MockFrameInfo("main.py", 5, "main"), | ||||||
|                     ], |                     ], | ||||||
|  |                 )] | ||||||
|             ), |             ), | ||||||
|         ] |         ] | ||||||
| 
 | 
 | ||||||
|  | @ -1202,7 +1253,9 @@ def test_nested_function_hierarchy(self): | ||||||
| 
 | 
 | ||||||
|         # Simulate a deep call hierarchy |         # Simulate a deep call hierarchy | ||||||
|         deep_call_frames = [ |         deep_call_frames = [ | ||||||
|             ( |             MockInterpreterInfo( | ||||||
|  |                 0, | ||||||
|  |                 [MockThreadInfo( | ||||||
|                     1, |                     1, | ||||||
|                     [ |                     [ | ||||||
|                         MockFrameInfo("level1.py", 10, "level1_func"), |                         MockFrameInfo("level1.py", 10, "level1_func"), | ||||||
|  | @ -1212,8 +1265,11 @@ def test_nested_function_hierarchy(self): | ||||||
|                         MockFrameInfo("level5.py", 50, "level5_func"), |                         MockFrameInfo("level5.py", 50, "level5_func"), | ||||||
|                         MockFrameInfo("main.py", 5, "main"), |                         MockFrameInfo("main.py", 5, "main"), | ||||||
|                     ], |                     ], | ||||||
|  |                 )] | ||||||
|             ), |             ), | ||||||
|             ( |             MockInterpreterInfo( | ||||||
|  |                 0, | ||||||
|  |                 [MockThreadInfo( | ||||||
|                     1, |                     1, | ||||||
|                     [  # Same hierarchy sampled again |                     [  # Same hierarchy sampled again | ||||||
|                         MockFrameInfo("level1.py", 10, "level1_func"), |                         MockFrameInfo("level1.py", 10, "level1_func"), | ||||||
|  | @ -1223,6 +1279,7 @@ def test_nested_function_hierarchy(self): | ||||||
|                         MockFrameInfo("level5.py", 50, "level5_func"), |                         MockFrameInfo("level5.py", 50, "level5_func"), | ||||||
|                         MockFrameInfo("main.py", 5, "main"), |                         MockFrameInfo("main.py", 5, "main"), | ||||||
|                     ], |                     ], | ||||||
|  |                 )] | ||||||
|             ), |             ), | ||||||
|         ] |         ] | ||||||
| 
 | 
 | ||||||
|  | @ -1260,40 +1317,52 @@ def test_alternating_call_patterns(self): | ||||||
|         # Simulate alternating execution paths |         # Simulate alternating execution paths | ||||||
|         pattern_frames = [ |         pattern_frames = [ | ||||||
|             # Pattern A: path through func_a |             # Pattern A: path through func_a | ||||||
|             ( |             MockInterpreterInfo( | ||||||
|  |                 0, | ||||||
|  |                 [MockThreadInfo( | ||||||
|                     1, |                     1, | ||||||
|                     [ |                     [ | ||||||
|                         MockFrameInfo("module.py", 10, "func_a"), |                         MockFrameInfo("module.py", 10, "func_a"), | ||||||
|                         MockFrameInfo("module.py", 30, "shared_func"), |                         MockFrameInfo("module.py", 30, "shared_func"), | ||||||
|                         MockFrameInfo("main.py", 5, "main"), |                         MockFrameInfo("main.py", 5, "main"), | ||||||
|                     ], |                     ], | ||||||
|  |                 )] | ||||||
|             ), |             ), | ||||||
|             # Pattern B: path through func_b |             # Pattern B: path through func_b | ||||||
|             ( |             MockInterpreterInfo( | ||||||
|  |                 0, | ||||||
|  |                 [MockThreadInfo( | ||||||
|                     1, |                     1, | ||||||
|                     [ |                     [ | ||||||
|                         MockFrameInfo("module.py", 20, "func_b"), |                         MockFrameInfo("module.py", 20, "func_b"), | ||||||
|                         MockFrameInfo("module.py", 30, "shared_func"), |                         MockFrameInfo("module.py", 30, "shared_func"), | ||||||
|                         MockFrameInfo("main.py", 5, "main"), |                         MockFrameInfo("main.py", 5, "main"), | ||||||
|                     ], |                     ], | ||||||
|  |                 )] | ||||||
|             ), |             ), | ||||||
|             # Pattern A again |             # Pattern A again | ||||||
|             ( |             MockInterpreterInfo( | ||||||
|  |                 0, | ||||||
|  |                 [MockThreadInfo( | ||||||
|                     1, |                     1, | ||||||
|                     [ |                     [ | ||||||
|                         MockFrameInfo("module.py", 10, "func_a"), |                         MockFrameInfo("module.py", 10, "func_a"), | ||||||
|                         MockFrameInfo("module.py", 30, "shared_func"), |                         MockFrameInfo("module.py", 30, "shared_func"), | ||||||
|                         MockFrameInfo("main.py", 5, "main"), |                         MockFrameInfo("main.py", 5, "main"), | ||||||
|                     ], |                     ], | ||||||
|  |                 )] | ||||||
|             ), |             ), | ||||||
|             # Pattern B again |             # Pattern B again | ||||||
|             ( |             MockInterpreterInfo( | ||||||
|  |                 0, | ||||||
|  |                 [MockThreadInfo( | ||||||
|                     1, |                     1, | ||||||
|                     [ |                     [ | ||||||
|                         MockFrameInfo("module.py", 20, "func_b"), |                         MockFrameInfo("module.py", 20, "func_b"), | ||||||
|                         MockFrameInfo("module.py", 30, "shared_func"), |                         MockFrameInfo("module.py", 30, "shared_func"), | ||||||
|                         MockFrameInfo("main.py", 5, "main"), |                         MockFrameInfo("main.py", 5, "main"), | ||||||
|                     ], |                     ], | ||||||
|  |                 )] | ||||||
|             ), |             ), | ||||||
|         ] |         ] | ||||||
| 
 | 
 | ||||||
|  | @ -1328,7 +1397,9 @@ def test_collapsed_stack_with_recursion(self): | ||||||
| 
 | 
 | ||||||
|         # Recursive call pattern |         # Recursive call pattern | ||||||
|         recursive_frames = [ |         recursive_frames = [ | ||||||
|             ( |             MockInterpreterInfo( | ||||||
|  |                 0, | ||||||
|  |                 [MockThreadInfo( | ||||||
|                     1, |                     1, | ||||||
|                     [ |                     [ | ||||||
|                         ("factorial.py", 10, "factorial"), |                         ("factorial.py", 10, "factorial"), | ||||||
|  | @ -1336,14 +1407,18 @@ def test_collapsed_stack_with_recursion(self): | ||||||
|                         ("factorial.py", 10, "factorial"),  # deeper |                         ("factorial.py", 10, "factorial"),  # deeper | ||||||
|                         ("main.py", 5, "main"), |                         ("main.py", 5, "main"), | ||||||
|                     ], |                     ], | ||||||
|  |                 )] | ||||||
|             ), |             ), | ||||||
|             ( |             MockInterpreterInfo( | ||||||
|  |                 0, | ||||||
|  |                 [MockThreadInfo( | ||||||
|                     1, |                     1, | ||||||
|                     [ |                     [ | ||||||
|                         ("factorial.py", 10, "factorial"), |                         ("factorial.py", 10, "factorial"), | ||||||
|                         ("factorial.py", 10, "factorial"),  # different depth |                         ("factorial.py", 10, "factorial"),  # different depth | ||||||
|                         ("main.py", 5, "main"), |                         ("main.py", 5, "main"), | ||||||
|                     ], |                     ], | ||||||
|  |                 )] | ||||||
|             ), |             ), | ||||||
|         ] |         ] | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -164,6 +164,20 @@ static PyStructSequence_Desc ThreadInfo_desc = { | ||||||
|     2 |     2 | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | // InterpreterInfo structseq type - replaces 2-tuple (interpreter_id, thread_list)
 | ||||||
|  | static PyStructSequence_Field InterpreterInfo_fields[] = { | ||||||
|  |     {"interpreter_id", "Interpreter ID"}, | ||||||
|  |     {"threads", "List of threads in this interpreter"}, | ||||||
|  |     {NULL} | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | static PyStructSequence_Desc InterpreterInfo_desc = { | ||||||
|  |     "_remote_debugging.InterpreterInfo", | ||||||
|  |     "Information about an interpreter", | ||||||
|  |     InterpreterInfo_fields, | ||||||
|  |     2 | ||||||
|  | }; | ||||||
|  | 
 | ||||||
| // AwaitedInfo structseq type - replaces 2-tuple (tid, awaited_by_list)
 | // AwaitedInfo structseq type - replaces 2-tuple (tid, awaited_by_list)
 | ||||||
| static PyStructSequence_Field AwaitedInfo_fields[] = { | static PyStructSequence_Field AwaitedInfo_fields[] = { | ||||||
|     {"thread_id", "Thread ID"}, |     {"thread_id", "Thread ID"}, | ||||||
|  | @ -193,6 +207,7 @@ typedef struct { | ||||||
|     PyTypeObject *FrameInfo_Type; |     PyTypeObject *FrameInfo_Type; | ||||||
|     PyTypeObject *CoroInfo_Type; |     PyTypeObject *CoroInfo_Type; | ||||||
|     PyTypeObject *ThreadInfo_Type; |     PyTypeObject *ThreadInfo_Type; | ||||||
|  |     PyTypeObject *InterpreterInfo_Type; | ||||||
|     PyTypeObject *AwaitedInfo_Type; |     PyTypeObject *AwaitedInfo_Type; | ||||||
| } RemoteDebuggingState; | } RemoteDebuggingState; | ||||||
| 
 | 
 | ||||||
|  | @ -2649,20 +2664,23 @@ _remote_debugging_RemoteUnwinder___init___impl(RemoteUnwinderObject *self, | ||||||
| @critical_section | @critical_section | ||||||
| _remote_debugging.RemoteUnwinder.get_stack_trace | _remote_debugging.RemoteUnwinder.get_stack_trace | ||||||
| 
 | 
 | ||||||
| Returns a list of stack traces for threads in the target process. | Returns stack traces for all interpreters and threads in process. | ||||||
| 
 | 
 | ||||||
| Each element in the returned list is a tuple of (thread_id, frame_list), where: | Each element in the returned list is a tuple of (interpreter_id, thread_list), where: | ||||||
| - thread_id is the OS thread identifier | - interpreter_id is the interpreter identifier | ||||||
| - frame_list is a list of tuples (function_name, filename, line_number) representing | - thread_list is a list of tuples (thread_id, frame_list) for threads in that interpreter | ||||||
|  |   - thread_id is the OS thread identifier | ||||||
|  |   - frame_list is a list of tuples (function_name, filename, line_number) representing | ||||||
|     the Python stack frames for that thread, ordered from most recent to oldest |     the Python stack frames for that thread, ordered from most recent to oldest | ||||||
| 
 | 
 | ||||||
| The threads returned depend on the initialization parameters: | The threads returned depend on the initialization parameters: | ||||||
| - If only_active_thread was True: returns only the thread holding the GIL | - If only_active_thread was True: returns only the thread holding the GIL across all interpreters | ||||||
| - If all_threads was True: returns all threads | - If all_threads was True: returns all threads across all interpreters | ||||||
| - Otherwise: returns only the main thread | - Otherwise: returns only the main thread of each interpreter | ||||||
| 
 | 
 | ||||||
| Example: | Example: | ||||||
|     [ |     [ | ||||||
|  |         (0, [  # Main interpreter | ||||||
|             (1234, [ |             (1234, [ | ||||||
|                 ('process_data', 'worker.py', 127), |                 ('process_data', 'worker.py', 127), | ||||||
|                 ('run_worker', 'worker.py', 45), |                 ('run_worker', 'worker.py', 45), | ||||||
|  | @ -2672,6 +2690,12 @@ The threads returned depend on the initialization parameters: | ||||||
|                 ('handle_request', 'server.py', 89), |                 ('handle_request', 'server.py', 89), | ||||||
|                 ('serve_forever', 'server.py', 52) |                 ('serve_forever', 'server.py', 52) | ||||||
|             ]) |             ]) | ||||||
|  |         ]), | ||||||
|  |         (1, [  # Sub-interpreter | ||||||
|  |             (1236, [ | ||||||
|  |                 ('sub_worker', 'sub.py', 15) | ||||||
|  |             ]) | ||||||
|  |         ]) | ||||||
|     ] |     ] | ||||||
| 
 | 
 | ||||||
| Raises: | Raises: | ||||||
|  | @ -2684,20 +2708,32 @@ The threads returned depend on the initialization parameters: | ||||||
| 
 | 
 | ||||||
| static PyObject * | static PyObject * | ||||||
| _remote_debugging_RemoteUnwinder_get_stack_trace_impl(RemoteUnwinderObject *self) | _remote_debugging_RemoteUnwinder_get_stack_trace_impl(RemoteUnwinderObject *self) | ||||||
| /*[clinic end generated code: output=666192b90c69d567 input=c527a4b858601408]*/ | /*[clinic end generated code: output=666192b90c69d567 input=bcff01c73cccc1c0]*/ | ||||||
| { | { | ||||||
|     PyObject* result = NULL; |     PyObject* result = PyList_New(0); | ||||||
|     // Read interpreter state into opaque buffer
 |     if (!result) { | ||||||
|  |         set_exception_cause(self, PyExc_MemoryError, "Failed to create stack trace result list"); | ||||||
|  |         return NULL; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Iterate over all interpreters
 | ||||||
|  |     uintptr_t current_interpreter = self->interpreter_addr; | ||||||
|  |     while (current_interpreter != 0) { | ||||||
|  |         // Read interpreter state to get the interpreter ID
 | ||||||
|         char interp_state_buffer[INTERP_STATE_BUFFER_SIZE]; |         char interp_state_buffer[INTERP_STATE_BUFFER_SIZE]; | ||||||
|         if (_Py_RemoteDebug_PagedReadRemoteMemory( |         if (_Py_RemoteDebug_PagedReadRemoteMemory( | ||||||
|                 &self->handle, |                 &self->handle, | ||||||
|             self->interpreter_addr, |                 current_interpreter, | ||||||
|                 INTERP_STATE_BUFFER_SIZE, |                 INTERP_STATE_BUFFER_SIZE, | ||||||
|                 interp_state_buffer) < 0) { |                 interp_state_buffer) < 0) { | ||||||
|             set_exception_cause(self, PyExc_RuntimeError, "Failed to read interpreter state buffer"); |             set_exception_cause(self, PyExc_RuntimeError, "Failed to read interpreter state buffer"); | ||||||
|  |             Py_CLEAR(result); | ||||||
|             goto exit; |             goto exit; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |         int64_t interpreter_id = GET_MEMBER(int64_t, interp_state_buffer, | ||||||
|  |                 self->debug_offsets.interpreter_state.id); | ||||||
|  | 
 | ||||||
|         // Get code object generation from buffer
 |         // Get code object generation from buffer
 | ||||||
|         uint64_t code_object_generation = GET_MEMBER(uint64_t, interp_state_buffer, |         uint64_t code_object_generation = GET_MEMBER(uint64_t, interp_state_buffer, | ||||||
|                 self->debug_offsets.interpreter_state.code_object_generation); |                 self->debug_offsets.interpreter_state.code_object_generation); | ||||||
|  | @ -2707,28 +2743,6 @@ _remote_debugging_RemoteUnwinder_get_stack_trace_impl(RemoteUnwinderObject *self | ||||||
|             _Py_hashtable_clear(self->code_object_cache); |             _Py_hashtable_clear(self->code_object_cache); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|     // If only_active_thread is true, we need to determine which thread holds the GIL
 |  | ||||||
|     PyThreadState* gil_holder = NULL; |  | ||||||
|     if (self->only_active_thread) { |  | ||||||
|         // The GIL state is already in interp_state_buffer, just read from there
 |  | ||||||
|         // Check if GIL is locked
 |  | ||||||
|         int gil_locked = GET_MEMBER(int, interp_state_buffer, |  | ||||||
|             self->debug_offsets.interpreter_state.gil_runtime_state_locked); |  | ||||||
| 
 |  | ||||||
|         if (gil_locked) { |  | ||||||
|             // Get the last holder (current holder when GIL is locked)
 |  | ||||||
|             gil_holder = GET_MEMBER(PyThreadState*, interp_state_buffer, |  | ||||||
|                 self->debug_offsets.interpreter_state.gil_runtime_state_holder); |  | ||||||
|         } else { |  | ||||||
|             // GIL is not locked, return empty list
 |  | ||||||
|             result = PyList_New(0); |  | ||||||
|             if (!result) { |  | ||||||
|                 set_exception_cause(self, PyExc_MemoryError, "Failed to create empty result list"); |  | ||||||
|             } |  | ||||||
|             goto exit; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
| #ifdef Py_GIL_DISABLED | #ifdef Py_GIL_DISABLED | ||||||
|         // Check TLBC generation and invalidate cache if needed
 |         // Check TLBC generation and invalidate cache if needed
 | ||||||
|         uint32_t current_tlbc_generation = GET_MEMBER(uint32_t, interp_state_buffer, |         uint32_t current_tlbc_generation = GET_MEMBER(uint32_t, interp_state_buffer, | ||||||
|  | @ -2739,47 +2753,101 @@ _remote_debugging_RemoteUnwinder_get_stack_trace_impl(RemoteUnwinderObject *self | ||||||
|         } |         } | ||||||
| #endif | #endif | ||||||
| 
 | 
 | ||||||
|  |         // Create a list to hold threads for this interpreter
 | ||||||
|  |         PyObject *interpreter_threads = PyList_New(0); | ||||||
|  |         if (!interpreter_threads) { | ||||||
|  |             set_exception_cause(self, PyExc_MemoryError, "Failed to create interpreter threads list"); | ||||||
|  |             Py_CLEAR(result); | ||||||
|  |             goto exit; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         uintptr_t current_tstate; |         uintptr_t current_tstate; | ||||||
|     if (self->only_active_thread && gil_holder != NULL) { |         if (self->only_active_thread) { | ||||||
|         // We have the GIL holder, process only that thread
 |             // Find the GIL holder for THIS interpreter
 | ||||||
|         current_tstate = (uintptr_t)gil_holder; |             int gil_locked = GET_MEMBER(int, interp_state_buffer, | ||||||
|  |                 self->debug_offsets.interpreter_state.gil_runtime_state_locked); | ||||||
|  | 
 | ||||||
|  |             if (!gil_locked) { | ||||||
|  |                 // This interpreter's GIL is not locked, skip it
 | ||||||
|  |                 Py_DECREF(interpreter_threads); | ||||||
|  |                 goto next_interpreter; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             // Get the GIL holder for this interpreter
 | ||||||
|  |             current_tstate = (uintptr_t)GET_MEMBER(PyThreadState*, interp_state_buffer, | ||||||
|  |                 self->debug_offsets.interpreter_state.gil_runtime_state_holder); | ||||||
|         } else if (self->tstate_addr == 0) { |         } else if (self->tstate_addr == 0) { | ||||||
|         // Get threads head from buffer
 |             // Get all threads for this interpreter
 | ||||||
|             current_tstate = GET_MEMBER(uintptr_t, interp_state_buffer, |             current_tstate = GET_MEMBER(uintptr_t, interp_state_buffer, | ||||||
|                     self->debug_offsets.interpreter_state.threads_head); |                     self->debug_offsets.interpreter_state.threads_head); | ||||||
|         } else { |         } else { | ||||||
|  |             // Target specific thread (only process first interpreter)
 | ||||||
|             current_tstate = self->tstate_addr; |             current_tstate = self->tstate_addr; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|     result = PyList_New(0); |  | ||||||
|     if (!result) { |  | ||||||
|         set_exception_cause(self, PyExc_MemoryError, "Failed to create stack trace result list"); |  | ||||||
|         goto exit; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|         while (current_tstate != 0) { |         while (current_tstate != 0) { | ||||||
|             PyObject* frame_info = unwind_stack_for_thread(self, ¤t_tstate); |             PyObject* frame_info = unwind_stack_for_thread(self, ¤t_tstate); | ||||||
|             if (!frame_info) { |             if (!frame_info) { | ||||||
|             Py_CLEAR(result); |                 Py_DECREF(interpreter_threads); | ||||||
|                 set_exception_cause(self, PyExc_RuntimeError, "Failed to unwind stack for thread"); |                 set_exception_cause(self, PyExc_RuntimeError, "Failed to unwind stack for thread"); | ||||||
|             goto exit; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         if (PyList_Append(result, frame_info) == -1) { |  | ||||||
|             Py_DECREF(frame_info); |  | ||||||
|                 Py_CLEAR(result); |                 Py_CLEAR(result); | ||||||
|  |                 goto exit; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if (PyList_Append(interpreter_threads, frame_info) == -1) { | ||||||
|  |                 Py_DECREF(frame_info); | ||||||
|  |                 Py_DECREF(interpreter_threads); | ||||||
|                 set_exception_cause(self, PyExc_RuntimeError, "Failed to append thread frame info"); |                 set_exception_cause(self, PyExc_RuntimeError, "Failed to append thread frame info"); | ||||||
|  |                 Py_CLEAR(result); | ||||||
|                 goto exit; |                 goto exit; | ||||||
|             } |             } | ||||||
|             Py_DECREF(frame_info); |             Py_DECREF(frame_info); | ||||||
| 
 | 
 | ||||||
|         // We are targeting a single tstate, break here
 |             // If targeting specific thread or only active thread, process just one
 | ||||||
|         if (self->tstate_addr) { |             if (self->tstate_addr || self->only_active_thread) { | ||||||
|                 break; |                 break; | ||||||
|             } |             } | ||||||
|  |         } | ||||||
| 
 | 
 | ||||||
|         // If we're only processing the GIL holder, we're done after one iteration
 |         // Create the InterpreterInfo StructSequence
 | ||||||
|         if (self->only_active_thread && gil_holder != NULL) { |         RemoteDebuggingState *state = RemoteDebugging_GetStateFromObject((PyObject*)self); | ||||||
|  |         PyObject *interpreter_info = PyStructSequence_New(state->InterpreterInfo_Type); | ||||||
|  |         if (!interpreter_info) { | ||||||
|  |             Py_DECREF(interpreter_threads); | ||||||
|  |             set_exception_cause(self, PyExc_MemoryError, "Failed to create InterpreterInfo"); | ||||||
|  |             Py_CLEAR(result); | ||||||
|  |             goto exit; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         PyObject *interp_id = PyLong_FromLongLong(interpreter_id); | ||||||
|  |         if (!interp_id) { | ||||||
|  |             Py_DECREF(interpreter_threads); | ||||||
|  |             Py_DECREF(interpreter_info); | ||||||
|  |             set_exception_cause(self, PyExc_MemoryError, "Failed to create interpreter ID"); | ||||||
|  |             Py_CLEAR(result); | ||||||
|  |             goto exit; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         PyStructSequence_SetItem(interpreter_info, 0, interp_id);  // steals reference
 | ||||||
|  |         PyStructSequence_SetItem(interpreter_info, 1, interpreter_threads);  // steals reference
 | ||||||
|  | 
 | ||||||
|  |         // Add this interpreter to the result list
 | ||||||
|  |         if (PyList_Append(result, interpreter_info) == -1) { | ||||||
|  |             Py_DECREF(interpreter_info); | ||||||
|  |             set_exception_cause(self, PyExc_RuntimeError, "Failed to append interpreter info"); | ||||||
|  |             Py_CLEAR(result); | ||||||
|  |             goto exit; | ||||||
|  |         } | ||||||
|  |         Py_DECREF(interpreter_info); | ||||||
|  | 
 | ||||||
|  | next_interpreter: | ||||||
|  | 
 | ||||||
|  |         // Get the next interpreter address
 | ||||||
|  |         current_interpreter = GET_MEMBER(uintptr_t, interp_state_buffer, | ||||||
|  |                 self->debug_offsets.interpreter_state.next); | ||||||
|  | 
 | ||||||
|  |         // If we're targeting a specific thread, stop after first interpreter
 | ||||||
|  |         if (self->tstate_addr != 0) { | ||||||
|             break; |             break; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  | @ -3053,6 +3121,14 @@ _remote_debugging_exec(PyObject *m) | ||||||
|         return -1; |         return -1; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     st->InterpreterInfo_Type = PyStructSequence_NewType(&InterpreterInfo_desc); | ||||||
|  |     if (st->InterpreterInfo_Type == NULL) { | ||||||
|  |         return -1; | ||||||
|  |     } | ||||||
|  |     if (PyModule_AddType(m, st->InterpreterInfo_Type) < 0) { | ||||||
|  |         return -1; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     st->AwaitedInfo_Type = PyStructSequence_NewType(&AwaitedInfo_desc); |     st->AwaitedInfo_Type = PyStructSequence_NewType(&AwaitedInfo_desc); | ||||||
|     if (st->AwaitedInfo_Type == NULL) { |     if (st->AwaitedInfo_Type == NULL) { | ||||||
|         return -1; |         return -1; | ||||||
|  | @ -3082,6 +3158,7 @@ remote_debugging_traverse(PyObject *mod, visitproc visit, void *arg) | ||||||
|     Py_VISIT(state->FrameInfo_Type); |     Py_VISIT(state->FrameInfo_Type); | ||||||
|     Py_VISIT(state->CoroInfo_Type); |     Py_VISIT(state->CoroInfo_Type); | ||||||
|     Py_VISIT(state->ThreadInfo_Type); |     Py_VISIT(state->ThreadInfo_Type); | ||||||
|  |     Py_VISIT(state->InterpreterInfo_Type); | ||||||
|     Py_VISIT(state->AwaitedInfo_Type); |     Py_VISIT(state->AwaitedInfo_Type); | ||||||
|     return 0; |     return 0; | ||||||
| } | } | ||||||
|  | @ -3095,6 +3172,7 @@ remote_debugging_clear(PyObject *mod) | ||||||
|     Py_CLEAR(state->FrameInfo_Type); |     Py_CLEAR(state->FrameInfo_Type); | ||||||
|     Py_CLEAR(state->CoroInfo_Type); |     Py_CLEAR(state->CoroInfo_Type); | ||||||
|     Py_CLEAR(state->ThreadInfo_Type); |     Py_CLEAR(state->ThreadInfo_Type); | ||||||
|  |     Py_CLEAR(state->InterpreterInfo_Type); | ||||||
|     Py_CLEAR(state->AwaitedInfo_Type); |     Py_CLEAR(state->AwaitedInfo_Type); | ||||||
|     return 0; |     return 0; | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										25
									
								
								Modules/clinic/_remote_debugging_module.c.h
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										25
									
								
								Modules/clinic/_remote_debugging_module.c.h
									
										
									
										generated
									
									
									
								
							|  | @ -125,20 +125,23 @@ PyDoc_STRVAR(_remote_debugging_RemoteUnwinder_get_stack_trace__doc__, | ||||||
| "get_stack_trace($self, /)\n" | "get_stack_trace($self, /)\n" | ||||||
| "--\n" | "--\n" | ||||||
| "\n" | "\n" | ||||||
| "Returns a list of stack traces for threads in the target process.\n" | "Returns stack traces for all interpreters and threads in process.\n" | ||||||
| "\n" | "\n" | ||||||
| "Each element in the returned list is a tuple of (thread_id, frame_list), where:\n" | "Each element in the returned list is a tuple of (interpreter_id, thread_list), where:\n" | ||||||
| "- thread_id is the OS thread identifier\n" | "- interpreter_id is the interpreter identifier\n" | ||||||
| "- frame_list is a list of tuples (function_name, filename, line_number) representing\n" | "- thread_list is a list of tuples (thread_id, frame_list) for threads in that interpreter\n" | ||||||
|  | "  - thread_id is the OS thread identifier\n" | ||||||
|  | "  - frame_list is a list of tuples (function_name, filename, line_number) representing\n" | ||||||
| "    the Python stack frames for that thread, ordered from most recent to oldest\n" | "    the Python stack frames for that thread, ordered from most recent to oldest\n" | ||||||
| "\n" | "\n" | ||||||
| "The threads returned depend on the initialization parameters:\n" | "The threads returned depend on the initialization parameters:\n" | ||||||
| "- If only_active_thread was True: returns only the thread holding the GIL\n" | "- If only_active_thread was True: returns only the thread holding the GIL across all interpreters\n" | ||||||
| "- If all_threads was True: returns all threads\n" | "- If all_threads was True: returns all threads across all interpreters\n" | ||||||
| "- Otherwise: returns only the main thread\n" | "- Otherwise: returns only the main thread of each interpreter\n" | ||||||
| "\n" | "\n" | ||||||
| "Example:\n" | "Example:\n" | ||||||
| "    [\n" | "    [\n" | ||||||
|  | "        (0, [  # Main interpreter\n" | ||||||
| "            (1234, [\n" | "            (1234, [\n" | ||||||
| "                (\'process_data\', \'worker.py\', 127),\n" | "                (\'process_data\', \'worker.py\', 127),\n" | ||||||
| "                (\'run_worker\', \'worker.py\', 45),\n" | "                (\'run_worker\', \'worker.py\', 45),\n" | ||||||
|  | @ -148,6 +151,12 @@ PyDoc_STRVAR(_remote_debugging_RemoteUnwinder_get_stack_trace__doc__, | ||||||
| "                (\'handle_request\', \'server.py\', 89),\n" | "                (\'handle_request\', \'server.py\', 89),\n" | ||||||
| "                (\'serve_forever\', \'server.py\', 52)\n" | "                (\'serve_forever\', \'server.py\', 52)\n" | ||||||
| "            ])\n" | "            ])\n" | ||||||
|  | "        ]),\n" | ||||||
|  | "        (1, [  # Sub-interpreter\n" | ||||||
|  | "            (1236, [\n" | ||||||
|  | "                (\'sub_worker\', \'sub.py\', 15)\n" | ||||||
|  | "            ])\n" | ||||||
|  | "        ])\n" | ||||||
| "    ]\n" | "    ]\n" | ||||||
| "\n" | "\n" | ||||||
| "Raises:\n" | "Raises:\n" | ||||||
|  | @ -288,4 +297,4 @@ _remote_debugging_RemoteUnwinder_get_async_stack_trace(PyObject *self, PyObject | ||||||
| 
 | 
 | ||||||
|     return return_value; |     return return_value; | ||||||
| } | } | ||||||
| /*[clinic end generated code: output=0dd1e6e8bab2a8b1 input=a9049054013a1b77]*/ | /*[clinic end generated code: output=2ba15411abf82c33 input=a9049054013a1b77]*/ | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Pablo Galindo Salgado
						Pablo Galindo Salgado