Skip to content

Commit 72b7c70

Browse files
profilers/python_ebpf.py: specifying newly TMPDIR on each instance of PyPerf (#921)
1 parent c620532 commit 72b7c70

File tree

2 files changed

+39
-8
lines changed

2 files changed

+39
-8
lines changed

gprofiler/profilers/python_ebpf.py

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,11 @@
1717
import os
1818
import resource
1919
import selectors
20+
import shutil
2021
import signal
2122
from pathlib import Path
2223
from subprocess import Popen
23-
from typing import Dict, List, Optional, Tuple, cast
24+
from typing import Dict, List, Optional, Tuple, Union, cast
2425

2526
from granulate_utils.linux.ns import is_running_in_init_pid
2627
from psutil import NoSuchProcess, Process
@@ -86,6 +87,10 @@ def __init__(
8687
self._kernel_offsets: Dict[str, int] = {}
8788
self._metadata = python.PythonMetadata(self._profiler_state.stop_event)
8889
self._verbose = verbose
90+
self._pyperf_staticx_tmpdir: Optional[Path] = None
91+
if os.environ.get("TMPDIR", None) is not None:
92+
# We want to create a new level of hirerachy in our current staticx tempdir.
93+
self._pyperf_staticx_tmpdir = Path(os.environ["TMPDIR"]) / ("pyperf_" + random_prefix())
8994

9095
@classmethod
9196
def _check_output(cls, process: Popen, output_path: Path) -> None:
@@ -153,6 +158,14 @@ def _pyperf_base_command(self) -> List[str]:
153158
cmd.extend(["-v", "4"])
154159
return cmd
155160

161+
def _staticx_cleanup(self) -> None:
162+
"""
163+
We experienced issues with PyPerf's staticx'd directory, so we want to clean it up on termination.
164+
"""
165+
assert not self.is_running(), "_staticx_cleanup is impossible while PyPerf is running"
166+
if self._pyperf_staticx_tmpdir is not None and self._pyperf_staticx_tmpdir.exists():
167+
shutil.rmtree(self._pyperf_staticx_tmpdir.as_posix())
168+
156169
def test(self) -> None:
157170
self._ebpf_environment()
158171

@@ -170,14 +183,16 @@ def test(self) -> None:
170183
"--duration",
171184
"1",
172185
]
173-
process = start_process(cmd)
186+
process = start_process(cmd, tmpdir=self._pyperf_staticx_tmpdir)
174187
try:
175188
poll_process(process, self._POLL_TIMEOUT, self._profiler_state.stop_event)
176189
except TimeoutError:
177190
process.kill()
178191
raise
179192
else:
180193
self._check_output(process, self.output_path)
194+
finally:
195+
self._staticx_cleanup()
181196

182197
def start(self) -> None:
183198
logger.info("Starting profiling of Python processes with PyPerf")
@@ -201,7 +216,7 @@ def start(self) -> None:
201216
for f in glob.glob(f"{str(self.output_path)}.*"):
202217
os.unlink(f)
203218

204-
process = start_process(cmd)
219+
process = start_process(cmd, tmpdir=self._pyperf_staticx_tmpdir)
205220
# wait until the transient data file appears - because once returning from here, PyPerf may
206221
# be polled via snapshot() and we need it to finish installing its signal handler.
207222
try:
@@ -212,6 +227,7 @@ def start(self) -> None:
212227
stdout = process.stdout.read()
213228
stderr = process.stderr.read()
214229
logger.error("PyPerf failed to start", stdout=stdout, stderr=stderr)
230+
self._staticx_cleanup()
215231
raise
216232
else:
217233
self.process = process
@@ -303,16 +319,23 @@ def snapshot(self) -> ProcessToProfileData:
303319
return profiles
304320

305321
def _terminate(self) -> Tuple[Optional[int], str, str]:
322+
exit_status: Optional[int] = None
323+
stdout: Union[str, bytes] = ""
324+
stderr: Union[str, bytes] = ""
325+
306326
self._unregister_process_selectors()
307327
if self.is_running():
308328
assert self.process is not None # for mypy
309329
self.process.terminate() # okay to call even if process is already dead
310330
exit_status, stdout, stderr = reap_process(self.process)
311331
self.process = None
312-
return exit_status, stdout.decode(), stderr.decode()
332+
333+
stdout = stdout.decode() if isinstance(stdout, bytes) else stdout
334+
stderr = stderr.decode() if isinstance(stderr, bytes) else stderr
313335

314336
assert self.process is None # means we're not running
315-
return None, "", ""
337+
self._staticx_cleanup()
338+
return exit_status, stdout, stderr
316339

317340
def stop(self) -> None:
318341
exit_status, stdout, stderr = self._terminate()

gprofiler/utils/__init__.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,11 @@ def wrapper() -> Any:
127127

128128

129129
def start_process(
130-
cmd: Union[str, List[str]], via_staticx: bool = False, term_on_parent_death: bool = True, **kwargs: Any
130+
cmd: Union[str, List[str]],
131+
via_staticx: bool = False,
132+
term_on_parent_death: bool = True,
133+
tmpdir: Optional[Path] = None,
134+
**kwargs: Any,
131135
) -> Popen:
132136
if isinstance(cmd, str):
133137
cmd = [cmd]
@@ -147,9 +151,13 @@ def start_process(
147151
cmd = [f"{staticx_dir}/.staticx.interp", "--library-path", staticx_dir] + cmd
148152
else:
149153
env = env if env is not None else os.environ.copy()
150-
# ensure `TMPDIR` env is propagated to the child processes (used by staticx)
151-
if "TMPDIR" not in env and "TMPDIR" in os.environ:
154+
if tmpdir is not None:
155+
tmpdir.mkdir(exist_ok=True)
156+
env["TMPDIR"] = tmpdir.as_posix()
157+
elif "TMPDIR" not in env and "TMPDIR" in os.environ:
158+
# ensure `TMPDIR` env is propagated to the child processes (used by staticx)
152159
env["TMPDIR"] = os.environ["TMPDIR"]
160+
153161
# explicitly remove our directory from LD_LIBRARY_PATH
154162
env["LD_LIBRARY_PATH"] = ""
155163

0 commit comments

Comments
 (0)