Skip to content

Commit 339a521

Browse files
pablogsalvstinner
authored andcommitted
gh-150389: Make perf profiler tests resilient (#150437)
(cherry picked from commit 638754c)
1 parent 84a21c5 commit 339a521

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

@@ -235,13 +247,12 @@ def baz():
235247
"""
236248
with temp_dir() as script_dir:
237249
script = make_script(script_dir, "perftest", code)
238-
env = {**os.environ, "PYTHON_JIT": "0"}
239250
with subprocess.Popen(
240251
[sys.executable, script],
241252
text=True,
242253
stderr=subprocess.PIPE,
243254
stdout=subprocess.PIPE,
244-
env=env,
255+
env=_perf_env(),
245256
) as process:
246257
stdout, stderr = process.communicate()
247258

@@ -338,9 +349,12 @@ def perf_command_works():
338349
"-c",
339350
'print("hello")',
340351
)
341-
env = {**os.environ, "PYTHON_JIT": "0"}
342352
stdout = subprocess.check_output(
343-
cmd, cwd=script_dir, text=True, stderr=subprocess.STDOUT, env=env
353+
cmd,
354+
cwd=script_dir,
355+
text=True,
356+
stderr=subprocess.STDOUT,
357+
env=_perf_env(),
344358
)
345359
except (subprocess.SubprocessError, OSError):
346360
return False
@@ -352,43 +366,49 @@ def perf_command_works():
352366

353367

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

419439

420440
class TestPerfProfilerMixin:
421-
def run_perf(self, script_dir, perf_mode, script):
441+
PERF_CAPTURE_ATTEMPTS = 3
442+
443+
def run_perf(self, script_dir, script, activate_trampoline=True):
422444
raise NotImplementedError()
423445

446+
def run_perf_with_retries(
447+
self, script_dir, script, expected_symbols=(), activate_trampoline=True
448+
):
449+
stdout = stderr = ""
450+
for _ in range(self.PERF_CAPTURE_ATTEMPTS):
451+
stdout, stderr = self.run_perf(
452+
script_dir, script, activate_trampoline=activate_trampoline
453+
)
454+
if activate_trampoline and any(
455+
symbol not in stdout for symbol in expected_symbols
456+
):
457+
continue
458+
break
459+
return stdout, stderr
460+
424461
def test_python_calls_appear_in_the_stack_if_perf_activated(self):
425462
with temp_dir() as script_dir:
426463
code = """if 1:
464+
from itertools import repeat
465+
427466
def foo(n):
428-
x = 0
429-
for i in range(n):
430-
x += i
467+
for _ in repeat(None, n):
468+
pass
431469
432470
def bar(n):
433471
foo(n)
434472
435473
def baz(n):
436474
bar(n)
437475
438-
baz(10000000)
476+
baz(40000000)
439477
"""
440478
script = make_script(script_dir, "perftest", code)
441-
stdout, stderr = self.run_perf(script_dir, script)
442-
self.assertEqual(stderr, "")
479+
expected_symbols = [
480+
f"py::foo:{script}",
481+
f"py::bar:{script}",
482+
f"py::baz:{script}",
483+
]
484+
stdout, _ = self.run_perf_with_retries(
485+
script_dir, script, expected_symbols
486+
)
443487

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

448491
def test_python_calls_do_not_appear_in_the_stack_if_perf_deactivated(self):
449492
with temp_dir() as script_dir:
450493
code = """if 1:
494+
from itertools import repeat
495+
451496
def foo(n):
452-
x = 0
453-
for i in range(n):
454-
x += i
497+
for _ in repeat(None, n):
498+
pass
455499
456500
def bar(n):
457501
foo(n)
458502
459503
def baz(n):
460504
bar(n)
461505
462-
baz(10000000)
506+
baz(40000000)
463507
"""
464508
script = make_script(script_dir, "perftest", code)
465-
stdout, stderr = self.run_perf(
509+
stdout, _ = self.run_perf_with_retries(
466510
script_dir, script, activate_trampoline=False
467511
)
468-
self.assertEqual(stderr, "")
469512

470513
self.assertNotIn(f"py::foo:{script}", stdout)
471514
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)