|
5 | 5 |
|
6 | 6 | import abc |
7 | 7 | import importlib.metadata |
| 8 | +import os |
8 | 9 | import os.path |
9 | 10 | import shlex |
| 11 | +import signal |
10 | 12 | import subprocess |
11 | 13 | import sys |
12 | 14 | import tempfile |
@@ -168,3 +170,84 @@ def exec_perf_cmd(self, cmd): |
168 | 170 | self.ctl_fd.write(f"{cmd}\n") |
169 | 171 | self.ctl_fd.flush() |
170 | 172 | self.ack_fd.readline() |
| 173 | + |
| 174 | + |
| 175 | +class tachyon(HookBase): |
| 176 | + """Profile the benchmark using sampling profiler (Tachyon). |
| 177 | +
|
| 178 | + The value of the `PYPERF_TACHYON_OPTS` environment variable is |
| 179 | + appended to the `profiling.sampling attach` command line. |
| 180 | +
|
| 181 | + This hook does not generate output filenames. Use -o or --output in |
| 182 | + `PYPERF_TACHYON_OPTS` to control output. For most formats, -o can |
| 183 | + point at an existing directory and the profiler will auto-generate a |
| 184 | + filename inside it. |
| 185 | +
|
| 186 | + Configuration environment variables: |
| 187 | + PYPERF_TACHYON_OPTS: Extra arguments passed to |
| 188 | + `python -m profiling.sampling attach`. |
| 189 | + """ |
| 190 | + |
| 191 | + def __init__(self): |
| 192 | + if sys.platform == "win32": |
| 193 | + raise HookError("tachyon hook is not supported on Windows") |
| 194 | + |
| 195 | + if sys.version_info < (3, 15): |
| 196 | + raise HookError( |
| 197 | + "tachyon hook requires Python 3.15+, " |
| 198 | + "current version: %s.%s" |
| 199 | + % (sys.version_info.major, sys.version_info.minor) |
| 200 | + ) |
| 201 | + |
| 202 | + try: |
| 203 | + import profiling.sampling # noqa: F401 |
| 204 | + except ImportError: |
| 205 | + raise HookError("profiling.sampling module not available") |
| 206 | + |
| 207 | + self.extra_opts = os.environ.get("PYPERF_TACHYON_OPTS", "") |
| 208 | + |
| 209 | + self._proc = None |
| 210 | + |
| 211 | + def __enter__(self): |
| 212 | + if self._proc is not None: |
| 213 | + self._stop_profiler() |
| 214 | + |
| 215 | + cmd = [ |
| 216 | + sys.executable, |
| 217 | + "-m", "profiling.sampling", |
| 218 | + "attach", |
| 219 | + str(os.getpid()), |
| 220 | + ] |
| 221 | + cmd += shlex.split(self.extra_opts) |
| 222 | + |
| 223 | + self._proc = subprocess.Popen( |
| 224 | + cmd, |
| 225 | + stdout=subprocess.DEVNULL, |
| 226 | + stderr=subprocess.DEVNULL, |
| 227 | + ) |
| 228 | + |
| 229 | + def __exit__(self, _exc_type, _exc_value, _traceback): |
| 230 | + self._stop_profiler() |
| 231 | + |
| 232 | + def _stop_profiler(self): |
| 233 | + if not self._proc: |
| 234 | + return |
| 235 | + |
| 236 | + if self._proc.poll() is None: |
| 237 | + self._proc.send_signal(signal.SIGINT) |
| 238 | + try: |
| 239 | + self._proc.wait(timeout=30) |
| 240 | + except subprocess.TimeoutExpired: |
| 241 | + self._proc.terminate() |
| 242 | + try: |
| 243 | + self._proc.wait(timeout=5) |
| 244 | + except subprocess.TimeoutExpired: |
| 245 | + self._proc.kill() |
| 246 | + self._proc.wait() |
| 247 | + |
| 248 | + self._proc = None |
| 249 | + |
| 250 | + def teardown(self, metadata): |
| 251 | + self._stop_profiler() |
| 252 | + if self.extra_opts: |
| 253 | + metadata["tachyon_extra_opts"] = self.extra_opts |
0 commit comments