Skip to content

Commit e98e791

Browse files
committed
gh-150389: Make perf profiler tests resilient
Keep perf test output independent from user perf configuration and debuginfod, reduce DWARF sample loss, and retry captures only when expected Python symbols are missing. Do not fail profiler assertions solely because perf script writes to stderr. Closes gh-150395.
1 parent a5be25d commit e98e791

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)