| |
$ ztrace summary sample.trace
进程:hotspot 总耗时:3.8s 模板:Time Profiler 采样点数:3044 总CPU时间:3044ms
自用时间 69.4% 2113ms hotspot heavyWork() 29.7% 905ms hotspot lightWork()
总时间(占用显著的调用者) 99.9% 3041ms main
调用栈 69.4% 2113ms main > heavyWork() 29.7% 904ms main > lightWork()
从 33,553 行浓缩到 13 行。LLM 所需的精炼信息,只需几行即可告诉你:“优化 `heavyWork()`,因为它占用了 70% 的 CPU 时间”。
## 过滤内容(以及原因)
并不是 xctrace 报告的所有信息都值得关注。在性能分析中,我无法优化 `libdispatch.dylib`,也无法重写 `dyld4::PrebuiltLoader::loadDependents`。这些调用对于查找**我自己的代码**瓶颈来说,都是无意义的噪音。
ztrace 过滤数据的层次包括:
**系统二进制文件**:所有位于 `/usr/lib/` 或 `/System/` 的内容都会被丢弃,这些是系统调用或 Swift 运行时。
**运行时内部实现**:像 `__swift_instantiateConcreteTypeFromMangledNameV2` 或 `DYLD-STUB$$sin`,虽然技术上属于你的二进制文件(因为它们静态链接进来了),但显然不是你写的代码,直接略过。
**未解析的符号**:生产环境中的应用程序(例如,Spotify)通常会被*去掉多余符号信息*。调用帧会显示为 `0x104885404` 这样的原始地址。这种信息会被 ztrace 过滤掉,并作出提醒,比如:“用户层 85% 的采样点没有符号信息”。这样你知道,分析数据是存在的,但需要 *dSYMs* 文件才能真正利用。
## 在实际应用中的表现
这个小测试很漂亮,但并不真实。那在实际应用程序中,它表现如何?我用终端模拟器 Ghostty 测试了一下:
进程:ghostty 总耗时:3.8s 模板:Time Profiler 采样点数:295 总CPU时间:295ms
自用时间 53.2% 157ms ghostty main 3.7% 11ms ghostty renderer.metal.RenderPass.begin 3.1% 9ms ghostty renderer.generic.Renderer(renderer.Metal).rebuildCells 2.7% 8ms ghostty renderer.generic.Renderer(renderer.Metal).drawFrame 2.4% 7ms ghostty renderer.generic.Renderer(renderer.Metal).updateFrame 2.0% 6ms ghostty heap.PageAllocator.alloc 1.7% 5ms ghostty terminal.page.Page.clonePartialRowFrom 1.7% 5ms ghostty font.shaper.coretext.Shaper.shape
这才叫有用信息。你能立刻看到:Metal 渲染器(渲染阶段、重建单元格、绘制帧)和字体 shaping 功能是主要耗时点。如果你在优化 Ghostty,就会精准知道从哪里入手。
并且,每个函数都附带模块名(比如 `ghostty`),在拥有多个框架的应用中,你可以轻松判断瓶颈是出在自己的代码还是某个依赖库。
## 技术选型(以及为何不选 Swift)
最初的 `CLAUDE.md` 里提到用 Swift。“跟使用场景一致”,我当时想。但在发现 95% 的工作是解析 XML 和格式化文本后,我切换到了 Python。
用 `xml.etree.ElementTree` 解析 XML 结构,三行代码就能解决。在 Swift 中,`XMLParser` 使用的是 SAX 流式解析——回调、可变状态、委托,这种做法对一个只需要“给我树状结构让我浏览”的场景而言,未免显得过于简陋。
除此之外,Python 脚本可以通过 `uv tool` 工具安装。而用 Swift 编译的二进制文件只能在 macOS/arm64 上运行。考虑到 xctrace 仅限于 macOS,这么说可能多虑了,但使用 `uv` 来分发实际上比编译和拷贝二进制要方便得多。
## 下一步计划
以下是 v0.1 版本尚缺的一些功能:
- **`ztrace record`:** 一条命令完成录制和摘要(便捷性改进,但并非优先)
- **可配置的过滤器:** 排除特定模块,调整调用栈深度
- **trace 对比功能:** 对优化前后进行对比,生成差异报告
- **支持内存分配分析:** 不仅仅是 CPU 数据,还包括内存分析
项目已上线,访问[GitHub仓库](https://github.com/frr149/ztrace)即可试用。
## 融入开发流程
ztrace 最有趣的一点是,你不用手动运行它,而是让 Claude Code 在性能分析时自动调用。
你只需在项目的 `CLAUDE.md` 文件中加入以下内容:
```markdown
### Profiling (xctrace)
- 使用 `ztrace summary <file.trace>` 读取性能跟踪文件,不要试图直接读取 xctrace 导出的 XML 文件。
- 流程:`xctrace record` → `ztrace summary`
- 参数: `--threshold 0.5` (显示更多函数), `--depth 10` (更深层的调用栈)
从此以后,每当 Claude Code 需要做性能分析时,流程如下:
| |
如果没有 ztrace,第 2 步会产生 30,000 行 XML,这要么会超出上下文窗口限制,要么使有用信息淹没在一堆无用信息中。而有了 ztrace,Claude 获取的正是它需要的关键信息,比如“70% 的 CPU 花在 heavyWork() 上,位于 Renderer.swift 的第 42 行”。
再次强调要点
ztrace 的诞生启示是,大语言模型在处理大规模原始数据时并不擅长。它擅长的是基于已经处理并提炼的信息进行推理。给 Claude 33,000 行 XML 就像让医生从 RAW 格式的核磁共振中直接诊断问题一样。医生需要的是清晰的图像,而不是散落的字节。
所以,下次当大语言模型告诉你“输出数据太庞大”时,不要想着用更大的上下文窗口模型去解决问题。相反,思考如何在数据送入模型之前,先将其浓缩加工为有用的信息。
归根究底,这正是工程师所做的:将无用信息转化为信号。无论是否有人工智能的参与。