Skip to content

Commit f7e8d0b

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 f7e8d0b

1 file changed

Lines changed: 67 additions & 32 deletions

File tree

Lib/test/test_perf_profiler.py

Lines changed: 67 additions & 32 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,10 +373,7 @@ 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"
367378
if not use_jit:
368379
base_cmd = (
@@ -385,6 +396,8 @@ def run_perf(cwd, *args, use_jit=False, **env_vars):
385396
"--call-graph=dwarf,65528",
386397
"-F99",
387398
"-k1",
399+
"-m",
400+
"4M",
388401
"-o",
389402
output_file,
390403
"--",
@@ -425,54 +438,77 @@ def run_perf(cwd, *args, use_jit=False, **env_vars):
425438

426439

427440
class TestPerfProfilerMixin:
428-
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):
429444
raise NotImplementedError()
430445

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+
431461
def test_python_calls_appear_in_the_stack_if_perf_activated(self):
432462
with temp_dir() as script_dir:
433463
code = """if 1:
464+
from itertools import repeat
465+
434466
def foo(n):
435-
x = 0
436-
for i in range(n):
437-
x += i
467+
for _ in repeat(None, n):
468+
pass
438469
439470
def bar(n):
440471
foo(n)
441472
442473
def baz(n):
443474
bar(n)
444475
445-
baz(10000000)
476+
baz(40000000)
446477
"""
447478
script = make_script(script_dir, "perftest", code)
448-
stdout, stderr = self.run_perf(script_dir, script)
449-
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+
)
450487

451-
self.assertIn(f"py::foo:{script}", stdout)
452-
self.assertIn(f"py::bar:{script}", stdout)
453-
self.assertIn(f"py::baz:{script}", stdout)
488+
for expected_symbol in expected_symbols:
489+
self.assertIn(expected_symbol, stdout)
454490

455491
def test_python_calls_do_not_appear_in_the_stack_if_perf_deactivated(self):
456492
with temp_dir() as script_dir:
457493
code = """if 1:
494+
from itertools import repeat
495+
458496
def foo(n):
459-
x = 0
460-
for i in range(n):
461-
x += i
497+
for _ in repeat(None, n):
498+
pass
462499
463500
def bar(n):
464501
foo(n)
465502
466503
def baz(n):
467504
bar(n)
468505
469-
baz(10000000)
506+
baz(40000000)
470507
"""
471508
script = make_script(script_dir, "perftest", code)
472-
stdout, stderr = self.run_perf(
509+
stdout, _ = self.run_perf_with_retries(
473510
script_dir, script, activate_trampoline=False
474511
)
475-
self.assertEqual(stderr, "")
476512

477513
self.assertNotIn(f"py::foo:{script}", stdout)
478514
self.assertNotIn(f"py::bar:{script}", stdout)
@@ -542,13 +578,12 @@ def compile_trampolines_for_all_functions():
542578

543579
with temp_dir() as script_dir:
544580
script = make_script(script_dir, "perftest", code)
545-
env = {**os.environ, "PYTHON_JIT": "0"}
546581
with subprocess.Popen(
547582
[sys.executable, "-Xperf", script],
548583
universal_newlines=True,
549584
stderr=subprocess.PIPE,
550585
stdout=subprocess.PIPE,
551-
env=env,
586+
env=_perf_env(),
552587
) as process:
553588
stdout, stderr = process.communicate()
554589

0 commit comments

Comments
 (0)