Skip to content

Commit cc6fea8

Browse files
[3.15] gh-150389: Make perf profiler tests resilient (GH-150437) (#150515)
1 parent d2b10e7 commit cc6fea8

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
@@ -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+
3752
class 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

361375
def 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

427447
class 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

Comments
 (0)