内容
# ue-trace-mcp
LLM驱动的虚幻引擎 `.utrace` 性能分析。包含两个部分:
- **`ue-program/TraceDigest/`** — 一个独立的虚幻引擎 `Program` 目标(与 `UnrealPak` 相同结构)。通过引擎的 `TraceServices` 模块读取 `.utrace` 文件并写入结构化的 JSON。支持长寿命的守护进程模式,保持解析会话在查询之间持续存在。需要从源代码构建引擎。
- **`mcp-server/`** — 一个 TypeScript MCP 服务器,封装二进制程序。按需生成守护进程,缓存输出 JSON,管理生命周期。**37 种工具**涵盖 Epic 在 TraceServices 中提供的每个虚幻引擎跟踪类别。
纯 TypeScript 解析器被考虑但最终被排除:线路层存在于 `EpicGames.Tracing` (约 800 行 C# 代码),但完整的语义分析(时序范围重建,每个计时器的百分位,帧/蝴蝶/线程/区域/计数器/内存/分配提供程序)位于 `Engine/Source/Developer/TraceServices/` C++ 代码中,无法移植。
## 仓库布局
```
ue-trace-mcp/
├── Makefile # 构建 / 链接 / 测试编排
├── ue-program/TraceDigest/ # 独立的 Program 目标
│ ├── README.md
│ └── Source/
│ ├── TraceDigest.Target.cs # 类型 = Program
│ └── TraceDigest/
│ ├── TraceDigest.Build.cs
│ └── Private/
│ ├── Main.cpp # INT32_MAIN 入口 — 调度到守护进程或一次性
│ ├── TraceDigestPCH.h
│ ├── Args.{h,cpp} # 所有 -mode/-file/-channel/-view/... 参数
│ ├── Session.{h,cpp} # FLoadedTrace:通过 TraceServices 打开 .utrace,导出合理的缓存文件路径
│ ├── Daemon.{h,cpp} # Unix 套接字服务器 + 接受循环 + 空闲定时器 + LOADING_PROGRESS 心跳
│ ├── Percentiles.h # 储备采样(上限 200k/计时器)+ GetPercentile 助手
│ ├── JsonOut.{h,cpp} # 流式 JSON 编写器
│ ├── TimerLookup.h # 解析定时器名称 -> TimerId(s)
│ ├── ProviderReadScope.h # 每提供程序的 BeginRead/EndRead 对的 RAII 助手
│ ├── Modes.h
│ └── Modes/
│ ├── SeriesBucket.h # 时间分桶的下采样器(计数器/内存/memalloc)
│ ├── Digest.cpp Timeline.cpp Frames.cpp Compare.cpp # cpu+gpu+region(Compare 扩展到 memory/memalloc/counter 在 v0.5)
│ ├── Overview.cpp Frame.cpp Butterfly.cpp Threads.cpp # cpu 钻取
│ ├── Channels.cpp # IChannelProvider
│ ├── Gpu.cpp # GPU 队列/时间线/围栏(包括旧版备用)
│ ├── Counters.cpp Bookmarks.cpp Regions.cpp Logs.cpp # 每个通道的读取
│ ├── Memory.cpp # LLM 标签树 + 样品(IMemoryProvider)
│ ├── Allocations.cpp # 每分配(IAllocationsProvider,同步轮询)
│ ├── Query.cpp # 跟踪查询意图调度
│ ├── Callstack.cpp Modules.cpp # v0.5:符号化
│ ├── Tasks.cpp # v0.5:ITasksProvider 任务图
│ ├── Asset.cpp # v0.5:ILoadTimeProfilerProvider(包/请求/导出)
│ └── Net.cpp # v0.5:INetProfilerProvider(实例/连接/数据包/对象)
└── mcp-server/ # TypeScript MCP 服务器
├── package.json
├── src/
│ ├── index.ts # 标准输入入口 + 信号/EOF 终结器
│ ├── server.ts # 工具注册(37 种工具通过 McpServer)
│ ├── digest.ts # 守护进程或一次性路由器 + 参数生成器
│ ├── daemon.ts # DaemonRegistry:生成/跟踪/查询/回收 + LOADING_PROGRESS 捕获
│ ├── binary.ts # 延迟 GH-release 下载与 SHA256 验证
│ ├── memory.ts # /proc/meminfo + VmRSS + 录取控制
│ ├── cache.ts # 输出 JSON LRU 按(路径,修改时间,变体)
│ ├── schemas.ts # zod 模式(每工具输入形状)
│ ├── tools.ts # 每工具调度器
│ └── types.ts # 每个模式输出的形状
└── tests/ # vitest,无需虚幻引擎(模拟二进制)
```
## 快速开始
单个命令 — 将 `.mcp.json` 写入(或合并到)当前目录:
```bash
npx -y @mtuska/ue-trace-mcp install
```
此操作是幂等的:它保留您已经拥有的其他 `mcpServers` 条目,并且只会触及 `mcpServers.ue-trace`。有用的标志:
- `--pin` — 锁定到此安装程序的确切版本(例如 `@mtuska/ue-trace-mcp@0.5.1`),而不是浮动到最新版本
- `--name <key>` — 选择不同的 `mcpServers` 键(默认 `ue-trace`)
- `--config <path>` — 指向非默认配置文件
- `--force` — 覆盖同一键下现有的条目
- `--dry-run` — 打印将要写入的内容而不修改磁盘
或手动连接:
```jsonc
{
"mcpServers": {
"ue-trace": {
"command": "npx",
"args": ["-y", "@mtuska/ue-trace-mcp"]
}
}
}
```
在第一次调用时,包会从 GitHub 发行版中下载匹配的 `TraceDigest` 二进制文件(约 10 MB),并使用 npm 包中嵌入的清单验证其 SHA-256,然后重用它。 Linux x64 和 Windows x64 开箱即用;macOS 尚未发布(设置 `TRACE_DIGEST_BIN` 为本地构建的二进制文件)。
版本自动保持同步:`npx @mtuska/ue-trace-mcp@0.5.0` 始终会拉取 `v0.5.0` 版本的 `TraceDigest`。
### 开发/从源代码路径
如果您正在修改此仓库或针对本地构建的二进制文件运行:
```bash
make build-program UE_SOURCE=/abs/path/to/UnrealEngine
make build-mcp
```
然后将 `.mcp.json` 指向本地服务器 + 二进制文件:
```jsonc
{
"mcpServers": {
"ue-trace": {
"command": "node",
"args": ["/abs/path/to/ue-trace-mcp/mcp-server/dist/index.js"],
"env": {
"TRACE_DIGEST_BIN": "/abs/path/to/UnrealEngine/Engine/Binaries/Linux/TraceDigest"
}
}
}
}
```
`TRACE_DIGEST_BIN` 覆盖延迟下载路径。
### 烟雾测试
```bash
make smoke FILE=/abs/path/to/foo.utrace # 直接二进制
make smoke-daemon FILE=/abs/path/to/foo.utrace # 冷 + 温控计时通过守护进程
```
## 守护进程模式
每次调用都不会重新解析跟踪。程序支持长寿命的守护进程:加载跟踪一次,然后在空闲超时或回收之前通过 Unix 套接字提供查询服务。
架构:
- **每个 `(绝对路径 × 修改时间)` 跟踪一个守护进程。** 不同跟踪有不同的守护进程。
- **延迟生成** 在第一个查询时。后续查询在同一跟踪上完全跳过解析(~10–200ms 每次调用而不是 ~1.7s 对于 ZombieProto 规模,或 ~20s 对于 1GB 编辑器捕获)。
- **内存录取**: 在生成之前,MCP 读取 `/proc/meminfo`,估计跟踪的占用内存(~4× 文件大小;当捕获 memalloc 时增加到 ~8×),如果加载会留下少于 `TRACE_MIN_FREE_MB`(默认 1024 MiB)的可用内存,则拒绝并提供清晰的错误信息。同时,如果设置了 `TRACE_MAX_DAEMON_MB`,还尊重每个守护进程的上限。
- **加载进度可见性**:二进制文件在解析过程中每 ~500ms 将 `LOADING_PROGRESS: elapsed_ms=<N>` 输出到 stderr;MCP 服务器捕获这些信息,并通过 `trace_status` 的新 `loading[]` 数组提供它们。LLM 可以在加载过程中轮询 `trace_status`,以区分繁忙的守护进程和卡住的守护进程(如果 `wall_elapsed_ms` 继续增加,而 `last_progress_elapsed_ms` 冻结,则二进制文件挂起)。
- **空闲自杀** 在 `TRACE_IDLE_TIMEOUT_SEC`(默认 600 = 10 分钟)后。防御深度 — 幸存于 MCP 崩溃。
- **LRU 驱逐** 当超过 `TRACE_MAX_DAEMONS`(默认 5)个跟踪驻留时。
- **MCP 关闭回收器**:当 MCP 服务器接收到 SIGTERM/SIGINT 或其标准输入关闭时,它会向所有生成的守护进程发送 SIGTERM → SIGKILL 宽限窗口。无孤儿。
- **显式卸载**:`trace_unload({ file })` 立即驱逐守护进程。`trace_status({})` 列出当前守护进程的 PID/占用内存/运行时间/估计和实际 RSS — 以及任何正在加载的守护进程。
环境变量覆盖:
- `TRACE_DIGEST_BIN` — 程序二进制文件路径(覆盖延迟下载路径)
- `TRACE_IDLE_TIMEOUT_SEC` — 守护进程自杀前的秒数(默认 600)
- `TRACE_MAX_DAEMONS` — 最大驻留守护进程数(默认 5)
- `TRACE_MIN_FREE_MB` — 加载后所需的最小系统内存(默认 1024)
- `TRACE_MAX_DAEMON_MB` — 每个守护进程的 RSS 上限(默认 -1 = 禁用)
- `TRACE_SPAWN_TIMEOUT_SEC` — 从生成到 `DAEMON_READY` 的硬超时(默认 600)。多 GB 跟踪可能需要比以前的 30 秒上限更长时间。
## 工具表面
37 个 MCP 工具。每个工具的 `inputSchema` 都是一个纯 JSON-Schema 对象 — Claude Code 和其他 MCP 客户端可以干净地接受它们。
命名约定将 **通道无关动词**(扁平命名,带 `channel` 参数)与 **通道绑定动词**(`trace_<channel>_<purpose>`)分开。
### 跨通道 / 无关(6)
| 工具 | 目的 |
| --- | --- |
| `trace_overview` | 第一印象。顶级键:`channels[]`,`cpu`,`memory`,`memalloc`。`cpu` 子块保持 v0.3 形状不变(`frame_count`,`frame_stats`,`slowest_frames`,`events`)— 仅路径前缀更改。 |
| `trace_channels` | 枚举捕获中存在的通道及其启用/只读标志。先调用此函数,以便 LLM 知道哪些其他工具具有数据。 |
| `trace_frame` | 深入一个帧:在其中运行的每个 CPU 事件。已经按意图跨通道。 |
| `trace_query` | 意图调度逃生阀。选择一个意图 + 参数进行跨切割问题,具体问题无法由特定工具回答(见下文)。 |
| `trace_unload` | 驱逐一个跟踪的守护进程。立即释放内存。 |
| `trace_status` | 每个守护进程的 PID/RSS/运行时间 + 系统内存 + `loading[]` 用于正在进行的生成。 |
### 通道无关动词带 `channel` 参数(6)
默认通道是 `cpu` — 每个 v0.3 调用站点都有效。
| 工具 | 通道支持 |
| --- | --- |
| `trace_digest` | `cpu`(默认),`gpu`,`region`。每个事件的聚合:计数,总毫秒,P50/P95/P99,最大毫秒。 |
| `trace_timeline` | `cpu`(默认),`gpu`,`region`。每个命名事件/区域的实例,具有 `{frame_idx, start_ms, duration_ms}`。 |
| `trace_callers` | `cpu`(默认),`gpu`。通过 `FCreateButterflyParams` 向上 butterfly — UE 原生支持两种筛选类型。 |
| `trace_callees` | `cpu`(默认),`gpu`。向下 butterfly。 |
| `trace_compare` | **v0.5**:`cpu`(默认),`gpu`,`region`,`memory`,`memalloc`,`counter`。cpu/gpu/region 共享事件聚合差异(计数,总计,P95,delta_p95,仅在 A/B 中);memory/memalloc/counter 每个都有自己的行形状。 |
| `trace_frames` | `frame_type: game`(默认)。按 `idx` 或 `duration_ms` 排序的每帧持续时间。 |
### 通道绑定(25)
| 通道 | 工具 |
| --- | --- |
| **cpu** | `trace_cpu_threads` — 每个线程的详细分解, top-10 计时器 |
| **gpu** | `trace_gpu_queues` — GPU 队列列表(包括旧版 Gpu1/Gpu2 备用);`trace_gpu_fences` — 解析的跨队列围栏对,带 stall_ms |
| **counter** | `trace_counter_catalogue` — 每个计数器 + 元数据;`trace_counter_series` — 一个计数器的时间分桶值 |
| **bookmark** | `trace_bookmark_list` — `TRACE_BOOKMARK` 注释 |
| **region** | `trace_region_list` — `TRACE_BEGIN/END_REGION` 跨度,可选按类别过滤。每行 `open` 标志表示跨度永远不会关闭。 |
| **log** | `trace_log_messages` — 带verbosity floor / 类别 / grep 过滤器的窗口化 `UE_LOG` 枚举 |
| **memory**(LLM 标签) | `trace_memory_trackers` / `trace_memory_tags` / `trace_memory_samples` — 标签树 + 按标签的时间序列 |
| **memalloc**(每分配) | `trace_memalloc_timeline`(聚合统计),`trace_memalloc_heaps`(FHeapSpec 树),`trace_memalloc_query`(基于规则的查询,同步包装) |
| **callstack / module** | **v0.5** `trace_callstack` — 解析一个 `callstack_id`(由每个事件行携带)到符号化帧; **v0.5** `trace_modules` — 发现的模块 + 符号解析统计 |
| **task graph** | **v0.5** `trace_task_list` — 带状态过滤的窗口化 `ITasksProvider` 枚举; **v0.5** `trace_task_drill` — 一个任务 id + 四个关系数组的全 FTaskInfo |
| **asset / loadtime** | **v0.5** `trace_asset_packages` / `trace_asset_requests` / `trace_asset_exports` — 每个包 + 每个请求 + 每个导出的加载时间,主线程/异步加载拆分 |
| **net** | **v0.5** `trace_net_instances` / `trace_net_connections` / `trace_net_packets`(窗口化) / `trace_net_objects` — 多人游戏 profiler 层次结构 |
每行有效负载都位于 `events`(或 `series` / `tags` / `heaps` / `frames` / `modules` — 形状不同于事件 — 文档见工具)。
### `trace_query` 意图
`trace_query` 是用于需要连接两个提供者的问题的逃生阀。调用 `intent: "list"` 以发现注册表;LLM 可以自我描述可用内容:
| 意图 | 连接 | 参数 |
| --- | --- | --- |
| `frames_where_counter_exceeds` | frames × counters | `{ counter, threshold, op?: ">\" | \">=\" }` |
| `events_inside_region` | regions × CPU timing | `{ region, event_prefix?, limit? }` |
| `logs_around_slow_frames` | frames × logs | `{ p_threshold?: 0.95, window_ms?: 50, limit? }` |
新意图在 C++ 端实现,无需强制 TS 架构更新 — `QueryArgs.intent` 是 `z.string()`,而非枚举。