Skip to content

Commit ffa440a

Browse files
miss-islingtonvstinnerpablogsal
authored
[3.13] gh-150389: Make perf profiler tests resilient (GH-150437) (GH-151501) (#151503)
[3.14] gh-150389: Make perf profiler tests resilient (GH-150437) (GH-151501) gh-150389: Make perf profiler tests resilient (GH-150437) (cherry picked from commit 638754c) (cherry picked from commit d06b473) Co-authored-by: Victor Stinner <vstinner@python.org> Co-authored-by: Pablo Galindo Salgado <Pablogsal@gmail.com>
1 parent 6c88cb9 commit ffa440a

1 file changed

Lines changed: 104 additions & 62 deletions

File tree

Lib/test/test_perf_profiler.py

Lines changed: 104 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,21 @@ def supports_trampoline_profiling():
3535
raise unittest.SkipTest("perf trampoline profiling not supported")
3636

3737

38+
def _perf_env(**env_vars):
39+
env = os.environ.copy()
40+
# Keep perf's output stable regardless of the builder's perf config.
41+
env.update(
42+
{
43+
"DEBUGINFOD_URLS": "",
44+
"PERF_CONFIG": os.devnull,
45+
}
46+
)
47+
if env_vars:
48+
env.update(env_vars)
49+
env["PYTHON_JIT"] = "0"
50+
return env
51+
52+
3853
class TestPerfTrampoline(unittest.TestCase):
3954
def setUp(self):
4055
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

@@ -133,13 +147,12 @@ def baz():
133147
"""
134148
with temp_dir() as script_dir:
135149
script = make_script(script_dir, "perftest", code)
136-
env = {**os.environ, "PYTHON_JIT": "0"}
137150
with subprocess.Popen(
138151
[sys.executable, "-Xperf", script],
139152
text=True,
140153
stderr=subprocess.PIPE,
141154
stdout=subprocess.PIPE,
142-
env=env,
155+
env=_perf_env(),
143156
) as process:
144157
stdout, stderr = process.communicate()
145158

@@ -199,13 +212,12 @@ def test_trampoline_works_after_fork_with_many_code_objects(self):
199212
"""
200213
with temp_dir() as script_dir:
201214
script = make_script(script_dir, "perftest", code)
202-
env = {**os.environ, "PYTHON_JIT": "0"}
203215
with subprocess.Popen(
204216
[sys.executable, "-Xperf", script],
205217
text=True,
206218
stderr=subprocess.PIPE,
207219
stdout=subprocess.PIPE,
208-
env=env,
220+
env=_perf_env(),
209221
) as process:
210222
stdout, stderr = process.communicate()
211223

@@ -236,13 +248,12 @@ def baz():
236248
"""
237249
with temp_dir() as script_dir:
238250
script = make_script(script_dir, "perftest", code)
239-
env = {**os.environ, "PYTHON_JIT": "0"}
240251
with subprocess.Popen(
241252
[sys.executable, script],
242253
text=True,
243254
stderr=subprocess.PIPE,
244255
stdout=subprocess.PIPE,
245-
env=env,
256+
env=_perf_env(),
246257
) as process:
247258
stdout, stderr = process.communicate()
248259

@@ -339,9 +350,12 @@ def perf_command_works():
339350
"-c",
340351
'print("hello")',
341352
)
342-
env = {**os.environ, "PYTHON_JIT": "0"}
343353
stdout = subprocess.check_output(
344-
cmd, cwd=script_dir, text=True, stderr=subprocess.STDOUT, env=env
354+
cmd,
355+
cwd=script_dir,
356+
text=True,
357+
stderr=subprocess.STDOUT,
358+
env=_perf_env(),
345359
)
346360
except (subprocess.SubprocessError, OSError):
347361
return False
@@ -353,43 +367,49 @@ def perf_command_works():
353367

354368

355369
def run_perf(cwd, *args, use_jit=False, **env_vars):
356-
env = os.environ.copy()
357-
if env_vars:
358-
env.update(env_vars)
359-
env["PYTHON_JIT"] = "0"
370+
env = _perf_env(**env_vars)
360371
output_file = cwd + "/perf_output.perf"
361-
if not use_jit:
362-
base_cmd = (
363-
"perf",
364-
"record",
365-
"--no-buildid",
366-
"--no-buildid-cache",
367-
"-g",
368-
"--call-graph=fp",
369-
"-o", output_file,
370-
"--"
371-
)
372+
base_cmd = [
373+
"perf",
374+
"record",
375+
"--no-buildid",
376+
"--no-buildid-cache",
377+
"-g",
378+
"--call-graph=dwarf,65528" if use_jit else "--call-graph=fp",
379+
]
380+
if use_jit:
381+
perf_commands = []
382+
# Some builders have low perf_event_mlock_kb limits.
383+
mmap_sizes = ("4M", "2M", "1M", "512K", "256K", "128K", None)
384+
for mmap_size in mmap_sizes:
385+
command = base_cmd.copy()
386+
if mmap_size is not None:
387+
command += ["-F99", "-k1", "-m", mmap_size]
388+
else:
389+
command += ["-F99", "-k1"]
390+
command += ["-o", output_file, "--"]
391+
perf_commands.append(command)
372392
else:
373-
base_cmd = (
374-
"perf",
375-
"record",
376-
"--no-buildid",
377-
"--no-buildid-cache",
378-
"-g",
379-
"--call-graph=dwarf,65528",
380-
"-F99",
381-
"-k1",
382-
"-o",
383-
output_file,
384-
"--",
393+
perf_commands = [base_cmd + ["-o", output_file, "--"]]
394+
395+
mmap_pages_error = "try again with a smaller value of -m/--mmap_pages"
396+
for index, base_cmd in enumerate(perf_commands):
397+
proc = subprocess.run(
398+
base_cmd + list(args),
399+
stdout=subprocess.PIPE,
400+
stderr=subprocess.PIPE,
401+
env=env,
402+
text=True,
385403
)
386-
proc = subprocess.run(
387-
base_cmd + args,
388-
stdout=subprocess.PIPE,
389-
stderr=subprocess.PIPE,
390-
env=env,
391-
text=True,
392-
)
404+
if (
405+
proc.returncode
406+
and use_jit
407+
and index != len(perf_commands) - 1
408+
and mmap_pages_error in proc.stderr
409+
):
410+
continue
411+
break
412+
393413
if proc.returncode:
394414
print(proc.stderr, file=sys.stderr)
395415
raise ValueError(f"Perf failed with return code {proc.returncode}")
@@ -419,54 +439,77 @@ def run_perf(cwd, *args, use_jit=False, **env_vars):
419439

420440

421441
class TestPerfProfilerMixin:
422-
def run_perf(self, script_dir, perf_mode, script):
442+
PERF_CAPTURE_ATTEMPTS = 3
443+
444+
def run_perf(self, script_dir, script, activate_trampoline=True):
423445
raise NotImplementedError()
424446

447+
def run_perf_with_retries(
448+
self, script_dir, script, expected_symbols=(), activate_trampoline=True
449+
):
450+
stdout = stderr = ""
451+
for _ in range(self.PERF_CAPTURE_ATTEMPTS):
452+
stdout, stderr = self.run_perf(
453+
script_dir, script, activate_trampoline=activate_trampoline
454+
)
455+
if activate_trampoline and any(
456+
symbol not in stdout for symbol in expected_symbols
457+
):
458+
continue
459+
break
460+
return stdout, stderr
461+
425462
def test_python_calls_appear_in_the_stack_if_perf_activated(self):
426463
with temp_dir() as script_dir:
427464
code = """if 1:
465+
from itertools import repeat
466+
428467
def foo(n):
429-
x = 0
430-
for i in range(n):
431-
x += i
468+
for _ in repeat(None, n):
469+
pass
432470
433471
def bar(n):
434472
foo(n)
435473
436474
def baz(n):
437475
bar(n)
438476
439-
baz(10000000)
477+
baz(40000000)
440478
"""
441479
script = make_script(script_dir, "perftest", code)
442-
stdout, stderr = self.run_perf(script_dir, script)
443-
self.assertEqual(stderr, "")
480+
expected_symbols = [
481+
f"py::foo:{script}",
482+
f"py::bar:{script}",
483+
f"py::baz:{script}",
484+
]
485+
stdout, _ = self.run_perf_with_retries(
486+
script_dir, script, expected_symbols
487+
)
444488

445-
self.assertIn(f"py::foo:{script}", stdout)
446-
self.assertIn(f"py::bar:{script}", stdout)
447-
self.assertIn(f"py::baz:{script}", stdout)
489+
for expected_symbol in expected_symbols:
490+
self.assertIn(expected_symbol, stdout)
448491

449492
def test_python_calls_do_not_appear_in_the_stack_if_perf_deactivated(self):
450493
with temp_dir() as script_dir:
451494
code = """if 1:
495+
from itertools import repeat
496+
452497
def foo(n):
453-
x = 0
454-
for i in range(n):
455-
x += i
498+
for _ in repeat(None, n):
499+
pass
456500
457501
def bar(n):
458502
foo(n)
459503
460504
def baz(n):
461505
bar(n)
462506
463-
baz(10000000)
507+
baz(40000000)
464508
"""
465509
script = make_script(script_dir, "perftest", code)
466-
stdout, stderr = self.run_perf(
510+
stdout, _ = self.run_perf_with_retries(
467511
script_dir, script, activate_trampoline=False
468512
)
469-
self.assertEqual(stderr, "")
470513

471514
self.assertNotIn(f"py::foo:{script}", stdout)
472515
self.assertNotIn(f"py::bar:{script}", stdout)
@@ -535,13 +578,12 @@ def compile_trampolines_for_all_functions():
535578

536579
with temp_dir() as script_dir:
537580
script = make_script(script_dir, "perftest", code)
538-
env = {**os.environ, "PYTHON_JIT": "0"}
539581
with subprocess.Popen(
540582
[sys.executable, "-Xperf", script],
541583
universal_newlines=True,
542584
stderr=subprocess.PIPE,
543585
stdout=subprocess.PIPE,
544-
env=env,
586+
env=_perf_env(),
545587
) as process:
546588
stdout, stderr = process.communicate()
547589

0 commit comments

Comments
 (0)