@@ -34,6 +34,21 @@ def supports_trampoline_profiling():
3434 raise unittest .SkipTest ("perf trampoline profiling not supported" )
3535
3636
37+ def _perf_env (** env_vars ):
38+ env = os .environ .copy ()
39+ # Keep perf's output stable regardless of the builder's perf config.
40+ env .update (
41+ {
42+ "DEBUGINFOD_URLS" : "" ,
43+ "PERF_CONFIG" : os .devnull ,
44+ }
45+ )
46+ if env_vars :
47+ env .update (env_vars )
48+ env ["PYTHON_JIT" ] = "0"
49+ return env
50+
51+
3752class TestPerfTrampoline (unittest .TestCase ):
3853 def setUp (self ):
3954 super ().setUp ()
@@ -63,13 +78,12 @@ def baz():
6378 """
6479 with temp_dir () as script_dir :
6580 script = make_script (script_dir , "perftest" , code )
66- env = {** os .environ , "PYTHON_JIT" : "0" }
6781 with subprocess .Popen (
6882 [sys .executable , "-Xperf" , script ],
6983 text = True ,
7084 stderr = subprocess .PIPE ,
7185 stdout = subprocess .PIPE ,
72- env = env ,
86+ env = _perf_env () ,
7387 ) as process :
7488 stdout , stderr = process .communicate ()
7589
@@ -132,13 +146,12 @@ def baz():
132146 """
133147 with temp_dir () as script_dir :
134148 script = make_script (script_dir , "perftest" , code )
135- env = {** os .environ , "PYTHON_JIT" : "0" }
136149 with subprocess .Popen (
137150 [sys .executable , "-Xperf" , script ],
138151 text = True ,
139152 stderr = subprocess .PIPE ,
140153 stdout = subprocess .PIPE ,
141- env = env ,
154+ env = _perf_env () ,
142155 ) as process :
143156 stdout , stderr = process .communicate ()
144157
@@ -198,13 +211,12 @@ def test_trampoline_works_after_fork_with_many_code_objects(self):
198211 """
199212 with temp_dir () as script_dir :
200213 script = make_script (script_dir , "perftest" , code )
201- env = {** os .environ , "PYTHON_JIT" : "0" }
202214 with subprocess .Popen (
203215 [sys .executable , "-Xperf" , script ],
204216 text = True ,
205217 stderr = subprocess .PIPE ,
206218 stdout = subprocess .PIPE ,
207- env = env ,
219+ env = _perf_env () ,
208220 ) as process :
209221 stdout , stderr = process .communicate ()
210222
@@ -242,13 +254,12 @@ def baz():
242254 code = set_eval_hook + code
243255 with temp_dir () as script_dir :
244256 script = make_script (script_dir , "perftest" , code )
245- env = {** os .environ , "PYTHON_JIT" : "0" }
246257 with subprocess .Popen (
247258 [sys .executable , script ],
248259 text = True ,
249260 stderr = subprocess .PIPE ,
250261 stdout = subprocess .PIPE ,
251- env = env ,
262+ env = _perf_env () ,
252263 ) as process :
253264 stdout , stderr = process .communicate ()
254265
@@ -345,9 +356,12 @@ def perf_command_works():
345356 "-c" ,
346357 'print("hello")' ,
347358 )
348- env = {** os .environ , "PYTHON_JIT" : "0" }
349359 stdout = subprocess .check_output (
350- cmd , cwd = script_dir , text = True , stderr = subprocess .STDOUT , env = env
360+ cmd ,
361+ cwd = script_dir ,
362+ text = True ,
363+ stderr = subprocess .STDOUT ,
364+ env = _perf_env (),
351365 )
352366 except (subprocess .SubprocessError , OSError ):
353367 return False
@@ -359,43 +373,49 @@ def perf_command_works():
359373
360374
361375def run_perf (cwd , * args , use_jit = False , ** env_vars ):
362- env = os .environ .copy ()
363- if env_vars :
364- env .update (env_vars )
365- env ["PYTHON_JIT" ] = "0"
376+ env = _perf_env (** env_vars )
366377 output_file = cwd + "/perf_output.perf"
367- if not use_jit :
368- base_cmd = (
369- "perf" ,
370- "record" ,
371- "--no-buildid" ,
372- "--no-buildid-cache" ,
373- "-g" ,
374- "--call-graph=fp" ,
375- "-o" , output_file ,
376- "--"
377- )
378+ base_cmd = [
379+ "perf" ,
380+ "record" ,
381+ "--no-buildid" ,
382+ "--no-buildid-cache" ,
383+ "-g" ,
384+ "--call-graph=dwarf,65528" if use_jit else "--call-graph=fp" ,
385+ ]
386+ if use_jit :
387+ perf_commands = []
388+ # Some builders have low perf_event_mlock_kb limits.
389+ mmap_sizes = ("4M" , "2M" , "1M" , "512K" , "256K" , "128K" , None )
390+ for mmap_size in mmap_sizes :
391+ command = base_cmd .copy ()
392+ if mmap_size is not None :
393+ command += ["-F99" , "-k1" , "-m" , mmap_size ]
394+ else :
395+ command += ["-F99" , "-k1" ]
396+ command += ["-o" , output_file , "--" ]
397+ perf_commands .append (command )
378398 else :
379- base_cmd = (
380- "perf" ,
381- "record" ,
382- "--no-buildid" ,
383- "--no-buildid-cache" ,
384- "-g" ,
385- "--call-graph=dwarf,65528" ,
386- "-F99" ,
387- "-k1" ,
388- "-o" ,
389- output_file ,
390- "--" ,
399+ perf_commands = [base_cmd + ["-o" , output_file , "--" ]]
400+
401+ mmap_pages_error = "try again with a smaller value of -m/--mmap_pages"
402+ for index , base_cmd in enumerate (perf_commands ):
403+ proc = subprocess .run (
404+ base_cmd + list (args ),
405+ stdout = subprocess .PIPE ,
406+ stderr = subprocess .PIPE ,
407+ env = env ,
408+ text = True ,
391409 )
392- proc = subprocess .run (
393- base_cmd + args ,
394- stdout = subprocess .PIPE ,
395- stderr = subprocess .PIPE ,
396- env = env ,
397- text = True ,
398- )
410+ if (
411+ proc .returncode
412+ and use_jit
413+ and index != len (perf_commands ) - 1
414+ and mmap_pages_error in proc .stderr
415+ ):
416+ continue
417+ break
418+
399419 if proc .returncode :
400420 print (proc .stderr , file = sys .stderr )
401421 raise ValueError (f"Perf failed with return code { proc .returncode } " )
@@ -425,54 +445,77 @@ def run_perf(cwd, *args, use_jit=False, **env_vars):
425445
426446
427447class TestPerfProfilerMixin :
428- def run_perf (self , script_dir , perf_mode , script ):
448+ PERF_CAPTURE_ATTEMPTS = 3
449+
450+ def run_perf (self , script_dir , script , activate_trampoline = True ):
429451 raise NotImplementedError ()
430452
453+ def run_perf_with_retries (
454+ self , script_dir , script , expected_symbols = (), activate_trampoline = True
455+ ):
456+ stdout = stderr = ""
457+ for _ in range (self .PERF_CAPTURE_ATTEMPTS ):
458+ stdout , stderr = self .run_perf (
459+ script_dir , script , activate_trampoline = activate_trampoline
460+ )
461+ if activate_trampoline and any (
462+ symbol not in stdout for symbol in expected_symbols
463+ ):
464+ continue
465+ break
466+ return stdout , stderr
467+
431468 def test_python_calls_appear_in_the_stack_if_perf_activated (self ):
432469 with temp_dir () as script_dir :
433470 code = """if 1:
471+ from itertools import repeat
472+
434473 def foo(n):
435- x = 0
436- for i in range(n):
437- x += i
474+ for _ in repeat(None, n):
475+ pass
438476
439477 def bar(n):
440478 foo(n)
441479
442480 def baz(n):
443481 bar(n)
444482
445- baz(10000000 )
483+ baz(40000000 )
446484 """
447485 script = make_script (script_dir , "perftest" , code )
448- stdout , stderr = self .run_perf (script_dir , script )
449- self .assertEqual (stderr , "" )
486+ expected_symbols = [
487+ f"py::foo:{ script } " ,
488+ f"py::bar:{ script } " ,
489+ f"py::baz:{ script } " ,
490+ ]
491+ stdout , _ = self .run_perf_with_retries (
492+ script_dir , script , expected_symbols
493+ )
450494
451- self .assertIn (f"py::foo:{ script } " , stdout )
452- self .assertIn (f"py::bar:{ script } " , stdout )
453- self .assertIn (f"py::baz:{ script } " , stdout )
495+ for expected_symbol in expected_symbols :
496+ self .assertIn (expected_symbol , stdout )
454497
455498 def test_python_calls_do_not_appear_in_the_stack_if_perf_deactivated (self ):
456499 with temp_dir () as script_dir :
457500 code = """if 1:
501+ from itertools import repeat
502+
458503 def foo(n):
459- x = 0
460- for i in range(n):
461- x += i
504+ for _ in repeat(None, n):
505+ pass
462506
463507 def bar(n):
464508 foo(n)
465509
466510 def baz(n):
467511 bar(n)
468512
469- baz(10000000 )
513+ baz(40000000 )
470514 """
471515 script = make_script (script_dir , "perftest" , code )
472- stdout , stderr = self .run_perf (
516+ stdout , _ = self .run_perf_with_retries (
473517 script_dir , script , activate_trampoline = False
474518 )
475- self .assertEqual (stderr , "" )
476519
477520 self .assertNotIn (f"py::foo:{ script } " , stdout )
478521 self .assertNotIn (f"py::bar:{ script } " , stdout )
@@ -542,13 +585,12 @@ def compile_trampolines_for_all_functions():
542585
543586 with temp_dir () as script_dir :
544587 script = make_script (script_dir , "perftest" , code )
545- env = {** os .environ , "PYTHON_JIT" : "0" }
546588 with subprocess .Popen (
547589 [sys .executable , "-Xperf" , script ],
548590 universal_newlines = True ,
549591 stderr = subprocess .PIPE ,
550592 stdout = subprocess .PIPE ,
551- env = env ,
593+ env = _perf_env () ,
552594 ) as process :
553595 stdout , stderr = process .communicate ()
554596
0 commit comments