|
19 | 19 |
|
20 | 20 | import subprocess |
21 | 21 |
|
| 22 | +# Profiling mode constants |
| 23 | +PROFILING_MODE_WALL = 0 |
| 24 | +PROFILING_MODE_CPU = 1 |
| 25 | +PROFILING_MODE_GIL = 2 |
| 26 | + |
22 | 27 | try: |
23 | 28 | from concurrent import interpreters |
24 | 29 | except ImportError: |
@@ -1747,7 +1752,8 @@ def busy(): |
1747 | 1752 |
|
1748 | 1753 | attempts = 10 |
1749 | 1754 | try: |
1750 | | - unwinder = RemoteUnwinder(p.pid, all_threads=True, cpu_time=True) |
| 1755 | + unwinder = RemoteUnwinder(p.pid, all_threads=True, mode=PROFILING_MODE_CPU, |
| 1756 | + skip_non_matching_threads=False) |
1751 | 1757 | for _ in range(attempts): |
1752 | 1758 | traces = unwinder.get_stack_trace() |
1753 | 1759 | # Check if any thread is running |
@@ -1780,5 +1786,117 @@ def busy(): |
1780 | 1786 | p.terminate() |
1781 | 1787 | p.wait(timeout=SHORT_TIMEOUT) |
1782 | 1788 |
|
| 1789 | + @unittest.skipIf( |
| 1790 | + sys.platform not in ("linux", "darwin", "win32"), |
| 1791 | + "Test only runs on unsupported platforms (not Linux, macOS, or Windows)", |
| 1792 | + ) |
| 1793 | + @unittest.skipIf(sys.platform == "android", "Android raises Linux-specific exception") |
| 1794 | + def test_thread_status_gil_detection(self): |
| 1795 | + port = find_unused_port() |
| 1796 | + script = textwrap.dedent( |
| 1797 | + f"""\ |
| 1798 | + import time, sys, socket, threading |
| 1799 | + import os |
| 1800 | +
|
| 1801 | + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
| 1802 | + sock.connect(('localhost', {port})) |
| 1803 | +
|
| 1804 | + def sleeper(): |
| 1805 | + tid = threading.get_native_id() |
| 1806 | + sock.sendall(f'ready:sleeper:{{tid}}\\n'.encode()) |
| 1807 | + time.sleep(10000) |
| 1808 | +
|
| 1809 | + def busy(): |
| 1810 | + tid = threading.get_native_id() |
| 1811 | + sock.sendall(f'ready:busy:{{tid}}\\n'.encode()) |
| 1812 | + x = 0 |
| 1813 | + while True: |
| 1814 | + x = x + 1 |
| 1815 | + time.sleep(0.5) |
| 1816 | +
|
| 1817 | + t1 = threading.Thread(target=sleeper) |
| 1818 | + t2 = threading.Thread(target=busy) |
| 1819 | + t1.start() |
| 1820 | + t2.start() |
| 1821 | + sock.sendall(b'ready:main\\n') |
| 1822 | + t1.join() |
| 1823 | + t2.join() |
| 1824 | + sock.close() |
| 1825 | + """ |
| 1826 | + ) |
| 1827 | + with os_helper.temp_dir() as work_dir: |
| 1828 | + script_dir = os.path.join(work_dir, "script_pkg") |
| 1829 | + os.mkdir(script_dir) |
| 1830 | + server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
| 1831 | + server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) |
| 1832 | + server_socket.bind(("localhost", port)) |
| 1833 | + server_socket.settimeout(SHORT_TIMEOUT) |
| 1834 | + server_socket.listen(1) |
| 1835 | + |
| 1836 | + script_name = _make_test_script(script_dir, "thread_status_script", script) |
| 1837 | + client_socket = None |
| 1838 | + try: |
| 1839 | + p = subprocess.Popen([sys.executable, script_name]) |
| 1840 | + client_socket, _ = server_socket.accept() |
| 1841 | + server_socket.close() |
| 1842 | + response = b"" |
| 1843 | + sleeper_tid = None |
| 1844 | + busy_tid = None |
| 1845 | + while True: |
| 1846 | + chunk = client_socket.recv(1024) |
| 1847 | + response += chunk |
| 1848 | + if b"ready:main" in response and b"ready:sleeper" in response and b"ready:busy" in response: |
| 1849 | + # Parse TIDs from the response |
| 1850 | + for line in response.split(b"\n"): |
| 1851 | + if line.startswith(b"ready:sleeper:"): |
| 1852 | + try: |
| 1853 | + sleeper_tid = int(line.split(b":")[-1]) |
| 1854 | + except Exception: |
| 1855 | + pass |
| 1856 | + elif line.startswith(b"ready:busy:"): |
| 1857 | + try: |
| 1858 | + busy_tid = int(line.split(b":")[-1]) |
| 1859 | + except Exception: |
| 1860 | + pass |
| 1861 | + break |
| 1862 | + |
| 1863 | + attempts = 10 |
| 1864 | + try: |
| 1865 | + unwinder = RemoteUnwinder(p.pid, all_threads=True, mode=PROFILING_MODE_GIL, |
| 1866 | + skip_non_matching_threads=False) |
| 1867 | + for _ in range(attempts): |
| 1868 | + traces = unwinder.get_stack_trace() |
| 1869 | + # Check if any thread is running |
| 1870 | + if any(thread_info.status == 0 for interpreter_info in traces |
| 1871 | + for thread_info in interpreter_info.threads): |
| 1872 | + break |
| 1873 | + time.sleep(0.5) # Give a bit of time to let threads settle |
| 1874 | + except PermissionError: |
| 1875 | + self.skipTest( |
| 1876 | + "Insufficient permissions to read the stack trace" |
| 1877 | + ) |
| 1878 | + |
| 1879 | + |
| 1880 | + # Find threads and their statuses |
| 1881 | + statuses = {} |
| 1882 | + for interpreter_info in traces: |
| 1883 | + for thread_info in interpreter_info.threads: |
| 1884 | + statuses[thread_info.thread_id] = thread_info.status |
| 1885 | + |
| 1886 | + self.assertIsNotNone(sleeper_tid, "Sleeper thread id not received") |
| 1887 | + self.assertIsNotNone(busy_tid, "Busy thread id not received") |
| 1888 | + self.assertIn(sleeper_tid, statuses, "Sleeper tid not found in sampled threads") |
| 1889 | + self.assertIn(busy_tid, statuses, "Busy tid not found in sampled threads") |
| 1890 | + self.assertEqual(statuses[sleeper_tid], 2, "Sleeper thread should be idle (1)") |
| 1891 | + self.assertEqual(statuses[busy_tid], 0, "Busy thread should be running (0)") |
| 1892 | + |
| 1893 | + finally: |
| 1894 | + if client_socket is not None: |
| 1895 | + client_socket.close() |
| 1896 | + p.terminate() |
| 1897 | + p.wait(timeout=SHORT_TIMEOUT) |
| 1898 | + |
| 1899 | + |
| 1900 | + |
1783 | 1901 | if __name__ == "__main__": |
1784 | 1902 | unittest.main() |
0 commit comments