| 
									
										
										
										
											2022-08-30 18:11:18 +01:00
										 |  |  | import unittest | 
					
						
							| 
									
										
										
										
											2023-05-07 21:42:26 +02:00
										 |  |  | import string | 
					
						
							| 
									
										
										
										
											2022-08-30 18:11:18 +01:00
										 |  |  | import subprocess | 
					
						
							|  |  |  | import sys | 
					
						
							|  |  |  | import sysconfig | 
					
						
							|  |  |  | import os | 
					
						
							|  |  |  | import pathlib | 
					
						
							| 
									
										
										
										
											2024-05-05 03:07:29 +02:00
										 |  |  | import shutil | 
					
						
							| 
									
										
										
										
											2022-08-30 18:11:18 +01:00
										 |  |  | from test import support | 
					
						
							|  |  |  | from test.support.script_helper import ( | 
					
						
							|  |  |  |     make_script, | 
					
						
							|  |  |  |     assert_python_failure, | 
					
						
							|  |  |  |     assert_python_ok, | 
					
						
							|  |  |  | ) | 
					
						
							|  |  |  | from test.support.os_helper import temp_dir | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | if not support.has_subprocess_support: | 
					
						
							|  |  |  |     raise unittest.SkipTest("test module requires subprocess") | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-09-19 19:42:51 +02:00
										 |  |  | if support.check_sanitizer(address=True, memory=True, ub=True): | 
					
						
							|  |  |  |     # gh-109580: Skip the test because it does crash randomly if Python is | 
					
						
							|  |  |  |     # built with ASAN. | 
					
						
							|  |  |  |     raise unittest.SkipTest("test crash randomly on ASAN/MSAN/UBSAN build") | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-08-30 18:11:18 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | def supports_trampoline_profiling(): | 
					
						
							|  |  |  |     perf_trampoline = sysconfig.get_config_var("PY_HAVE_PERF_TRAMPOLINE") | 
					
						
							|  |  |  |     if not perf_trampoline: | 
					
						
							|  |  |  |         return False | 
					
						
							|  |  |  |     return int(perf_trampoline) == 1 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | if not supports_trampoline_profiling(): | 
					
						
							|  |  |  |     raise unittest.SkipTest("perf trampoline profiling not supported") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class TestPerfTrampoline(unittest.TestCase): | 
					
						
							|  |  |  |     def setUp(self): | 
					
						
							|  |  |  |         super().setUp() | 
					
						
							|  |  |  |         self.perf_files = set(pathlib.Path("/tmp/").glob("perf-*.map")) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def tearDown(self) -> None: | 
					
						
							|  |  |  |         super().tearDown() | 
					
						
							|  |  |  |         files_to_delete = ( | 
					
						
							|  |  |  |             set(pathlib.Path("/tmp/").glob("perf-*.map")) - self.perf_files | 
					
						
							|  |  |  |         ) | 
					
						
							|  |  |  |         for file in files_to_delete: | 
					
						
							|  |  |  |             file.unlink() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def test_trampoline_works(self): | 
					
						
							|  |  |  |         code = """if 1:
 | 
					
						
							|  |  |  |                 def foo(): | 
					
						
							|  |  |  |                     pass | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 def bar(): | 
					
						
							|  |  |  |                     foo() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 def baz(): | 
					
						
							|  |  |  |                     bar() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 baz() | 
					
						
							|  |  |  |                 """
 | 
					
						
							|  |  |  |         with temp_dir() as script_dir: | 
					
						
							|  |  |  |             script = make_script(script_dir, "perftest", code) | 
					
						
							|  |  |  |             with subprocess.Popen( | 
					
						
							|  |  |  |                 [sys.executable, "-Xperf", script], | 
					
						
							| 
									
										
										
										
											2022-08-30 19:37:22 +01:00
										 |  |  |                 text=True, | 
					
						
							| 
									
										
										
										
											2022-08-30 18:11:18 +01:00
										 |  |  |                 stderr=subprocess.PIPE, | 
					
						
							|  |  |  |                 stdout=subprocess.PIPE, | 
					
						
							|  |  |  |             ) as process: | 
					
						
							|  |  |  |                 stdout, stderr = process.communicate() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         self.assertEqual(stderr, "") | 
					
						
							|  |  |  |         self.assertEqual(stdout, "") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         perf_file = pathlib.Path(f"/tmp/perf-{process.pid}.map") | 
					
						
							|  |  |  |         self.assertTrue(perf_file.exists()) | 
					
						
							|  |  |  |         perf_file_contents = perf_file.read_text() | 
					
						
							| 
									
										
										
										
											2024-05-05 03:07:29 +02:00
										 |  |  |         perf_lines = perf_file_contents.splitlines() | 
					
						
							|  |  |  |         expected_symbols = [ | 
					
						
							|  |  |  |             f"py::foo:{script}", | 
					
						
							|  |  |  |             f"py::bar:{script}", | 
					
						
							|  |  |  |             f"py::baz:{script}", | 
					
						
							|  |  |  |         ] | 
					
						
							| 
									
										
										
										
											2023-05-07 21:42:26 +02:00
										 |  |  |         for expected_symbol in expected_symbols: | 
					
						
							| 
									
										
										
										
											2024-05-05 03:07:29 +02:00
										 |  |  |             perf_line = next( | 
					
						
							|  |  |  |                 (line for line in perf_lines if expected_symbol in line), None | 
					
						
							|  |  |  |             ) | 
					
						
							|  |  |  |             self.assertIsNotNone( | 
					
						
							|  |  |  |                 perf_line, f"Could not find {expected_symbol} in perf file" | 
					
						
							|  |  |  |             ) | 
					
						
							| 
									
										
										
										
											2023-05-07 21:42:26 +02:00
										 |  |  |             perf_addr = perf_line.split(" ")[0] | 
					
						
							| 
									
										
										
										
											2024-05-05 03:07:29 +02:00
										 |  |  |             self.assertFalse( | 
					
						
							|  |  |  |                 perf_addr.startswith("0x"), "Address should not be prefixed with 0x" | 
					
						
							|  |  |  |             ) | 
					
						
							|  |  |  |             self.assertTrue( | 
					
						
							|  |  |  |                 set(perf_addr).issubset(string.hexdigits), | 
					
						
							|  |  |  |                 "Address should contain only hex characters", | 
					
						
							|  |  |  |             ) | 
					
						
							| 
									
										
										
										
											2022-08-30 18:11:18 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |     def test_trampoline_works_with_forks(self): | 
					
						
							|  |  |  |         code = """if 1:
 | 
					
						
							|  |  |  |                 import os, sys | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 def foo_fork(): | 
					
						
							|  |  |  |                     pass | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 def bar_fork(): | 
					
						
							|  |  |  |                     foo_fork() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 def baz_fork(): | 
					
						
							|  |  |  |                     bar_fork() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 def foo(): | 
					
						
							|  |  |  |                     pid = os.fork() | 
					
						
							|  |  |  |                     if pid == 0: | 
					
						
							|  |  |  |                         print(os.getpid()) | 
					
						
							|  |  |  |                         baz_fork() | 
					
						
							|  |  |  |                     else: | 
					
						
							|  |  |  |                         _, status = os.waitpid(-1, 0) | 
					
						
							|  |  |  |                         sys.exit(status) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 def bar(): | 
					
						
							|  |  |  |                     foo() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 def baz(): | 
					
						
							|  |  |  |                     bar() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 baz() | 
					
						
							|  |  |  |                 """
 | 
					
						
							|  |  |  |         with temp_dir() as script_dir: | 
					
						
							|  |  |  |             script = make_script(script_dir, "perftest", code) | 
					
						
							|  |  |  |             with subprocess.Popen( | 
					
						
							|  |  |  |                 [sys.executable, "-Xperf", script], | 
					
						
							| 
									
										
										
										
											2022-09-01 02:47:40 -07:00
										 |  |  |                 text=True, | 
					
						
							| 
									
										
										
										
											2022-08-30 18:11:18 +01:00
										 |  |  |                 stderr=subprocess.PIPE, | 
					
						
							|  |  |  |                 stdout=subprocess.PIPE, | 
					
						
							|  |  |  |             ) as process: | 
					
						
							|  |  |  |                 stdout, stderr = process.communicate() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         self.assertEqual(process.returncode, 0) | 
					
						
							|  |  |  |         self.assertEqual(stderr, "") | 
					
						
							|  |  |  |         child_pid = int(stdout.strip()) | 
					
						
							|  |  |  |         perf_file = pathlib.Path(f"/tmp/perf-{process.pid}.map") | 
					
						
							|  |  |  |         perf_child_file = pathlib.Path(f"/tmp/perf-{child_pid}.map") | 
					
						
							|  |  |  |         self.assertTrue(perf_file.exists()) | 
					
						
							|  |  |  |         self.assertTrue(perf_child_file.exists()) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         perf_file_contents = perf_file.read_text() | 
					
						
							|  |  |  |         self.assertIn(f"py::foo:{script}", perf_file_contents) | 
					
						
							|  |  |  |         self.assertIn(f"py::bar:{script}", perf_file_contents) | 
					
						
							|  |  |  |         self.assertIn(f"py::baz:{script}", perf_file_contents) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         child_perf_file_contents = perf_child_file.read_text() | 
					
						
							|  |  |  |         self.assertIn(f"py::foo_fork:{script}", child_perf_file_contents) | 
					
						
							|  |  |  |         self.assertIn(f"py::bar_fork:{script}", child_perf_file_contents) | 
					
						
							|  |  |  |         self.assertIn(f"py::baz_fork:{script}", child_perf_file_contents) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def test_sys_api(self): | 
					
						
							|  |  |  |         code = """if 1:
 | 
					
						
							|  |  |  |                 import sys | 
					
						
							|  |  |  |                 def foo(): | 
					
						
							|  |  |  |                     pass | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 def spam(): | 
					
						
							|  |  |  |                     pass | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 def bar(): | 
					
						
							|  |  |  |                     sys.deactivate_stack_trampoline() | 
					
						
							|  |  |  |                     foo() | 
					
						
							|  |  |  |                     sys.activate_stack_trampoline("perf") | 
					
						
							|  |  |  |                     spam() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 def baz(): | 
					
						
							|  |  |  |                     bar() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 sys.activate_stack_trampoline("perf") | 
					
						
							|  |  |  |                 baz() | 
					
						
							|  |  |  |                 """
 | 
					
						
							|  |  |  |         with temp_dir() as script_dir: | 
					
						
							|  |  |  |             script = make_script(script_dir, "perftest", code) | 
					
						
							|  |  |  |             with subprocess.Popen( | 
					
						
							|  |  |  |                 [sys.executable, script], | 
					
						
							| 
									
										
										
										
											2022-09-01 02:47:40 -07:00
										 |  |  |                 text=True, | 
					
						
							| 
									
										
										
										
											2022-08-30 18:11:18 +01:00
										 |  |  |                 stderr=subprocess.PIPE, | 
					
						
							|  |  |  |                 stdout=subprocess.PIPE, | 
					
						
							|  |  |  |             ) as process: | 
					
						
							|  |  |  |                 stdout, stderr = process.communicate() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         self.assertEqual(stderr, "") | 
					
						
							|  |  |  |         self.assertEqual(stdout, "") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         perf_file = pathlib.Path(f"/tmp/perf-{process.pid}.map") | 
					
						
							|  |  |  |         self.assertTrue(perf_file.exists()) | 
					
						
							|  |  |  |         perf_file_contents = perf_file.read_text() | 
					
						
							|  |  |  |         self.assertNotIn(f"py::foo:{script}", perf_file_contents) | 
					
						
							|  |  |  |         self.assertIn(f"py::spam:{script}", perf_file_contents) | 
					
						
							|  |  |  |         self.assertIn(f"py::bar:{script}", perf_file_contents) | 
					
						
							|  |  |  |         self.assertIn(f"py::baz:{script}", perf_file_contents) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def test_sys_api_with_existing_trampoline(self): | 
					
						
							|  |  |  |         code = """if 1:
 | 
					
						
							|  |  |  |                 import sys | 
					
						
							|  |  |  |                 sys.activate_stack_trampoline("perf") | 
					
						
							|  |  |  |                 sys.activate_stack_trampoline("perf") | 
					
						
							|  |  |  |                 """
 | 
					
						
							|  |  |  |         assert_python_ok("-c", code) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def test_sys_api_with_invalid_trampoline(self): | 
					
						
							|  |  |  |         code = """if 1:
 | 
					
						
							|  |  |  |                 import sys | 
					
						
							|  |  |  |                 sys.activate_stack_trampoline("invalid") | 
					
						
							|  |  |  |                 """
 | 
					
						
							|  |  |  |         rc, out, err = assert_python_failure("-c", code) | 
					
						
							|  |  |  |         self.assertIn("invalid backend: invalid", err.decode()) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def test_sys_api_get_status(self): | 
					
						
							|  |  |  |         code = """if 1:
 | 
					
						
							|  |  |  |                 import sys | 
					
						
							|  |  |  |                 sys.activate_stack_trampoline("perf") | 
					
						
							|  |  |  |                 assert sys.is_stack_trampoline_active() is True | 
					
						
							|  |  |  |                 sys.deactivate_stack_trampoline() | 
					
						
							|  |  |  |                 assert sys.is_stack_trampoline_active() is False | 
					
						
							|  |  |  |                 """
 | 
					
						
							|  |  |  |         assert_python_ok("-c", code) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-05-05 03:07:29 +02:00
										 |  |  | def is_unwinding_reliable_with_frame_pointers(): | 
					
						
							| 
									
										
										
										
											2022-08-30 18:11:18 +01:00
										 |  |  |     cflags = sysconfig.get_config_var("PY_CORE_CFLAGS") | 
					
						
							|  |  |  |     if not cflags: | 
					
						
							|  |  |  |         return False | 
					
						
							| 
									
										
										
										
											2024-05-01 08:05:53 -07:00
										 |  |  |     return "no-omit-frame-pointer" in cflags and "_Py_JIT" not in cflags | 
					
						
							| 
									
										
										
										
											2022-08-30 18:11:18 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def perf_command_works(): | 
					
						
							|  |  |  |     try: | 
					
						
							|  |  |  |         cmd = ["perf", "--help"] | 
					
						
							| 
									
										
										
										
											2022-09-01 02:47:40 -07:00
										 |  |  |         stdout = subprocess.check_output(cmd, text=True) | 
					
						
							| 
									
										
										
										
											2022-08-30 18:11:18 +01:00
										 |  |  |     except (subprocess.SubprocessError, OSError): | 
					
						
							|  |  |  |         return False | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # perf version does not return a version number on Fedora. Use presence | 
					
						
							|  |  |  |     # of "perf.data" in help as indicator that it's perf from Linux tools. | 
					
						
							|  |  |  |     if "perf.data" not in stdout: | 
					
						
							|  |  |  |         return False | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # Check that we can run a simple perf run | 
					
						
							|  |  |  |     with temp_dir() as script_dir: | 
					
						
							|  |  |  |         try: | 
					
						
							|  |  |  |             output_file = script_dir + "/perf_output.perf" | 
					
						
							|  |  |  |             cmd = ( | 
					
						
							|  |  |  |                 "perf", | 
					
						
							|  |  |  |                 "record", | 
					
						
							|  |  |  |                 "-g", | 
					
						
							|  |  |  |                 "--call-graph=fp", | 
					
						
							|  |  |  |                 "-o", | 
					
						
							|  |  |  |                 output_file, | 
					
						
							|  |  |  |                 "--", | 
					
						
							|  |  |  |                 sys.executable, | 
					
						
							|  |  |  |                 "-c", | 
					
						
							|  |  |  |                 'print("hello")', | 
					
						
							|  |  |  |             ) | 
					
						
							|  |  |  |             stdout = subprocess.check_output( | 
					
						
							| 
									
										
										
										
											2022-09-01 02:47:40 -07:00
										 |  |  |                 cmd, cwd=script_dir, text=True, stderr=subprocess.STDOUT | 
					
						
							| 
									
										
										
										
											2022-08-30 18:11:18 +01:00
										 |  |  |             ) | 
					
						
							|  |  |  |         except (subprocess.SubprocessError, OSError): | 
					
						
							|  |  |  |             return False | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if "hello" not in stdout: | 
					
						
							|  |  |  |             return False | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return True | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-05-05 03:07:29 +02:00
										 |  |  | def run_perf(cwd, *args, use_jit=False, **env_vars): | 
					
						
							| 
									
										
										
										
											2022-08-30 18:11:18 +01:00
										 |  |  |     if env_vars: | 
					
						
							|  |  |  |         env = os.environ.copy() | 
					
						
							|  |  |  |         env.update(env_vars) | 
					
						
							|  |  |  |     else: | 
					
						
							|  |  |  |         env = None | 
					
						
							|  |  |  |     output_file = cwd + "/perf_output.perf" | 
					
						
							| 
									
										
										
										
											2024-05-05 03:07:29 +02:00
										 |  |  |     if not use_jit: | 
					
						
							|  |  |  |         base_cmd = ("perf", "record", "-g", "--call-graph=fp", "-o", output_file, "--") | 
					
						
							|  |  |  |     else: | 
					
						
							|  |  |  |         base_cmd = ( | 
					
						
							|  |  |  |             "perf", | 
					
						
							|  |  |  |             "record", | 
					
						
							|  |  |  |             "-g", | 
					
						
							|  |  |  |             "--call-graph=dwarf,65528", | 
					
						
							|  |  |  |             "-F99", | 
					
						
							|  |  |  |             "-k1", | 
					
						
							|  |  |  |             "-o", | 
					
						
							|  |  |  |             output_file, | 
					
						
							|  |  |  |             "--", | 
					
						
							|  |  |  |         ) | 
					
						
							| 
									
										
										
										
											2022-08-30 18:11:18 +01:00
										 |  |  |     proc = subprocess.run( | 
					
						
							|  |  |  |         base_cmd + args, | 
					
						
							|  |  |  |         stdout=subprocess.PIPE, | 
					
						
							|  |  |  |         stderr=subprocess.PIPE, | 
					
						
							|  |  |  |         env=env, | 
					
						
							|  |  |  |     ) | 
					
						
							|  |  |  |     if proc.returncode: | 
					
						
							| 
									
										
										
										
											2024-05-05 03:07:29 +02:00
										 |  |  |         print(proc.stderr, file=sys.stderr) | 
					
						
							| 
									
										
										
										
											2022-08-30 18:11:18 +01:00
										 |  |  |         raise ValueError(f"Perf failed with return code {proc.returncode}") | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-05-05 03:07:29 +02:00
										 |  |  |     if use_jit: | 
					
						
							|  |  |  |         jit_output_file = cwd + "/jit_output.dump" | 
					
						
							|  |  |  |         command = ("perf", "inject", "-j", "-i", output_file, "-o", jit_output_file) | 
					
						
							|  |  |  |         proc = subprocess.run( | 
					
						
							|  |  |  |             command, stderr=subprocess.PIPE, stdout=subprocess.PIPE, env=env | 
					
						
							|  |  |  |         ) | 
					
						
							|  |  |  |         if proc.returncode: | 
					
						
							|  |  |  |             print(proc.stderr) | 
					
						
							|  |  |  |             raise ValueError(f"Perf failed with return code {proc.returncode}") | 
					
						
							|  |  |  |         # Copy the jit_output_file to the output_file | 
					
						
							|  |  |  |         os.rename(jit_output_file, output_file) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-08-30 18:11:18 +01:00
										 |  |  |     base_cmd = ("perf", "script") | 
					
						
							|  |  |  |     proc = subprocess.run( | 
					
						
							|  |  |  |         ("perf", "script", "-i", output_file), | 
					
						
							|  |  |  |         stdout=subprocess.PIPE, | 
					
						
							|  |  |  |         stderr=subprocess.PIPE, | 
					
						
							|  |  |  |         env=env, | 
					
						
							|  |  |  |         check=True, | 
					
						
							|  |  |  |     ) | 
					
						
							|  |  |  |     return proc.stdout.decode("utf-8", "replace"), proc.stderr.decode( | 
					
						
							|  |  |  |         "utf-8", "replace" | 
					
						
							|  |  |  |     ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-05-05 03:07:29 +02:00
										 |  |  | class TestPerfProfilerMixin: | 
					
						
							|  |  |  |     def run_perf(self, script_dir, perf_mode, script): | 
					
						
							|  |  |  |         raise NotImplementedError() | 
					
						
							| 
									
										
										
										
											2022-08-30 18:11:18 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |     def test_python_calls_appear_in_the_stack_if_perf_activated(self): | 
					
						
							|  |  |  |         with temp_dir() as script_dir: | 
					
						
							|  |  |  |             code = """if 1:
 | 
					
						
							|  |  |  |                 def foo(n): | 
					
						
							|  |  |  |                     x = 0 | 
					
						
							|  |  |  |                     for i in range(n): | 
					
						
							|  |  |  |                         x += i | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 def bar(n): | 
					
						
							|  |  |  |                     foo(n) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 def baz(n): | 
					
						
							|  |  |  |                     bar(n) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 baz(10000000) | 
					
						
							|  |  |  |                 """
 | 
					
						
							|  |  |  |             script = make_script(script_dir, "perftest", code) | 
					
						
							| 
									
										
										
										
											2024-05-05 03:07:29 +02:00
										 |  |  |             stdout, stderr = self.run_perf(script_dir, script) | 
					
						
							| 
									
										
										
										
											2022-08-30 18:11:18 +01:00
										 |  |  |             self.assertEqual(stderr, "") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             self.assertIn(f"py::foo:{script}", stdout) | 
					
						
							|  |  |  |             self.assertIn(f"py::bar:{script}", stdout) | 
					
						
							|  |  |  |             self.assertIn(f"py::baz:{script}", stdout) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-05-05 03:07:29 +02:00
										 |  |  |     def test_python_calls_do_not_appear_in_the_stack_if_perf_deactivated(self): | 
					
						
							| 
									
										
										
										
											2022-08-30 18:11:18 +01:00
										 |  |  |         with temp_dir() as script_dir: | 
					
						
							|  |  |  |             code = """if 1:
 | 
					
						
							|  |  |  |                 def foo(n): | 
					
						
							|  |  |  |                     x = 0 | 
					
						
							|  |  |  |                     for i in range(n): | 
					
						
							|  |  |  |                         x += i | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 def bar(n): | 
					
						
							|  |  |  |                     foo(n) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 def baz(n): | 
					
						
							|  |  |  |                     bar(n) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 baz(10000000) | 
					
						
							|  |  |  |                 """
 | 
					
						
							|  |  |  |             script = make_script(script_dir, "perftest", code) | 
					
						
							| 
									
										
										
										
											2024-05-05 03:07:29 +02:00
										 |  |  |             stdout, stderr = self.run_perf( | 
					
						
							|  |  |  |                 script_dir, script, activate_trampoline=False | 
					
						
							|  |  |  |             ) | 
					
						
							| 
									
										
										
										
											2022-08-30 18:11:18 +01:00
										 |  |  |             self.assertEqual(stderr, "") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             self.assertNotIn(f"py::foo:{script}", stdout) | 
					
						
							|  |  |  |             self.assertNotIn(f"py::bar:{script}", stdout) | 
					
						
							|  |  |  |             self.assertNotIn(f"py::baz:{script}", stdout) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-05-05 03:07:29 +02:00
										 |  |  | @unittest.skipUnless(perf_command_works(), "perf command doesn't work") | 
					
						
							|  |  |  | @unittest.skipUnless( | 
					
						
							|  |  |  |     is_unwinding_reliable_with_frame_pointers(), | 
					
						
							|  |  |  |     "Unwinding is unreliable with frame pointers", | 
					
						
							|  |  |  | ) | 
					
						
							|  |  |  | class TestPerfProfiler(unittest.TestCase, TestPerfProfilerMixin): | 
					
						
							|  |  |  |     def run_perf(self, script_dir, script, activate_trampoline=True): | 
					
						
							|  |  |  |         if activate_trampoline: | 
					
						
							|  |  |  |             return run_perf(script_dir, sys.executable, "-Xperf", script) | 
					
						
							|  |  |  |         return run_perf(script_dir, sys.executable, script) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def setUp(self): | 
					
						
							|  |  |  |         super().setUp() | 
					
						
							|  |  |  |         self.perf_files = set(pathlib.Path("/tmp/").glob("perf-*.map")) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def tearDown(self) -> None: | 
					
						
							|  |  |  |         super().tearDown() | 
					
						
							|  |  |  |         files_to_delete = ( | 
					
						
							|  |  |  |             set(pathlib.Path("/tmp/").glob("perf-*.map")) - self.perf_files | 
					
						
							|  |  |  |         ) | 
					
						
							|  |  |  |         for file in files_to_delete: | 
					
						
							|  |  |  |             file.unlink() | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-10-26 20:57:29 -07:00
										 |  |  |     def test_pre_fork_compile(self): | 
					
						
							|  |  |  |         code = """if 1:
 | 
					
						
							|  |  |  |                 import sys | 
					
						
							|  |  |  |                 import os | 
					
						
							|  |  |  |                 import sysconfig | 
					
						
							|  |  |  |                 from _testinternalcapi import ( | 
					
						
							|  |  |  |                     compile_perf_trampoline_entry, | 
					
						
							|  |  |  |                     perf_trampoline_set_persist_after_fork, | 
					
						
							|  |  |  |                 ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 def foo_fork(): | 
					
						
							|  |  |  |                     pass | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 def bar_fork(): | 
					
						
							|  |  |  |                     foo_fork() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 def foo(): | 
					
						
							| 
									
										
										
										
											2024-05-05 03:07:29 +02:00
										 |  |  |                     import time; time.sleep(1) | 
					
						
							| 
									
										
										
										
											2023-10-26 20:57:29 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |                 def bar(): | 
					
						
							|  |  |  |                     foo() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 def compile_trampolines_for_all_functions(): | 
					
						
							|  |  |  |                     perf_trampoline_set_persist_after_fork(1) | 
					
						
							|  |  |  |                     for _, obj in globals().items(): | 
					
						
							|  |  |  |                         if callable(obj) and hasattr(obj, '__code__'): | 
					
						
							|  |  |  |                             compile_perf_trampoline_entry(obj.__code__) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 if __name__ == "__main__": | 
					
						
							|  |  |  |                     compile_trampolines_for_all_functions() | 
					
						
							|  |  |  |                     pid = os.fork() | 
					
						
							|  |  |  |                     if pid == 0: | 
					
						
							|  |  |  |                         print(os.getpid()) | 
					
						
							|  |  |  |                         bar_fork() | 
					
						
							|  |  |  |                     else: | 
					
						
							|  |  |  |                         bar() | 
					
						
							|  |  |  |                 """
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         with temp_dir() as script_dir: | 
					
						
							|  |  |  |             script = make_script(script_dir, "perftest", code) | 
					
						
							|  |  |  |             with subprocess.Popen( | 
					
						
							|  |  |  |                 [sys.executable, "-Xperf", script], | 
					
						
							|  |  |  |                 universal_newlines=True, | 
					
						
							|  |  |  |                 stderr=subprocess.PIPE, | 
					
						
							|  |  |  |                 stdout=subprocess.PIPE, | 
					
						
							|  |  |  |             ) as process: | 
					
						
							|  |  |  |                 stdout, stderr = process.communicate() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         self.assertEqual(process.returncode, 0) | 
					
						
							|  |  |  |         self.assertNotIn("Error:", stderr) | 
					
						
							|  |  |  |         child_pid = int(stdout.strip()) | 
					
						
							|  |  |  |         perf_file = pathlib.Path(f"/tmp/perf-{process.pid}.map") | 
					
						
							|  |  |  |         perf_child_file = pathlib.Path(f"/tmp/perf-{child_pid}.map") | 
					
						
							|  |  |  |         self.assertTrue(perf_file.exists()) | 
					
						
							|  |  |  |         self.assertTrue(perf_child_file.exists()) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         perf_file_contents = perf_file.read_text() | 
					
						
							|  |  |  |         self.assertIn(f"py::foo:{script}", perf_file_contents) | 
					
						
							|  |  |  |         self.assertIn(f"py::bar:{script}", perf_file_contents) | 
					
						
							|  |  |  |         self.assertIn(f"py::foo_fork:{script}", perf_file_contents) | 
					
						
							|  |  |  |         self.assertIn(f"py::bar_fork:{script}", perf_file_contents) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         child_perf_file_contents = perf_child_file.read_text() | 
					
						
							|  |  |  |         self.assertIn(f"py::foo_fork:{script}", child_perf_file_contents) | 
					
						
							|  |  |  |         self.assertIn(f"py::bar_fork:{script}", child_perf_file_contents) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # Pre-compiled perf-map entries of a forked process must be | 
					
						
							|  |  |  |         # identical in both the parent and child perf-map files. | 
					
						
							|  |  |  |         perf_file_lines = perf_file_contents.split("\n") | 
					
						
							|  |  |  |         for line in perf_file_lines: | 
					
						
							| 
									
										
										
										
											2024-05-05 03:07:29 +02:00
										 |  |  |             if f"py::foo_fork:{script}" in line or f"py::bar_fork:{script}" in line: | 
					
						
							| 
									
										
										
										
											2023-10-26 20:57:29 -07:00
										 |  |  |                 self.assertIn(line, child_perf_file_contents) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-05-07 20:41:07 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | def _is_perf_vesion_at_least(major, minor): | 
					
						
							|  |  |  |     # The output of perf --version looks like "perf version 6.7-3" but | 
					
						
							|  |  |  |     # it can also be perf version "perf version 5.15.143" | 
					
						
							| 
									
										
										
										
											2024-05-05 03:07:29 +02:00
										 |  |  |     try: | 
					
						
							| 
									
										
										
										
											2024-05-07 20:41:07 +01:00
										 |  |  |         output = subprocess.check_output(["perf", "--version"], text=True) | 
					
						
							|  |  |  |     except (subprocess.CalledProcessError, FileNotFoundError): | 
					
						
							| 
									
										
										
										
											2024-05-05 03:07:29 +02:00
										 |  |  |         return False | 
					
						
							| 
									
										
										
										
											2024-05-07 20:41:07 +01:00
										 |  |  |     version = output.split()[2] | 
					
						
							|  |  |  |     version = version.split("-")[0] | 
					
						
							| 
									
										
										
										
											2024-05-05 03:07:29 +02:00
										 |  |  |     version = version.split(".") | 
					
						
							| 
									
										
										
										
											2024-05-07 20:41:07 +01:00
										 |  |  |     version = tuple(map(int, version)) | 
					
						
							|  |  |  |     return version >= (major, minor) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-05-05 03:07:29 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | @unittest.skipUnless(perf_command_works(), "perf command doesn't work") | 
					
						
							| 
									
										
										
										
											2024-05-07 20:41:07 +01:00
										 |  |  | @unittest.skipUnless(_is_perf_vesion_at_least(6, 6), "perf command may not work due to a perf bug") | 
					
						
							| 
									
										
										
										
											2024-05-05 03:07:29 +02:00
										 |  |  | class TestPerfProfilerWithDwarf(unittest.TestCase, TestPerfProfilerMixin): | 
					
						
							|  |  |  |     def run_perf(self, script_dir, script, activate_trampoline=True): | 
					
						
							|  |  |  |         if activate_trampoline: | 
					
						
							|  |  |  |             return run_perf( | 
					
						
							| 
									
										
										
										
											2024-05-07 13:47:21 +02:00
										 |  |  |                 script_dir, sys.executable, "-Xperf_jit", script, use_jit=True | 
					
						
							| 
									
										
										
										
											2024-05-05 03:07:29 +02:00
										 |  |  |             ) | 
					
						
							|  |  |  |         return run_perf(script_dir, sys.executable, script, use_jit=True) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def setUp(self): | 
					
						
							|  |  |  |         super().setUp() | 
					
						
							|  |  |  |         self.perf_files = set(pathlib.Path("/tmp/").glob("jit*.dump")) | 
					
						
							|  |  |  |         self.perf_files |= set(pathlib.Path("/tmp/").glob("jitted-*.so")) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def tearDown(self) -> None: | 
					
						
							|  |  |  |         super().tearDown() | 
					
						
							|  |  |  |         files_to_delete = set(pathlib.Path("/tmp/").glob("jit*.dump")) | 
					
						
							|  |  |  |         files_to_delete |= set(pathlib.Path("/tmp/").glob("jitted-*.so")) | 
					
						
							|  |  |  |         files_to_delete = files_to_delete - self.perf_files | 
					
						
							|  |  |  |         for file in files_to_delete: | 
					
						
							|  |  |  |             file.unlink() | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-08-30 18:11:18 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | if __name__ == "__main__": | 
					
						
							|  |  |  |     unittest.main() |