| #!/usr/bin/env python3 |
| # Copyright 2026 The IREE Authors |
| # |
| # Licensed under the Apache License v2.0 with LLVM Exceptions. |
| # See https://llvm.org/LICENSE.txt for license information. |
| # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception |
| |
| """Captures one process invocation with Tracy.""" |
| |
| from __future__ import annotations |
| |
| import argparse |
| import datetime |
| import os |
| from pathlib import Path |
| import re |
| import shlex |
| import shutil |
| import signal |
| import socket |
| import subprocess |
| import sys |
| import tempfile |
| import time |
| |
| |
| def eprint(message: str) -> None: |
| print(f"[iree-tracy-capture] {message}", file=sys.stderr) |
| |
| |
| def parse_arguments() -> argparse.Namespace: |
| parser = argparse.ArgumentParser( |
| description="Starts tracy-capture, runs one command, and writes a .tracy file.", |
| ) |
| parser.add_argument("--output-dir", type=Path, default=Path(".tracy")) |
| parser.add_argument("--name") |
| parser.add_argument("--port", type=int, default=0) |
| parser.add_argument("--address", default="127.0.0.1") |
| parser.add_argument("--capture-tool", type=Path) |
| parser.add_argument("--capture-exit-timeout", type=float, default=10.0) |
| parser.add_argument("command", nargs=argparse.REMAINDER) |
| args = parser.parse_args() |
| if args.command and args.command[0] == "--": |
| args.command = args.command[1:] |
| if not args.command: |
| parser.error("expected -- <command> [args...]") |
| return args |
| |
| |
| def sanitize_name(value: str) -> str: |
| value = value.strip().replace("//", "").replace(":", "-").replace("/", "-") |
| value = re.sub(r"[^A-Za-z0-9_.-]+", "-", value).strip("-") |
| return value or "trace" |
| |
| |
| def choose_port(requested_port: int) -> int: |
| if requested_port: |
| return requested_port |
| with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as listen_socket: |
| listen_socket.bind(("127.0.0.1", 0)) |
| return int(listen_socket.getsockname()[1]) |
| |
| |
| def executable(path: Path) -> bool: |
| return path.exists() and os.access(path, os.X_OK) |
| |
| |
| def find_capture_tool(explicit_tool: Path | None) -> Path: |
| candidates: list[Path] = [] |
| if explicit_tool is not None: |
| candidates.append(explicit_tool) |
| if os.environ.get("IREE_TRACY_CAPTURE"): |
| candidates.append(Path(os.environ["IREE_TRACY_CAPTURE"])) |
| for tool_name in ("iree-tracy-capture", "tracy-capture"): |
| path_from_path = shutil.which(tool_name) |
| if path_from_path: |
| candidates.append(Path(path_from_path)) |
| for candidate in candidates: |
| if executable(candidate): |
| return candidate |
| raise RuntimeError( |
| "Could not find tracy-capture; set IREE_TRACY_CAPTURE, pass " |
| "--capture-tool, or put tracy-capture/iree-tracy-capture on PATH" |
| ) |
| |
| |
| def terminate(process: subprocess.Popen) -> None: |
| if process.poll() is not None: |
| return |
| process.send_signal(signal.SIGINT) |
| try: |
| process.wait(timeout=5) |
| return |
| except subprocess.TimeoutExpired: |
| process.terminate() |
| try: |
| process.wait(timeout=5) |
| except subprocess.TimeoutExpired: |
| process.kill() |
| |
| |
| def run_workload( |
| command: list[str], |
| *, |
| env: dict[str, str], |
| capture_process: subprocess.Popen, |
| ) -> int: |
| workload_process = subprocess.Popen(command, env=env) |
| try: |
| while True: |
| workload_return_code = workload_process.poll() |
| if workload_return_code is not None: |
| return int(workload_return_code) |
| if capture_process.poll() is not None: |
| terminate(workload_process) |
| raise RuntimeError("tracy-capture exited before the workload") |
| time.sleep(0.1) |
| except BaseException: |
| terminate(workload_process) |
| raise |
| |
| |
| def read_capture_output(capture_output) -> str: |
| capture_output.flush() |
| capture_output.seek(0) |
| return capture_output.read().strip() |
| |
| |
| def main() -> int: |
| args = parse_arguments() |
| timestamp = datetime.datetime.now().strftime("%Y%m%d-%H%M%S") |
| trace_name = sanitize_name(args.name or Path(args.command[0]).name) |
| output_dir = args.output_dir.resolve() |
| output_dir.mkdir(parents=True, exist_ok=True) |
| trace_path = output_dir / f"{timestamp}-{trace_name}.tracy" |
| trace_port = choose_port(args.port) |
| |
| run_env = os.environ.copy() |
| run_env["TRACY_NO_EXIT"] = "1" |
| run_env["TRACY_ONLY_IPV4"] = "1" |
| run_env["TRACY_ONLY_LOCALHOST"] = "1" |
| run_env["TRACY_PORT"] = str(trace_port) |
| |
| capture_output = None |
| capture_process: subprocess.Popen | None = None |
| workload_return_code = 1 |
| try: |
| capture_tool = find_capture_tool(args.capture_tool) |
| capture_command = [ |
| str(capture_tool), |
| "-o", |
| str(trace_path), |
| "-f", |
| "-a", |
| args.address, |
| "-p", |
| str(trace_port), |
| ] |
| capture_output = tempfile.TemporaryFile(mode="w+", encoding="utf-8") |
| eprint(shlex.join(capture_command)) |
| capture_process = subprocess.Popen( |
| capture_command, |
| stdout=capture_output, |
| stderr=subprocess.STDOUT, |
| text=True, |
| ) |
| time.sleep(0.2) |
| if capture_process.poll() is not None: |
| raise RuntimeError("tracy-capture exited before the workload") |
| |
| eprint(shlex.join(args.command)) |
| workload_return_code = run_workload( |
| args.command, |
| env=run_env, |
| capture_process=capture_process, |
| ) |
| |
| eprint("waiting for tracy-capture to finish") |
| try: |
| capture_return_code = int( |
| capture_process.wait(timeout=args.capture_exit_timeout) |
| ) |
| except subprocess.TimeoutExpired: |
| terminate(capture_process) |
| raise RuntimeError("tracy-capture did not finish after the workload exited") |
| if capture_return_code != 0: |
| raise RuntimeError( |
| f"tracy-capture failed with exit code {capture_return_code}" |
| ) |
| if not trace_path.exists(): |
| raise RuntimeError(f"tracy-capture did not produce {trace_path}") |
| |
| eprint(f"trace: {trace_path}") |
| return workload_return_code |
| except KeyboardInterrupt: |
| if capture_process is not None: |
| terminate(capture_process) |
| raise |
| except Exception as exc: |
| eprint(str(exc)) |
| if capture_output is not None: |
| output = read_capture_output(capture_output) |
| if output: |
| eprint("tracy-capture output:") |
| sys.stderr.write(output + "\n") |
| return workload_return_code if workload_return_code != 0 else 1 |
| finally: |
| if capture_process is not None and capture_process.poll() is None: |
| terminate(capture_process) |
| if capture_output is not None: |
| capture_output.close() |
| |
| |
| if __name__ == "__main__": |
| sys.exit(main()) |