5
5
import sysconfig
6
6
import os
7
7
import pathlib
8
+ import shutil
8
9
from test import support
9
10
from test .support .script_helper import (
10
11
make_script ,
@@ -76,14 +77,27 @@ def baz():
76
77
perf_file = pathlib .Path (f"/tmp/perf-{ process .pid } .map" )
77
78
self .assertTrue (perf_file .exists ())
78
79
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
+ ]
81
86
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
+ )
84
93
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
+ )
87
101
88
102
def test_trampoline_works_with_forks (self ):
89
103
code = """if 1:
@@ -212,7 +226,7 @@ def test_sys_api_get_status(self):
212
226
assert_python_ok ("-c" , code )
213
227
214
228
215
- def is_unwinding_reliable ():
229
+ def is_unwinding_reliable_with_frame_pointers ():
216
230
cflags = sysconfig .get_config_var ("PY_CORE_CFLAGS" )
217
231
if not cflags :
218
232
return False
@@ -259,24 +273,49 @@ def perf_command_works():
259
273
return True
260
274
261
275
262
- def run_perf (cwd , * args , ** env_vars ):
276
+ def run_perf (cwd , * args , use_jit = False , ** env_vars ):
263
277
if env_vars :
264
278
env = os .environ .copy ()
265
279
env .update (env_vars )
266
280
else :
267
281
env = None
268
282
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
+ )
270
297
proc = subprocess .run (
271
298
base_cmd + args ,
272
299
stdout = subprocess .PIPE ,
273
300
stderr = subprocess .PIPE ,
274
301
env = env ,
275
302
)
276
303
if proc .returncode :
277
- print (proc .stderr )
304
+ print (proc .stderr , file = sys . stderr )
278
305
raise ValueError (f"Perf failed with return code { proc .returncode } " )
279
306
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
+
280
319
base_cmd = ("perf" , "script" )
281
320
proc = subprocess .run (
282
321
("perf" , "script" , "-i" , output_file ),
@@ -290,20 +329,9 @@ def run_perf(cwd, *args, **env_vars):
290
329
)
291
330
292
331
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 ()
307
335
308
336
def test_python_calls_appear_in_the_stack_if_perf_activated (self ):
309
337
with temp_dir () as script_dir :
@@ -322,14 +350,14 @@ def baz(n):
322
350
baz(10000000)
323
351
"""
324
352
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 )
326
354
self .assertEqual (stderr , "" )
327
355
328
356
self .assertIn (f"py::foo:{ script } " , stdout )
329
357
self .assertIn (f"py::bar:{ script } " , stdout )
330
358
self .assertIn (f"py::baz:{ script } " , stdout )
331
359
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 ):
333
361
with temp_dir () as script_dir :
334
362
code = """if 1:
335
363
def foo(n):
@@ -346,7 +374,9 @@ def baz(n):
346
374
baz(10000000)
347
375
"""
348
376
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
+ )
350
380
self .assertEqual (stderr , "" )
351
381
352
382
self .assertNotIn (f"py::foo:{ script } " , stdout )
@@ -423,12 +453,56 @@ def compile_trampolines_for_all_functions():
423
453
# identical in both the parent and child perf-map files.
424
454
perf_file_lines = perf_file_contents .split ("\n " )
425
455
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
- ):
456
+ if f"py::foo_fork:{ script } " in line or f"py::bar_fork:{ script } " in line :
430
457
self .assertIn (line , child_perf_file_contents )
431
458
432
459
460
+ @unittest .skipUnless (perf_command_works (), "perf command doesn't work" )
461
+ @unittest .skipUnless (
462
+ is_unwinding_reliable_with_frame_pointers (),
463
+ "Unwinding is unreliable with frame pointers" ,
464
+ )
465
+ class TestPerfProfiler (unittest .TestCase , TestPerfProfilerMixin ):
466
+ def run_perf (self , script_dir , script , activate_trampoline = True ):
467
+ if activate_trampoline :
468
+ return run_perf (script_dir , sys .executable , "-Xperf" , script )
469
+ return run_perf (script_dir , sys .executable , script )
470
+
471
+ def setUp (self ):
472
+ super ().setUp ()
473
+ self .perf_files = set (pathlib .Path ("/tmp/" ).glob ("perf-*.map" ))
474
+
475
+ def tearDown (self ) -> None :
476
+ super ().tearDown ()
477
+ files_to_delete = (
478
+ set (pathlib .Path ("/tmp/" ).glob ("perf-*.map" )) - self .perf_files
479
+ )
480
+ for file in files_to_delete :
481
+ file .unlink ()
482
+
483
+
484
+ @unittest .skipUnless (perf_command_works (), "perf command doesn't work" )
485
+ class TestPerfProfilerWithDwarf (unittest .TestCase , TestPerfProfilerMixin ):
486
+ def run_perf (self , script_dir , script , activate_trampoline = True ):
487
+ if activate_trampoline :
488
+ return run_perf (
489
+ script_dir , sys .executable , "-Xperfjit" , script , use_jit = True
490
+ )
491
+ return run_perf (script_dir , sys .executable , script , use_jit = True )
492
+
493
+ def setUp (self ):
494
+ super ().setUp ()
495
+ self .perf_files = set (pathlib .Path ("/tmp/" ).glob ("jit*.dump" ))
496
+ self .perf_files |= set (pathlib .Path ("/tmp/" ).glob ("jitted-*.so" ))
497
+
498
+ def tearDown (self ) -> None :
499
+ super ().tearDown ()
500
+ files_to_delete = set (pathlib .Path ("/tmp/" ).glob ("jit*.dump" ))
501
+ files_to_delete |= set (pathlib .Path ("/tmp/" ).glob ("jitted-*.so" ))
502
+ files_to_delete = files_to_delete - self .perf_files
503
+ for file in files_to_delete :
504
+ file .unlink ()
505
+
506
+
433
507
if __name__ == "__main__" :
434
508
unittest .main ()
0 commit comments