import os import re import shlex import time # Mixed into DevCommand (bin/dev), which provides _project_root and # _run_in_root. class TracyCommand: ## # Builds a Tracy tool from the third_party/tracy submodule, then runs it. # Building from the submodule keeps the tool's version in sync with the # client compiled into the engine. # dev tracy [--fresh] [args...] Opens the profiler (viewer). # dev tracy --save [name] [--fresh] [args...] # Captures a trace headlessly to # traces/.tracy (default # name: a timestamp) until the app # exits or Ctrl+C. # The profiler uses the X11 backend unless WAYLAND_DISPLAY is set. Tools # are configured on first use (or --fresh, or when the backend changes), # then built incrementally. Other arguments are passed through to the tool # (e.g. a .tracy file to open, or `-s 10` to capture for 10 seconds). def tracy(self, *args, **kwargs): fresh, save, name, args = self._parse_tracy_args(args) if save: binary = self._build_tracy_tool("capture", "tracy-capture", {}, fresh) traces_dir = os.path.join(self._project_root(), "traces") os.makedirs(traces_dir, exist_ok=True) name = name or time.strftime("%Y-%m-%d-%H%M%S") if not name.endswith(".tracy"): name += ".tracy" args = ["-o", os.path.join(traces_dir, name), *args] else: legacy = "OFF" if os.environ.get("WAYLAND_DISPLAY") else "ON" binary = self._build_tracy_tool("profiler", "tracy-profiler", {"LEGACY": legacy}, fresh) print(f"Running: {shlex.join([binary, *args])}") os.execv(binary, [binary, *args]) def _parse_tracy_args(self, args): fresh = False save = False name = None rest = [] args = list(args) while args: arg = args.pop(0) if arg == "--fresh": fresh = True elif arg == "--save": save = True if args and not args[0].startswith("-"): name = args.pop(0) else: rest.append(arg) return fresh, save, name, rest # Configures third_party/tracy/ into /build when it has no # build yet, --fresh is given, or a cached define differs from `defines`. def _build_tracy_tool(self, tool, binary_name, defines, fresh): source_dir = os.path.join("third_party", "tracy", tool) build_dir = os.path.join(source_dir, "build") cache_path = os.path.join(self._project_root(), build_dir, "CMakeCache.txt") stale = not os.path.isfile(cache_path) if not stale: with open(cache_path) as f: cache = f.read() stale = any( re.search(rf"^{key}:BOOL={value}$", cache, re.MULTILINE) is None for key, value in defines.items() ) if fresh or stale: configure = ["cmake", "-B", build_dir, "-S", source_dir, "-DCMAKE_BUILD_TYPE=Release"] configure += [f"-D{key}={value}" for key, value in defines.items()] if fresh: configure.append("--fresh") self._run_in_root(configure) self._run_in_root(["cmake", "--build", build_dir, "--parallel"]) return os.path.join(self._project_root(), build_dir, binary_name)