Skip to content

Commit 2f3e277

Browse files
committed
gh-141592: test_perf_profiler: fix race in perf map cleanup using addCleanup
Signed-off-by: Paresh Joshi <rahulj9223@gmail.com>
1 parent 4ceb077 commit 2f3e277

1 file changed

Lines changed: 83 additions & 37 deletions

File tree

Lib/test/test_perf_profiler.py

Lines changed: 83 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -35,17 +35,27 @@ def supports_trampoline_profiling():
3535

3636

3737
class TestPerfTrampoline(unittest.TestCase):
38-
def setUp(self):
39-
super().setUp()
40-
self.perf_files = set(pathlib.Path("/tmp/").glob("perf-*.map"))
41-
42-
def tearDown(self) -> None:
43-
super().tearDown()
44-
files_to_delete = (
45-
set(pathlib.Path("/tmp/").glob("perf-*.map")) - self.perf_files
46-
)
47-
for file in files_to_delete:
48-
file.unlink()
38+
#
39+
# We've removed the buggy setUp and tearDown methods.
40+
# They caused race conditions by scanning /tmp for `perf-*.map` files.
41+
# Instead, we now use `addCleanup()` inside each test to delete
42+
# the *specific* files we know that test created.
43+
#
44+
45+
def _cleanup_perf_map(self, pid):
46+
"""
47+
Helper to safely delete a specific perf map file.
48+
We register this with addCleanup() to make sure it runs
49+
even if the test assertions fail.
50+
"""
51+
perf_map = pathlib.Path(f"/tmp/perf-{pid}.map")
52+
try:
53+
if perf_map.exists():
54+
perf_map.unlink()
55+
except OSError:
56+
# Surpress errors, e.g., file already removed or
57+
# another kind of race condition.
58+
pass
4959

5060
@unittest.skipIf(support.check_bolt_optimized(), "fails on BOLT instrumented binaries")
5161
def test_trampoline_works(self):
@@ -64,13 +74,21 @@ def baz():
6474
with temp_dir() as script_dir:
6575
script = make_script(script_dir, "perftest", code)
6676
env = {**os.environ, "PYTHON_JIT": "0"}
67-
with subprocess.Popen(
77+
78+
# Start the subprocess...
79+
process = subprocess.Popen(
6880
[sys.executable, "-Xperf", script],
6981
text=True,
7082
stderr=subprocess.PIPE,
7183
stdout=subprocess.PIPE,
7284
env=env,
73-
) as process:
85+
)
86+
87+
# ...and immediately register a cleanup task for its PID.
88+
# This is guaranteed to run after the test.
89+
self.addCleanup(self._cleanup_perf_map, process.pid)
90+
91+
with process:
7492
stdout, stderr = process.communicate()
7593

7694
self.assertEqual(stderr, "")
@@ -116,9 +134,11 @@ def baz_fork():
116134
def foo():
117135
pid = os.fork()
118136
if pid == 0:
137+
# We're in the child process
119138
print(os.getpid())
120139
baz_fork()
121140
else:
141+
# We're in the parent process
122142
_, status = os.waitpid(-1, 0)
123143
sys.exit(status)
124144
@@ -140,11 +160,19 @@ def baz():
140160
stdout=subprocess.PIPE,
141161
env=env,
142162
) as process:
163+
# Register cleanup for the PARENT process
164+
self.addCleanup(self._cleanup_perf_map, process.pid)
143165
stdout, stderr = process.communicate()
144166

145167
self.assertEqual(process.returncode, 0)
146168
self.assertEqual(stderr, "")
169+
170+
# The child process printed its PID to stdout
147171
child_pid = int(stdout.strip())
172+
173+
# Register cleanup for the CHILD process
174+
self.addCleanup(self._cleanup_perf_map, child_pid)
175+
148176
perf_file = pathlib.Path(f"/tmp/perf-{process.pid}.map")
149177
perf_child_file = pathlib.Path(f"/tmp/perf-{child_pid}.map")
150178
self.assertTrue(perf_file.exists())
@@ -199,6 +227,8 @@ def baz():
199227
stdout=subprocess.PIPE,
200228
env=env,
201229
) as process:
230+
# Register cleanup inside the loop for each process created
231+
self.addCleanup(self._cleanup_perf_map, process.pid)
202232
stdout, stderr = process.communicate()
203233

204234
self.assertEqual(stderr, "")
@@ -308,6 +338,10 @@ def perf_command_works():
308338

309339

310340
def run_perf(cwd, *args, use_jit=False, **env_vars):
341+
# This helper function runs perf and stores its output files *inside*
342+
# the 'cwd' (which is a temp_dir()). This is good! It means
343+
# those files (`perf_output.perf`, `jit_output.dump`)
344+
# are automatically cleaned up when the temp_dir() context manager exits.
311345
env = os.environ.copy()
312346
if env_vars:
313347
env.update(env_vars)
@@ -434,23 +468,30 @@ def baz(n):
434468
"Unwinding is unreliable with frame pointers",
435469
)
436470
class TestPerfProfiler(unittest.TestCase, TestPerfProfilerMixin):
471+
#
472+
# Just like in TestPerfTrampoline, we remove the racy
473+
# setUp/tearDown methods.
474+
#
475+
476+
def _cleanup_perf_map(self, pid):
477+
"""
478+
Helper to safely delete a specific perf map file.
479+
We need this for test_pre_fork_compile.
480+
"""
481+
perf_map = pathlib.Path(f"/tmp/perf-{pid}.map")
482+
try:
483+
if perf_map.exists():
484+
perf_map.unlink()
485+
except OSError:
486+
pass
487+
437488
def run_perf(self, script_dir, script, activate_trampoline=True):
489+
# The run_perf helper handles its own temp files inside script_dir
490+
# so we don't need to add cleanups for it here.
438491
if activate_trampoline:
439492
return run_perf(script_dir, sys.executable, "-Xperf", script)
440493
return run_perf(script_dir, sys.executable, script)
441494

442-
def setUp(self):
443-
super().setUp()
444-
self.perf_files = set(pathlib.Path("/tmp/").glob("perf-*.map"))
445-
446-
def tearDown(self) -> None:
447-
super().tearDown()
448-
files_to_delete = (
449-
set(pathlib.Path("/tmp/").glob("perf-*.map")) - self.perf_files
450-
)
451-
for file in files_to_delete:
452-
file.unlink()
453-
454495
def test_pre_fork_compile(self):
455496
code = """if 1:
456497
import sys
@@ -499,11 +540,17 @@ def compile_trampolines_for_all_functions():
499540
stdout=subprocess.PIPE,
500541
env=env,
501542
) as process:
543+
# This test also creates map files, so we clean them up
544+
# just like in TestPerfTrampoline.
545+
self.addCleanup(self._cleanup_perf_map, process.pid)
502546
stdout, stderr = process.communicate()
503547

504548
self.assertEqual(process.returncode, 0)
505549
self.assertNotIn("Error:", stderr)
550+
506551
child_pid = int(stdout.strip())
552+
self.addCleanup(self._cleanup_perf_map, child_pid)
553+
507554
perf_file = pathlib.Path(f"/tmp/perf-{process.pid}.map")
508555
perf_child_file = pathlib.Path(f"/tmp/perf-{child_pid}.map")
509556
self.assertTrue(perf_file.exists())
@@ -550,25 +597,24 @@ def _is_perf_version_at_least(major, minor):
550597
_is_perf_version_at_least(6, 6), "perf command may not work due to a perf bug"
551598
)
552599
class TestPerfProfilerWithDwarf(unittest.TestCase, TestPerfProfilerMixin):
600+
#
601+
# We've removed the racy setUp/tearDown methods from here, too.
602+
# The `run_perf` function (with use_jit=True) already creates its
603+
# .dump and .so files inside the `temp_dir()`, which gets
604+
# cleaned up automatically. The old setUp/tearDown were
605+
# trying to clean up /tmp, which was buggy and unreliable.
606+
#
607+
553608
def run_perf(self, script_dir, script, activate_trampoline=True):
554609
if activate_trampoline:
555610
return run_perf(
556611
script_dir, sys.executable, "-Xperf_jit", script, use_jit=True
557612
)
558613
return run_perf(script_dir, sys.executable, script, use_jit=True)
559614

560-
def setUp(self):
561-
super().setUp()
562-
self.perf_files = set(pathlib.Path("/tmp/").glob("jit*.dump"))
563-
self.perf_files |= set(pathlib.Path("/tmp/").glob("jitted-*.so"))
564-
565-
def tearDown(self) -> None:
566-
super().tearDown()
567-
files_to_delete = set(pathlib.Path("/tmp/").glob("jit*.dump"))
568-
files_to_delete |= set(pathlib.Path("/tmp/").glob("jitted-*.so"))
569-
files_to_delete = files_to_delete - self.perf_files
570-
for file in files_to_delete:
571-
file.unlink()
615+
#
616+
# No setUp or tearDown needed!
617+
#
572618

573619

574620
if __name__ == "__main__":

0 commit comments

Comments
 (0)