55import sysconfig
66import os
77import pathlib
8+ import shutil
89from test import support
910from test .support .script_helper import (
1011 make_script ,
@@ -76,14 +77,27 @@ def baz():
7677 perf_file = pathlib .Path (f"/tmp/perf-{ process .pid } .map" )
7778 self .assertTrue (perf_file .exists ())
7879 perf_file_contents = perf_file .read_text ()
79- perf_lines = perf_file_contents .splitlines ();
80- expected_symbols = [f"py::foo:{ script } " , f"py::bar:{ script } " , f"py::baz:{ script } " ]
80+ perf_lines = perf_file_contents .splitlines ()
81+ expected_symbols = [
82+ f"py::foo:{ script } " ,
83+ f"py::bar:{ script } " ,
84+ f"py::baz:{ script } " ,
85+ ]
8186 for expected_symbol in expected_symbols :
82- perf_line = next ((line for line in perf_lines if expected_symbol in line ), None )
83- self .assertIsNotNone (perf_line , f"Could not find { expected_symbol } in perf file" )
87+ perf_line = next (
88+ (line for line in perf_lines if expected_symbol in line ), None
89+ )
90+ self .assertIsNotNone (
91+ perf_line , f"Could not find { expected_symbol } in perf file"
92+ )
8493 perf_addr = perf_line .split (" " )[0 ]
85- self .assertFalse (perf_addr .startswith ("0x" ), "Address should not be prefixed with 0x" )
86- self .assertTrue (set (perf_addr ).issubset (string .hexdigits ), "Address should contain only hex characters" )
94+ self .assertFalse (
95+ perf_addr .startswith ("0x" ), "Address should not be prefixed with 0x"
96+ )
97+ self .assertTrue (
98+ set (perf_addr ).issubset (string .hexdigits ),
99+ "Address should contain only hex characters" ,
100+ )
87101
88102 def test_trampoline_works_with_forks (self ):
89103 code = """if 1:
@@ -212,7 +226,7 @@ def test_sys_api_get_status(self):
212226 assert_python_ok ("-c" , code )
213227
214228
215- def is_unwinding_reliable ():
229+ def is_unwinding_reliable_with_frame_pointers ():
216230 cflags = sysconfig .get_config_var ("PY_CORE_CFLAGS" )
217231 if not cflags :
218232 return False
@@ -259,24 +273,49 @@ def perf_command_works():
259273 return True
260274
261275
262- def run_perf (cwd , * args , ** env_vars ):
276+ def run_perf (cwd , * args , use_jit = False , ** env_vars ):
263277 if env_vars :
264278 env = os .environ .copy ()
265279 env .update (env_vars )
266280 else :
267281 env = None
268282 output_file = cwd + "/perf_output.perf"
269- base_cmd = ("perf" , "record" , "-g" , "--call-graph=fp" , "-o" , output_file , "--" )
283+ if not use_jit :
284+ base_cmd = ("perf" , "record" , "-g" , "--call-graph=fp" , "-o" , output_file , "--" )
285+ else :
286+ base_cmd = (
287+ "perf" ,
288+ "record" ,
289+ "-g" ,
290+ "--call-graph=dwarf,65528" ,
291+ "-F99" ,
292+ "-k1" ,
293+ "-o" ,
294+ output_file ,
295+ "--" ,
296+ )
270297 proc = subprocess .run (
271298 base_cmd + args ,
272299 stdout = subprocess .PIPE ,
273300 stderr = subprocess .PIPE ,
274301 env = env ,
275302 )
276303 if proc .returncode :
277- print (proc .stderr )
304+ print (proc .stderr , file = sys . stderr )
278305 raise ValueError (f"Perf failed with return code { proc .returncode } " )
279306
307+ if use_jit :
308+ jit_output_file = cwd + "/jit_output.dump"
309+ command = ("perf" , "inject" , "-j" , "-i" , output_file , "-o" , jit_output_file )
310+ proc = subprocess .run (
311+ command , stderr = subprocess .PIPE , stdout = subprocess .PIPE , env = env
312+ )
313+ if proc .returncode :
314+ print (proc .stderr )
315+ raise ValueError (f"Perf failed with return code { proc .returncode } " )
316+ # Copy the jit_output_file to the output_file
317+ os .rename (jit_output_file , output_file )
318+
280319 base_cmd = ("perf" , "script" )
281320 proc = subprocess .run (
282321 ("perf" , "script" , "-i" , output_file ),
@@ -290,20 +329,9 @@ def run_perf(cwd, *args, **env_vars):
290329 )
291330
292331
293- @unittest .skipUnless (perf_command_works (), "perf command doesn't work" )
294- @unittest .skipUnless (is_unwinding_reliable (), "Unwinding is unreliable" )
295- class TestPerfProfiler (unittest .TestCase ):
296- def setUp (self ):
297- super ().setUp ()
298- self .perf_files = set (pathlib .Path ("/tmp/" ).glob ("perf-*.map" ))
299-
300- def tearDown (self ) -> None :
301- super ().tearDown ()
302- files_to_delete = (
303- set (pathlib .Path ("/tmp/" ).glob ("perf-*.map" )) - self .perf_files
304- )
305- for file in files_to_delete :
306- file .unlink ()
332+ class TestPerfProfilerMixin :
333+ def run_perf (self , script_dir , perf_mode , script ):
334+ raise NotImplementedError ()
307335
308336 def test_python_calls_appear_in_the_stack_if_perf_activated (self ):
309337 with temp_dir () as script_dir :
@@ -322,14 +350,14 @@ def baz(n):
322350 baz(10000000)
323351 """
324352 script = make_script (script_dir , "perftest" , code )
325- stdout , stderr = run_perf (script_dir , sys . executable , "-Xperf" , script )
353+ stdout , stderr = self . run_perf (script_dir , script )
326354 self .assertEqual (stderr , "" )
327355
328356 self .assertIn (f"py::foo:{ script } " , stdout )
329357 self .assertIn (f"py::bar:{ script } " , stdout )
330358 self .assertIn (f"py::baz:{ script } " , stdout )
331359
332- def test_python_calls_do_not_appear_in_the_stack_if_perf_activated (self ):
360+ def test_python_calls_do_not_appear_in_the_stack_if_perf_deactivated (self ):
333361 with temp_dir () as script_dir :
334362 code = """if 1:
335363 def foo(n):
@@ -346,13 +374,38 @@ def baz(n):
346374 baz(10000000)
347375 """
348376 script = make_script (script_dir , "perftest" , code )
349- stdout , stderr = run_perf (script_dir , sys .executable , script )
377+ stdout , stderr = self .run_perf (
378+ script_dir , script , activate_trampoline = False
379+ )
350380 self .assertEqual (stderr , "" )
351381
352382 self .assertNotIn (f"py::foo:{ script } " , stdout )
353383 self .assertNotIn (f"py::bar:{ script } " , stdout )
354384 self .assertNotIn (f"py::baz:{ script } " , stdout )
355385
386+ @unittest .skipUnless (perf_command_works (), "perf command doesn't work" )
387+ @unittest .skipUnless (
388+ is_unwinding_reliable_with_frame_pointers (),
389+ "Unwinding is unreliable with frame pointers" ,
390+ )
391+ class TestPerfProfiler (unittest .TestCase , TestPerfProfilerMixin ):
392+ def run_perf (self , script_dir , script , activate_trampoline = True ):
393+ if activate_trampoline :
394+ return run_perf (script_dir , sys .executable , "-Xperf" , script )
395+ return run_perf (script_dir , sys .executable , script )
396+
397+ def setUp (self ):
398+ super ().setUp ()
399+ self .perf_files = set (pathlib .Path ("/tmp/" ).glob ("perf-*.map" ))
400+
401+ def tearDown (self ) -> None :
402+ super ().tearDown ()
403+ files_to_delete = (
404+ set (pathlib .Path ("/tmp/" ).glob ("perf-*.map" )) - self .perf_files
405+ )
406+ for file in files_to_delete :
407+ file .unlink ()
408+
356409 def test_pre_fork_compile (self ):
357410 code = """if 1:
358411 import sys
@@ -370,7 +423,7 @@ def bar_fork():
370423 foo_fork()
371424
372425 def foo():
373- pass
426+ import time; time.sleep(1)
374427
375428 def bar():
376429 foo()
@@ -423,12 +476,41 @@ def compile_trampolines_for_all_functions():
423476 # identical in both the parent and child perf-map files.
424477 perf_file_lines = perf_file_contents .split ("\n " )
425478 for line in perf_file_lines :
426- if (
427- f"py::foo_fork:{ script } " in line
428- or f"py::bar_fork:{ script } " in line
429- ):
479+ if f"py::foo_fork:{ script } " in line or f"py::bar_fork:{ script } " in line :
430480 self .assertIn (line , child_perf_file_contents )
431481
482+ def _is_kernel_version_at_least (major , minor ):
483+ try :
484+ with open ("/proc/version" ) as f :
485+ version = f .readline ().split ()[2 ]
486+ except FileNotFoundError :
487+ return False
488+ version = version .split ("." )
489+ return int (version [0 ]) > major or (int (version [0 ]) == major and int (version [1 ]) >= minor )
490+
491+ @unittest .skipUnless (perf_command_works (), "perf command doesn't work" )
492+ @unittest .skipUnless (_is_kernel_version_at_least (6 , 6 ), "perf command may not work due to a perf bug" )
493+ class TestPerfProfilerWithDwarf (unittest .TestCase , TestPerfProfilerMixin ):
494+ def run_perf (self , script_dir , script , activate_trampoline = True ):
495+ if activate_trampoline :
496+ return run_perf (
497+ script_dir , sys .executable , "-Xperfjit" , script , use_jit = True
498+ )
499+ return run_perf (script_dir , sys .executable , script , use_jit = True )
500+
501+ def setUp (self ):
502+ super ().setUp ()
503+ self .perf_files = set (pathlib .Path ("/tmp/" ).glob ("jit*.dump" ))
504+ self .perf_files |= set (pathlib .Path ("/tmp/" ).glob ("jitted-*.so" ))
505+
506+ def tearDown (self ) -> None :
507+ super ().tearDown ()
508+ files_to_delete = set (pathlib .Path ("/tmp/" ).glob ("jit*.dump" ))
509+ files_to_delete |= set (pathlib .Path ("/tmp/" ).glob ("jitted-*.so" ))
510+ files_to_delete = files_to_delete - self .perf_files
511+ for file in files_to_delete :
512+ file .unlink ()
513+
432514
433515if __name__ == "__main__" :
434516 unittest .main ()
0 commit comments