1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
title: "33,000 行 XML 告诉你 heavyWork() 函数耗时过长:如何驯服 xctrace 应对大语言模型 (LLM)"
date: 2026-03-08T14:00:00+01:00
draft: false
slug: "ztrace-xctrace-compact-summary-llm"
slug_en: "ztrace-xctrace-compact-summary-llm"
description: "xctrace 导出包含 33,000 行的 XML 文档,会超出任何 LLM 的上下文窗口。ztrace 将其浓缩为 10 行有用信息。本文讲解其中的缘由与方法。"
tags: ["xctrace", "instruments", "性能分析", "大语言模型", "Claude Code", "Python", "性能优化"]
categories: ["观点"]

translation:
  hash: ""
  last_translated: ""
  notes: |
    - "以大白话来说": "in plain language". No religious connotation.
    - "驯服": used metaphorically as "to tame" (a tool/output). Not literal.
    - "临时解决方案": "quick-and-dirty solution". Neutral term.
    - "无用信息": means "filler/fluff/noise" in this context.
    - "注意这一点": "here's the key point" / "pay attention to this".
---

上周,我在用 *Instruments* 工具对一个 Swift 应用进行性能分析。没什么稀奇的:运行 `xctrace record`,再运行 `xctrace export`,然后把导出的 XML 拷贝到 Claude Code 的上下文,让它帮忙分析热点。

结果 Claude 跟我说:`"XML 文件太大,无法可靠地处理。"`

33,553 行 XML,只为分析一个只有两个函数的程序。

## 真正的问题

`xctrace export` 是一个很棒的工具。它什么都给你:每一个采样点、每一个调用栈、每一帧的二进制信息、内存地址、UUID,简直面面俱到,精准无比,完美无缺。

但问题,也正是源于它的完美无缺。

对一个应用程序进行性能分析时,我并不需要所有的 3,044 个细节采样点。我不需要知道第 1,847 个采样点在 00:02.847.882 捕捉到了 `libswiftCore.dylib` 内存地址为 `0x1027ec9a8` 的内容。我只需要知道 `heavyWork()` 花掉了 70% 的时间,而 `lightWork()` 只用了 30%。

用大白话来说:我需要的是 **10 行总结**,而不是 33,000 行繁文缛节。

## 为什么 XML 格式是正确的选择(但噪音不可取)

在有人提出“2026 年了还用 XML 才是问题”之前——并不是这样。

XML 对于 xctrace 的功能来说,是非常理想的格式。试想一下:

- **层次结构**:一个调用栈是一个框架的树状结构。一个采样点包含了一个调用栈,一个线程,一个进程。XML 自然而然地可以建模这些内容。
- **自描述性**:每个元素都有名字、带类型的属性,并且结构可以被验证。你不用去猜 CSV 第七列的内容代表什么。
- **优雅的去重**:xctrace 使用了 `id` 和 `ref` 系统,首次定义一个框架时是这样的:`id="59" name="heavyWork()"`,后续只需引用 `ref="59"`。可以看作是一种序列化的 *flyweight pattern*。
- **可以用标准工具解析**:XPath、`xmllint`、`xml.etree.ElementTree`…… 不需要专属解析器。

xctrace 的 XML 格式并不是*冗余*。它是 *Instruments* 所需的结构化信息,用来重建交互式调用树、对比运行情况,以及按线程和进程进行筛选。它是专门为一个可以展开和折叠节点的 GUI 工具设计的。

问题在于,试图将这些信息塞进大语言模型的上下文窗口时,情况就糟糕了。试图读取《堂吉诃德》全文,仅仅为了找到“风车”的那一句话,这样的信息虽然都在,但信号与噪声的比例使得有效提取变得几乎不可能。

## 解决方案:ztrace

于是我构建了一个名为 `ztrace` 的工具。这是一段 Python 脚本,用来接收 `.trace` 文件并生成一个精简版的摘要。

核心思路很简单:

1. 执行 `xctrace export --toc` 获取元数据(进程、时长、模板)
2. 执行 `xctrace export --xpath` 提取 `time-profile` 表格
3. 解析 XML 并解析 `id` 和 `ref` 系统
4. 过滤掉系统框架的调用(所有位于 `/usr/lib/` 或 `/System/` 下的部分)
5. 按函数聚合数据并生成摘要

注意这一点:第 3 步比看起来重要得多。xctrace 并不会在每次调用栈中重复定义一个框架,它只会首次定义然后通过 `ref="59"` 的引用。在不解析引用的情况下,你会丢失大部分信息。

## 结果

在一个用于测试的样例中(一个简单程序,`heavyWork()` 占用约 70%,`lightWork()` 占用约 30% 的 CPU):

$ 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 需要做性能分析时,流程如下:

1
2
3
4
5
6
7
# 1. 记录性能数据
xctrace record --template 'Time Profiler' --time-limit 5s --launch -- .build/debug/MyApp

# 2. 摘要(10 行总结,适合上下文窗口)
ztrace summary MyApp.trace

# 3. Claude 读取摘要,并建议优化方向

如果没有 ztrace,第 2 步会产生 30,000 行 XML,这要么会超出上下文窗口限制,要么使有用信息淹没在一堆无用信息中。而有了 ztrace,Claude 获取的正是它需要的关键信息,比如“70% 的 CPU 花在 heavyWork() 上,位于 Renderer.swift 的第 42 行”。

再次强调要点

ztrace 的诞生启示是,大语言模型在处理大规模原始数据时并不擅长。它擅长的是基于已经处理并提炼的信息进行推理。给 Claude 33,000 行 XML 就像让医生从 RAW 格式的核磁共振中直接诊断问题一样。医生需要的是清晰的图像,而不是散落的字节。

所以,下次当大语言模型告诉你“输出数据太庞大”时,不要想着用更大的上下文窗口模型去解决问题。相反,思考如何在数据送入模型之前,先将其浓缩加工为有用的信息。

归根究底,这正是工程师所做的:将无用信息转化为信号。无论是否有人工智能的参与。