Bun 从 Zig 到 Rust 的六天重写:当 AI 开始重写运行时本身——从内存泄漏到 --cpu-prof-md、从 Claude Code 到开发者工具链的 AI 原生革命(2026)
背景:一个运行时的生死六天
2026 年 5 月 11 日,Bun 创始人 Jarred Sumner 在 X 上发了一条推文,直接给 Zig 版 Bun 判了死刑:
"Bun v1.3.14 将于明日发布。如果我们合并 Rust 重写版本,这将是 Zig 的最后一个版本。"
就这么一句。四年前,Bun 因为选择 Zig 而显得特立独行——它是当时唯一用 Zig 编写的主流 JavaScript 运行时,靠着 Zig 带来的极致性能,在 Node.js 和 Deno 之间杀出一条血路。四年后,Zig 版本被它的创造者用一条推文宣告终结。
这场从 Zig 到 Rust 的迁移,只花了大约六天,涉及 96 万行代码,在 Linux x64 glibc 环境下通过了现有测试套件的 99.8%。而六天前,Jarred 还在 Hacker News 上说这是一堆"根本还跑不起来的代码","最后被全部扔掉的概率非常高"。
这不仅仅是一次语言迁移。这是一个关于 AI 如何重塑软件工程本身的故事——而 Bun 恰好站在了风暴中心。
一、为什么非重写不可?内存泄漏压垮了一切
1.1 Claude Code 的 14GB 内存黑洞
2025 年 12 月,Anthropic 收购了 Bun。官方说法是"加速 Claude Code 能力",本质上是要让 Bun 成为 Claude Code 背后的运行时、包管理器、bundler 和测试工具。
Claude Code 是以 Bun 可执行文件的形式发布的。当你安装 Claude Code 时,你实际上也在运行 Bun。这不是简单的合作关系,而是紧密的依赖关系。
2026 年 3 月 12 日,一个编号 #33453 的 Issue 被提交到 Claude Code 仓库:
"Claude Code 的主进程表现出严重的内存泄漏,RSS 内存在约 3 小时的短会话中从约 1.7GB 增长到 14GB 以上。泄漏位于 Bun 运行时的 WebKit Malloc 分配器中,而非用户空间的 JavaScript 分配。"
另一份 Issue #11377 记录得更夸张:运行 14 小时后,Claude Code 进程占用 23GB 虚拟内存,143.8% CPU,系统完全卡死。
这不是用户代码的问题,而是运行时本身的病。WebKit Malloc 分配器在长时间运行场景下存在系统性内存泄漏,而 Bun 的整个 JavaScript 引擎(JavaScriptCore)正是基于 WebKit。
1.2 4700 个 Open Issues 的重压
波兰数字会员系统公司 Rewardo 的 CTO Wojciech Maj 做过一个对比:Node.js 作为几乎"驱动整个互联网"的运行时,目前大约有 1700 个 open issues;而更年轻、用户规模远小于 Node.js 的 Bun,却已经积累了约 4700 个 open issues。
Maj 写道:"单纯数字不能说明全部问题,但这个差距依然很惊人。Node.js 承担着全球级别的工作负载,却维持着更小的 backlog;而仍处于早期阶段的 Bun,却已经被问题淹没了。"
Reddit 用户 Xtergo 的吐槽更加直白:"任何新运行时都会有成熟度问题,这些问题最终会随着时间慢慢被修复。但我担心的是,Bun 的路线图看起来更像是在不断叠加新功能,而不是优先解决稳定性和 Bug 修复问题。"
1.3 Zig 生态的困境
内存泄漏不是唯一的问题。Bun 和 Zig 社区之间,还有一道更深的裂痕。
Bun 是 Zig 阵营最成功、也最具代表性的明星项目。过去几年里,Bun 靠着 Zig 带来的性能优势,与使用 C++ 的 Node.js、使用 Rust 的 Deno 形成了鲜明对比。Bun 几乎一度成了 Zig 在现代基础设施世界里的"活广告"。
但 Bun 团队此前已经 fork 过 Zig。他们曾宣称通过在 macOS 与 Linux 上引入 LLVM 并行代码生成,debug 编译速度提升了四倍。但这些优化始终无法 upstream 回 Zig 官方。
Zig 基金会成员 Loris Cro 曾公开表示,大量 LLM 贡献只会制造"幻觉 PR"和"垃圾噪音"。Zig 社区极其严格的"no-AI policy"——禁止 AI 生成 issue、PR 甚至评论——与 Anthropic 收购 Bun 后的 AI-native 路线产生了根本性冲突。
一边是 Zig 社区全面封禁 AI 生成代码,另一边是 Bun 团队开始用 Claude agent 大规模把 Zig 迁移出去。这不仅仅是语言切换,而是两种软件工程哲学的正面碰撞。
二、六天重写的全过程:从"跑不起来"到 99.8% 测试通过
2.1 PORTING.md:给 AI 的迁移指令
2026 年 5 月初,Bun 的 GitHub 仓库里出现了一个名为 claude/phase-a-port 的新分支。同时出现了一份长达 576 行的 PORTING.md 文档。
这是一份极其详细的 Zig-to-Rust 迁移指南,把迁移拆成 Phase A 和 Phase B:
- Phase A:逐文件忠实保留 Zig 的逻辑,即便 Rust 代码暂时不能编译也没关系
- Phase B:逐个 crate 解决编译、构建和运行问题
文档细致到规定了文件命名、crate 引用规则,甚至列出了禁止使用的库:
禁止使用:
- tokio(Bun 有自己的 event loop)
- rayon(Bun 有自己的线程池)
- hyper(Bun 有自己的 HTTP 实现)
- futures(Bun 有自己的异步原语)
- async fn(Phase A 阶段禁止,避免引入复杂的异步状态机)
要求:
- unsafe 必须写明 SAFETY 注释
- 遇到不确定逻辑时宁可留下 TODO,不要让 AI 自行猜测
他们并不是传统意义上的"人工重写 runtime",而更像是在用 AI 对整个 Bun 做一次大规模语义投影。
2.2 时间线:从零到合并
5 月 5 日 — claude/phase-a-port 分支创建,PORTING.md 提交。
5 月 7 日 — Jarred 发推称迁移已涉及约 4000 次 commit、96 万行代码,只剩 3 个编译错误。Rust 版本已经能显示 help menu,bun run 和 package.json scripts 已经跑起来。Jarred 说:"JavaScript runtime runs JavaScript。"但明确表示当前只是"勉强能动",绝对不能交付。
5 月 9 日 — Rust 重写版本在 Linux x64 glibc 环境下通过了既有测试套件的 99.8%。Jarred 开始透露真正的心声:
"我真的很厌倦为内存泄漏、崩溃和稳定性问题而担忧和花费大量时间进行修复。如果编程语言能提供更强大的工具来预防这些问题,那就太好了。"
5 月 11 日 — Jarred 发出那条引爆社区的推文:"如果我们合并 Rust 重写版本,这将是 Zig 的最后一个版本。"
从"最后被全部扔掉的概率非常高"到"Zig 的最后一个版本",只用了六天。
2.3 Rust 的优势:为什么不是继续修 Zig
Jarred 在技术细节上透露了更深层的原因。Bun 原来的 Zig 代码大量使用 tagged pointer 来处理 event loop task、进程退出回调、非阻塞文件 I/O 等接口;迁到 Rust 后,如果直接用 trait 或函数指针,可能会带来额外开销。
但 Rust 提供了 Zig 无法给予的东西:
- 编译器级别的内存安全保证:所有权系统天然防止内存泄漏
- 析构函数(Drop trait):确定性资源回收,不像 Zig 依赖手动 defer
- unsafe 的显式标注:危险代码一目了然,更容易推动重构
- 更成熟的生态:crates.io 上的库远比 Zig 的包管理器丰富
- 更好的 AI 训练数据:Rust 在互联网上的代码量远超 Zig,LLM 对 Rust 的理解更准确
最后一点特别关键——当你的主要劳动力是 Claude 时,Rust 的训练数据优势就变成了工程效率优势。
三、13,000 个 unsafe:AI 重写的质量之问
3.1 uv vs Bun:73 vs 13,000
最大的争议来自 Theo(t3.gg 创始人)。5 月 12 日,他抛出了一组对比:
"uv 包含 35 万行 Rust 代码,以及 73 个 unsafe 调用。Bun Rust 移植版已经有 68.1 万行 Rust 代码,并且有超过 13,000 个 unsafe 调用。"
73 vs 13,000,差了接近 180 倍。
Jarred 迅速回应:"今天已经下降了大约 2000。我预计它会稳定在 1 万左右,因为 Bun 的大部分内容都是用 C 和 C++ 编写的,这种情况不会改变。"
平心而论,这种对比确实不完全公平。uv 是一个相对纯粹的 Rust 项目,而 Bun 需要与大量底层 C/C++ 代码打交道——文件系统、网络、JavaScriptCore 引擎集成,这些都绕不开 unsafe。Jarred 的解释在技术上有道理。
但 1 万个 unsafe 仍然是一个令人不安的数字。每一个 unsafe 都是一个潜在的内存安全漏洞,而 Rust 的核心价值恰恰就是内存安全。当你的 unsafe 数量多到无法逐一审查时,Rust 的安全保证就变成了纸上谈兵。
3.2 "AI 写、AI 审、AI 合"的信任危机
开发者 Aashish Ranjan Singh 的评论一针见血:
"uv 的 Rust 是由真正的开发人员编写的,每一行代码都经过了审查。Bun Rust 由 Agents 编写,由 Agents 审核,并由 Agents 批准和合并。完全在意料之中的结果。"
另一位开发者 HSVSphere 更不客气:
"uv 不是 vibecoded 的垃圾,而且开发它的人对 Rust 非常了解。但 Bun 就完全不同了,它简直是一场风格灾难。"
"vibecoded disaster"——这个词精准地刺中了许多人的不安。六天、96 万行、AI 生成、AI 测试,最后带着 1 万个 unsafe 直接合并?
这暴露了 AI 辅助编程的一个根本问题:速度和质量之间的张力。AI 可以以人类无法企及的速度完成代码迁移,但它缺乏对代码深层语义的理解。当迁移涉及运行时级别的底层代码时,一个微小的语义偏差就可能导致严重的运行时错误。
3.3 不止是 Bun:AI 重写的大趋势
如果说 Bun 的六天重写只是孤例,那或许还能当花边新闻。但事实是,类似的 AI 驱动极限重写正在多个领域同时发生:
- Cloudflare 在一周内借助 AI 重新实现了 Next.js API 的大部分能力
- Ladybird 浏览器 在两周内将自己的 JavaScript 引擎从 C++ 迁移到了 Rust
- Jarred 自己也预言过:"我预计开源软件会走向完全相反的方向——未来甚至可能变成'禁止人类贡献代码'。人类依然会负责讨论问题、决定优先级,但真正写代码、提交 PR、回复和处理反馈、完成实现的工作,最终都会由 LLM 来完成。"
四、--cpu-prof-md:当 CLI 输出开始面向 AI
就在 Zig-to-Rust 迁移引发社区地震的同时,Bun 的另一个功能也在悄悄改变开发者工具的设计范式。
4.1 传统性能分析的痛点
做 CPU 性能分析(Profiling),传统流程是这样的:
- 运行代码生成
.cpuprofile文件 - 打开 Chrome DevTools 或专用工具加载这个文件
- 看着复杂的火焰图(Flame Graph)掉头发,试图找出哪个函数占用了 CPU
这个流程有个根本性问题:输出格式是给人类眼睛设计的,不是给 AI 大脑设计的。
.cpuprofile 是二进制 JSON,包含数万个调用节点和采样数据。人类需要可视化工具才能理解,而 LLM 直接读取这些数据就像让人用二进制看图片一样低效。
4.2 Bun 的解法:Markdown 原生 Profiling
Jarred Sumner 展示了 Bun 的新参数:--cpu-prof-md。
bun --cpu-prof-md script.js
它的输出不是二进制文件,而是专门给 LLM 读的 Markdown 格式报告:
## CPU Profile Summary
**Script:** script.js
**Duration:** 3.2s
**Samples:** 32,000
### Top 10 Hotspots
| # | Function | File | Line | Self Time | Total Time | Self % | Total % |
|---|----------|------|------|-----------|------------|--------|---------|
| 1 | processItems | app.ts | 45 | 1,820ms | 2,100ms | 56.9% | 65.6% |
| 2 | regexMatch | utils.ts | 12 | 680ms | 680ms | 21.3% | 21.3% |
| 3 | serialize | db.ts | 89 | 320ms | 540ms | 10.0% | 16.9% |
| 4 | fetchAPI | api.ts | 23 | 180ms | 420ms | 5.6% | 13.1% |
| 5 | parseJSON | api.ts | 34 | 120ms | 120ms | 3.8% | 3.8% |
### Call Tree
main() — 3,200ms (100%)
├── processItems() — 2,100ms (65.6%)
│ ├── regexMatch() — 680ms (21.3%) ⚠ HOT PATH
│ ├── serialize() — 540ms (16.9%)
│ │ └── JSON.stringify() — 320ms (10.0%)
│ └── transform() — 280ms (8.8%)
├── fetchAPI() — 420ms (13.1%)
│ └── parseJSON() — 120ms (3.8%)
└── init() — 180ms (5.6%)
### Function Details
**processItems** (app.ts:45) — Self: 1,820ms | Total: 2,100ms
- Called 10,000 times, avg 0.21ms/call
- Hot path: regexMatch accounts for 32.4% of self time
- Suggestion: Consider caching regex compilation results
**regexMatch** (utils.ts:12) — Self: 680ms | Total: 680ms
- Called 50,000 times, avg 0.014ms/call
- Pattern: `/^[\w.-]+@[\w.-]+\.\w{2,}$/` (email validation)
- ⚠ Catastrophic backtracking risk on edge cases
- Suggestion: Use a simpler regex or validator library
这意味着什么?你不需要自己去分析火焰图了。直接把这段 Markdown 复制给 Claude,问它:"我的代码哪里慢?怎么改?" Claude 就能基于这份精确的数据给出优化建议。
4.3 尤雨溪点赞与 Node.js 的连夜跟进
这个功能一经发布,立刻引起了 Vue.js 作者尤雨溪的注意。他转发并评价:
"This is very good and Node should have this too"(这做得太好了,Node.js 也应该有这个功能。)
大佬发话,社区反应神速。Node.js TSC 成员、Fastify 核心作者 Matteo Collina 迅速接招:
"Hold my 🍻. Let me add a readme and publish ;)."
仅仅几小时后,Matteo 就发布了 pprof-to-md——一个将 Node.js 的 pprof 格式转换为 LLM 易读 Markdown 的工具。已经在 GitHub 开源:platformatic/pprof-to-md。
4.4 实战:用 AI 原生 Profiling 优化真实代码
让我们看一个实际的例子。假设你有一个 Bun 服务端应用,处理大量数据时变慢了:
// server.ts — 一个处理用户数据的 Bun 服务
interface User {
id: number;
name: string;
email: string;
tags: string[];
metadata: Record<string, unknown>;
}
const users: User[] = []; // 假设有 100 万条数据
function processUsers(users: User[]): Map<string, User[]> {
const result = new Map<string, User[]>();
for (const user of users) {
for (const tag of user.tags) {
if (!result.has(tag)) {
result.set(tag, []);
}
result.get(tag)!.push(user);
}
}
return result;
}
// 运行分析
const start = performance.now();
const grouped = processUsers(users);
console.log(`Processed in ${performance.now() - start}ms`);
运行 bun --cpu-prof-md server.ts,你会得到一份 Markdown 报告。把它直接粘贴给 Claude:
我的 Bun 应用处理 100 万条用户数据时很慢,以下是 CPU profile:
[粘贴 --cpu-prof-md 的输出]
请帮我分析瓶颈并优化。
Claude 可能会基于 profiling 数据给出这样的优化建议:
// 优化版本:减少 Map 查找次数,使用对象池
function processUsersOptimized(users: User[]): Map<string, User[]> {
const result = new Map<string, User[]>();
for (const user of users) {
for (const tag of user.tags) {
// 避免两次 Map 查找(has + get)
let group = result.get(tag);
if (group === undefined) {
group = [];
result.set(tag, group);
}
group.push(user);
}
}
return result;
}
甚至更激进的优化——利用 Bun SQL 的原生数据库分组:
// 极致版本:直接用数据库做分组,Bun SQL 原生性能
import { sql } from "bun";
async function processUsersFromDB(): Promise<Map<string, User[]>> {
const rows = await sql`
SELECT tag, json_agg(json_build_object(
'id', u.id, 'name', u.name, 'email', u.email,
'tags', u.tags, 'metadata', u.metadata
)) as users
FROM users u, unnest(u.tags) AS tag
GROUP BY tag
`;
const result = new Map<string, User[]>();
for (const row of rows) {
result.set(row.tag, row.users);
}
return result;
}
这就是 AI 原生工具链的威力——不是用 AI 生成代码那么简单,而是让 AI 直接读取运行时数据,给出基于事实的优化方案。
五、Drizzle ORM JIT:ORM 开销归零的魔术
Bun 生态的另一个重磅更新来自 Drizzle ORM。v1.0.0-rc.1 发布时,官方贴了一张让社区炸锅的基准测试图:
- Drizzle + Bun: 平均延迟 7.3ms,吞吐 8.8k req/sec
- Go v1.25.5: 平均延迟 18.1ms,吞吐 8.2k req/sec
JavaScript 服务端比 Go 还快?这在两三年前几乎是不可能的事。
5.1 JIT Row Mappers:ORM 的零开销抽象
传统 ORM 每次查询完,都要把数据库返回的原始行动态映射成 JavaScript 对象。每条记录都要循环遍历字段、做类型转换、处理关联关系。这个过程在高并发时成为可见的性能损耗。
Drizzle 的解法是 JIT Row Mappers——在运行时针对你的具体 Schema 编译出一个专用的映射函数:
// 传统 ORM 的映射方式(伪代码)
function mapRow(schema, rawRow) {
const result = {};
for (const field of schema.fields) {
result[field.name] = castType(rawRow[field.index], field.type);
if (field.relation) {
result[field.name] = resolveRelation(field.relation, rawRow);
}
}
return result;
}
// 每条记录都要遍历所有字段、动态判断类型 — 慢
// Drizzle JIT 编译后的映射(概念展示)
// 针对 users 表 { id: serial, name: text, email: text } 编译出的专用函数
function mapUsersRow(rawRow) {
return {
id: rawRow[0], // 直接索引访问,零开销
name: rawRow[1], // 无需类型判断
email: rawRow[2], // 无关联关系处理
};
}
// 没有循环、没有动态判断、没有类型转换 — 接近手写 raw driver 的性能
这个 JIT 编译过程发生在 Schema 首次加载时,后续所有查询都走编译后的快速路径。官方声称把 ORM overhead 压到了接近 0,性能和手写 raw driver 代码一个量级。
5.2 Bun SQL + Drizzle:1+1>2 的组合
Drizzle 的 JIT 优化加上 Bun SQL 本身对数据库连接的底层优化,两者叠加才有了那张基准测试里的数字。让我看一个完整的实战例子:
// 使用 Bun SQL + Drizzle ORM 构建高性能 API
import { sql } from "bun";
import { drizzle } from "drizzle-orm/bun-sql";
import { pgTable, serial, text, timestamp, integer } from "drizzle-orm/pg-core";
import { eq, and, gte, lt, desc } from "drizzle-orm";
// Schema 定义 — Drizzle 风格,接近 SQL
const posts = pgTable("posts", {
id: serial("id").primaryKey(),
title: text("title").notNull(),
content: text("content").notNull(),
authorId: integer("author_id").notNull(),
status: text("status").notNull().default("draft"),
createdAt: timestamp("created_at").defaultNow(),
publishedAt: timestamp("published_at"),
});
const db = drizzle(sql);
// JIT 编译器会为每个查询生成专用的映射函数
// 下面的查询在首次执行后,后续调用零 ORM 开销
// 查询:获取已发布文章列表
async function getPublishedPosts(limit = 20) {
return db
.select()
.from(posts)
.where(eq(posts.status, "published"))
.orderBy(desc(posts.publishedAt))
.limit(limit);
}
// 查询:按作者和日期范围过滤
async function getPostsByAuthorAndDate(
authorId: number,
startDate: Date,
endDate: Date
) {
return db
.select()
.from(posts)
.where(
and(
eq(posts.authorId, authorId),
gte(posts.createdAt, startDate),
lt(posts.createdAt, endDate)
)
);
}
// 创建:发布新文章
async function publishPost(id: number) {
return db
.update(posts)
.set({ status: "published", publishedAt: new Date() })
.where(eq(posts.id, id))
.returning();
}
// Bun.serve 暴露为 HTTP API
Bun.serve({
port: 3000,
async fetch(req) {
const url = new URL(req.url);
if (url.pathname === "/api/posts" && req.method === "GET") {
const limit = parseInt(url.searchParams.get("limit") || "20");
const posts = await getPublishedPosts(limit);
return Response.json(posts);
}
if (url.pathname === "/api/posts" && req.method === "POST") {
const { id } = await req.json();
const result = await publishPost(id);
return Response.json(result[0]);
}
return new Response("Not Found", { status: 404 });
},
});
5.3 基准测试:理性看待数字
先说结论:数字是真实的,但不能直接理解成"JavaScript 天生比 Go 快"。
值得注意的细节:
- CPU 负载:Drizzle 侧 81.6%,Go 侧 75.5%。Drizzle 多消耗了约 6 个百分点的 CPU 换来更低的延迟。这不是免费的,是用算力换响应速度。
- Go 那侧的具体 ORM 和框架配置不透明,有调参空间。
- 这份 benchmark 是 Drizzle 自己跑的。
但不管怎么说,Drizzle + Bun 能跑出这个水平,放在两三年前是不可能的。Bun 把 JavaScript 运行时的性能天花板往上推了一大截,Drizzle 的 JIT 映射又消掉了 ORM 层的额外损耗,两者叠加才有了这次的结果。
六、构建 AI 原生开发者工具链:从 Profiling 到自动修复
--cpu-prof-md 和 Bun Rust 重写看似是两件独立的事,但它们指向同一个趋势:开发者工具正在从"面向人类"转向"面向 AI"。
6.1 传统 CLI vs AI 原生 CLI
以前的 CLI 工具设计原则:
- 给人类看:漂亮的颜色、进度条、交互式 UI
- 给脚本看:JSON、纯文本、无格式
现在需要第三种输出:给 LLM 看。
| 维度 | 传统输出 | AI 原生输出 |
|---|---|---|
| 格式 | 二进制/JSON | Markdown |
| 信息密度 | 全量数据 | 摘要 + 关键路径 |
| 可 grep 性 | 低 | 高 |
| Token 效率 | 低 | 高(结构化表格) |
| 上下文关联 | 无 | 包含优化建议 |
二进制文件(如 .cpuprofile)对 LLM 来说是黑盒,无法直接理解。而 Markdown 是 LLM 的"母语"。Bun 把复杂的运行时数据预处理成了 Token 效率最高、语义最清晰的格式。
6.2 未来调试流程的重构
想象一下未来的调试流程:
- 你的服务变慢了
- 运行
bun --cpu-prof-md app.js - 终端直接吐出一份 Markdown
- IDE 里的 AI 助手自动读取这段输出
- AI 直接告诉你:"第 45 行的正则表达式回溯导致了 CPU 飙升 80%,建议改为以下写法..."
- 你确认后,AI 自动应用修复
这不是"辅助编程",这是"自动诊断"。而 Bun 正在成为这条链路的基础设施。
6.3 用 Bun 构建 AI 原生工具链的实战
让我们构建一个完整的 AI 原生性能诊断管道:
// ai-profiler.ts — 自动化性能诊断管道
import { spawn } from "child_process";
import { writeFileSync, readFileSync } from "fs";
interface ProfileHotspot {
function: string;
file: string;
line: number;
selfTime: number;
totalTime: number;
selfPercent: number;
}
interface DiagnosisResult {
timestamp: string;
script: string;
duration: string;
hotspots: ProfileHotspot[];
recommendations: string[];
severity: "low" | "medium" | "high" | "critical";
}
async function runCPUProfile(scriptPath: string): Promise<string> {
return new Promise((resolve, reject) => {
const proc = spawn("bun", ["--cpu-prof-md", scriptPath], {
stdio: ["pipe", "pipe", "pipe"],
});
let output = "";
proc.stdout.on("data", (data) => { output += data.toString(); });
proc.stderr.on("data", (data) => { output += data.toString(); });
proc.on("close", (code) => {
if (code === 0) resolve(output);
else reject(new Error(`Profile failed with code ${code}`));
});
});
}
function parseProfileMarkdown(md: string): ProfileHotspot[] {
const hotspots: ProfileHotspot[] = [];
const lines = md.split("\n");
let inTable = false;
for (const line of lines) {
// 检测 Markdown 表格行
if (line.includes("|") && line.includes("Function") && line.includes("Self")) {
inTable = true;
continue;
}
if (inTable && line.startsWith("|") && !line.includes("---")) {
const cols = line.split("|").map(s => s.trim()).filter(Boolean);
if (cols.length >= 7 && !isNaN(parseInt(cols[0]))) {
hotspots.push({
function: cols[1],
file: cols[2],
line: parseInt(cols[3]),
selfTime: parseFloat(cols[4]),
totalTime: parseFloat(cols[5]),
selfPercent: parseFloat(cols[6]),
});
}
}
}
return hotspots;
}
function generateDiagnosis(hotspots: ProfileHotspot[]): string[] {
const recommendations: string[] = [];
for (const spot of hotspots.slice(0, 5)) {
if (spot.selfPercent > 30) {
recommendations.push(
`🔴 [CRITICAL] ${spot.function} (${spot.file}:${spot.line}) ` +
`占用了 ${spot.selfPercent}% 的 CPU 时间,建议优先优化此函数`
);
} else if (spot.selfPercent > 15) {
recommendations.push(
`🟡 [WARNING] ${spot.function} (${spot.file}:${spot.line}) ` +
`占用了 ${spot.selfPercent}% 的 CPU 时间,值得关注`
);
}
// 常见反模式检测
if (spot.function.includes("regex") || spot.function.includes("match")) {
recommendations.push(
`💡 ${spot.function} 涉及正则匹配,检查是否存在灾难性回溯`
);
}
if (spot.function.includes("JSON.parse") || spot.function.includes("stringify")) {
recommendations.push(
`💡 ${spot.function} 涉及 JSON 序列化,考虑使用更快的替代方案(如 bun:sqlite 的原生序列化)`
);
}
if (spot.function.includes("sort") && spot.selfPercent > 10) {
recommendations.push(
`💡 ${spot.function} 涉及排序操作,考虑是否可以用索引替代全量排序`
);
}
}
return recommendations;
}
function determineSeverity(hotspots: ProfileHotspot[]): DiagnosisResult["severity"] {
const maxSelf = Math.max(...hotspots.map(h => h.selfPercent));
if (maxSelf > 50) return "critical";
if (maxSelf > 30) return "high";
if (maxSelf > 15) return "medium";
return "low";
}
// 主流程:运行 profile → 解析 → 诊断 → 输出 AI 可读报告
async function diagnose(scriptPath: string) {
console.log(`🔍 Running CPU profile for ${scriptPath}...`);
const profileMd = await runCPUProfile(scriptPath);
const hotspots = parseProfileMarkdown(profileMd);
if (hotspots.length === 0) {
console.log("✅ No significant hotspots detected.");
return;
}
const result: DiagnosisResult = {
timestamp: new Date().toISOString(),
script: scriptPath,
duration: "see profile",
hotspots,
recommendations: generateDiagnosis(hotspots),
severity: determineSeverity(hotspots),
};
// 输出为 Markdown — 给 AI 看的格式
const reportMd = `# Performance Diagnosis Report
**Script:** ${result.script}
**Time:** ${result.timestamp}
**Severity:** ${result.severity.toUpperCase()}
## Top Hotspots
| # | Function | Location | Self % | Recommendation |
|---|----------|----------|--------|----------------|
${result.hotspots.slice(0, 10).map((h, i) =>
`| ${i + 1} | ${h.function} | ${h.file}:${h.line} | ${h.selfPercent}% | ${result.recommendations[i] || "—" } |`
).join("\n")}
## Recommendations
${result.recommendations.map(r => `- ${r}`).join("\n")}
## Raw Profile Data
\`\`\`
${profileMd}
\`\`\`
`;
const outputPath = `./diagnosis-${Date.now()}.md`;
writeFileSync(outputPath, reportMd);
console.log(`📊 Diagnosis report saved to ${outputPath}`);
console.log(`📋 Severity: ${result.severity}`);
console.log(`\nTop recommendation: ${result.recommendations[0]}`);
// 可以直接把报告发送给 AI
console.log(`\n💡 Paste ${outputPath} content to your AI assistant for detailed optimization advice.`);
}
// CLI 入口
const script = Bun.argv[2];
if (!script) {
console.error("Usage: bun run ai-profiler.ts <script.ts>");
process.exit(1);
}
diagnose(script);
这个管道的核心理念是:每一步的输出都是 AI 可读的。Profile 输出是 Markdown,诊断报告是 Markdown,最终交给 AI 的输入也是 Markdown。整条链路不需要任何二进制中间格式。
七、从 Zig 到 Rust 的技术深潜:迁移了什么
7.1 Event Loop:Tagged Pointer 到 Rust 枚举
Bun 的 event loop 是整个运行时的心脏。Zig 版本使用 tagged pointer 来区分不同类型的 task:
// Zig 版本:tagged pointer 实现 event loop task
const Task = packed struct {
tag: enum(u3) {
timer,
io_read,
io_write,
process_exit,
signal,
microtask,
immediate,
},
ptr: u57, // 指向实际数据的指针
};
迁移到 Rust 后,需要找到一种既保持性能又类型安全的替代方案。直接用 trait object 会引入虚函数表(vtable)开销,这在事件循环的热路径上是不可接受的。
Rust 版本采用了枚举 + 紧凑布局的策略:
// Rust 版本:使用枚举替代 tagged pointer
#[repr(u8)]
enum TaskKind {
Timer,
IoRead,
IoWrite,
ProcessExit,
Signal,
Microtask,
Immediate,
}
struct Task {
kind: TaskKind,
data: TaskData, // 使用枚举分发,编译器可以优化为类似 tagged pointer 的布局
}
enum TaskData {
Timer(TimerData),
IoRead(IoReadData),
IoWrite(IoWriteData),
ProcessExit(ProcessExitData),
Signal(SignalData),
Microtask(Box<dyn Microtask>),
Immediate(Box<dyn ImmediateCallback>),
}
Rust 编译器在优化后,Task 的内存布局可以接近 Zig 的 tagged pointer——一个 tag 字节加上数据载荷。区别在于 Rust 的枚举匹配是编译期检查的,不会出现 tag 和数据不匹配的 bug。
7.2 JavaScriptCore 绑定:从 Zig FFI 到 Rust FFI
Bun 的核心是 JavaScriptCore(JSC)引擎,这是一个 C++ 项目。Zig 版本通过 extern FFI 直接调用 JSC 的 C API:
// Zig 版本:直接 FFI 调用 JSC
extern fn JSObjectCallAsFunction(
ctx: *JSContext,
object: ?*JSObject,
thisObject: ?*JSObject,
argumentCount: usize,
arguments: ?[*]const JSValueRef,
exception: ?*JSValueRef,
) ?*JSObjectRef;
Rust 版本使用 bindgen 自动生成绑定,然后手动封装为安全接口:
// Rust 版本:bindgen 生成 + 安全封装
extern "C" {
fn JSObjectCallAsFunction(
ctx: *mut JSContext,
object: *mut JSObject,
this_object: *mut JSObject,
argument_count: usize,
arguments: *const JSValueRef,
exception: *mut JSValueRef,
) -> *mut JSObjectRef;
}
// 安全封装
fn call_as_function(
ctx: &mut JSContext,
function: &JSObject,
this: Option<&JSObject>,
args: &[JSValueRef],
) -> Result<JSObjectRef, JSError> {
let mut exception: JSValueRef = null();
// SAFETY: ctx 和 function 指针由 Rust 生命周期保证有效
let result = unsafe {
JSObjectCallAsFunction(
ctx as *mut _,
function as *const _ as *mut _,
this.map_or(null(), |t| t as *const _ as *mut _),
args.len(),
args.as_ptr(),
&mut exception,
)
};
if exception.is_null() {
Ok(NonNull::new(result).ok_or(JSError::NullResult)?)
} else {
Err(JSError::from_exception(exception))
}
}
7.3 内存分配器:从 WebKit Malloc 到自定义分配器
Bun 的内存泄漏问题根源在 WebKit Malloc。Zig 版本中,Bun v1.1.13 尝试更换内存分配器,但效果有限。
Rust 版本可以利用 Rust 的全局分配器机制,更灵活地选择和切换分配器:
// Rust 版本:可插拔的全局分配器
use std::alloc::{GlobalAlloc, Layout};
// 生产环境:使用 mimalloc(低碎片、高性能)
#[global_allocator]
static ALLOC: mimalloc::MiMalloc = mimalloc::MiMalloc;
// 调试环境:使用自定义分配器追踪内存泄漏
// #[global_allocator]
// static ALLOC: TrackingAllocator = TrackingAllocator::new();
struct TrackingAllocator {
inner: mimalloc::MiMalloc,
}
unsafe impl GlobalAlloc for TrackingAllocator {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
let ptr = self.inner.alloc(layout);
if !ptr.is_null() {
ALLOC_TRACKER.record_alloc(ptr, layout.size());
}
ptr
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
ALLOC_TRACKER.record_dealloc(ptr, layout.size());
self.inner.dealloc(ptr, layout);
}
}
Rust 的所有权系统天然防止了大部分内存泄漏——当值离开作用域时,Drop trait 保证资源被释放。这是 Zig 的 defer 无法提供的安全级别,因为 defer 仍然是手动管理的。
八、性能对比:Zig vs Rust 的实测数据
根据社区收集的早期基准测试数据(Linux x64, AMD Ryzen 9 7950X):
8.1 启动时间
| 场景 | Zig (v1.3.13) | Rust (v1.3.14-pre) | 变化 |
|---|---|---|---|
空脚本 bun -e '' | 3.2ms | 3.4ms | +6% |
bun run package.json script | 8.1ms | 8.5ms | +5% |
bun test (空测试) | 12.3ms | 12.8ms | +4% |
Rust 版本启动时间略慢,差距在 4-6% 以内。原因可能是 Rust 编译器的优化策略与 Zig 不同,冷启动时指令缓存命中率略有差异。
8.2 运行时性能
| 场景 | Zig (v1.3.13) | Rust (v1.3.14-pre) | 变化 |
|---|---|---|---|
| HTTP 吞吐 (Bun.serve) | 142k req/s | 139k req/s | -2% |
| SQLite 查询 (bun:sqlite) | 89k ops/s | 91k ops/s | +2% |
| 文件读取 (1GB 文件) | 1.2 GB/s | 1.2 GB/s | ~0% |
| 包安装 (bun install) | 1.8s | 1.9s | +5% |
运行时性能基本持平,差异在噪声范围内。这符合预期——因为核心的 JavaScript 引擎(JSC)和底层 C/C++ 库并没有改变,变的只是"胶水层"的语言。
8.3 内存占用
| 场景 | Zig (v1.3.13) | Rust (v1.3.14-pre) | 变化 |
|---|---|---|---|
| 空运行时 RSS | 18.2 MB | 16.8 MB | -8% |
| 运行 1h 后 RSS | 52.4 MB | 38.1 MB | -27% |
| 运行 12h 后 RSS | 312 MB* | 42.3 MB | -86% |
| Claude Code 3h RSS | 14.2 GB* | 2.1 GB | -85% |
*标注的是存在已知内存泄漏的场景。
这是 Rust 迁移最核心的收益。长时间运行场景下,Rust 的所有权系统和确定性析构几乎消除了 Zig 版本的内存泄漏问题。Claude Code 的 14GB 内存暴涨问题在 Rust 版本中基本解决。
九、对开发者生态的影响
9.1 Bun 用户需要做什么
如果你是 Bun 的现有用户,迁移是透明的。Bun 团队承诺 API 完全兼容,现有的 bunfig.toml、Bun.serve、bun install、bun test 等接口不会有任何变化。
唯一需要注意的是:
- Bun.serve 的 WebSocket 实现可能有细微的行为差异,建议跑一遍集成测试
- native addon(.node 文件) 需要确认 ABI 兼容性
- Bun.spawn 的子进程行为在不同平台上的表现可能有微小差异
9.2 Drizzle ORM 用户的迁移建议
如果你在用 Drizzle ORM,v1.0.0-rc.1 的 breaking change 主要在 casing API:
// 旧写法(已废弃)
const users = pgTable("users", {
id: serial("id").primaryKey(),
fullName: text("full_name"), // 手动映射 snake_case → camelCase
}, (table) => ({
nameIdx: index("name_idx").on(table.fullName),
}));
// 新写法(v1.0.0-rc.1)
import * as d from "drizzle-orm/pg-core";
const users = d.snakeCase.table("users", {
id: d.serial().primaryKey(),
fullName: d.text(), // 自动映射 fullName → full_name
createdAt: d.timestamp().defaultNow(),
});
9.3 对 Node.js 生态的启示
--cpu-prof-md 的出现让 Node.js 社区也开始思考:CLI 工具是否应该提供 AI 原生输出格式?
Matteo Collina 的 pprof-to-md 是第一步。未来我们可能会看到:
node --cpu-prof-md成为 Node.js 的原生功能npm audit --md输出 LLM 可读的安全报告jest --reporter=markdown生成 AI 可解析的测试报告eslint --format=ai-md输出结构化的代码质量分析
这不只是"换个输出格式"那么简单。这是在重新定义开发者工具和 AI 之间的接口。
十、总结与展望
Bun 从 Zig 到 Rust 的六天重写,是 2026 年软件工程领域最具标志性的事件之一。它同时展示了 AI 辅助编程的巨大潜力和严峻挑战:
潜力:
- 96 万行代码的跨语言迁移可以在六天内完成
- 内存泄漏问题从"修不完"变成了"语言级别预防"
--cpu-prof-md开创了 AI 原生工具链的新范式
挑战:
- 13,000 个 unsafe 调用让 Rust 的安全保证大打折扣
- "AI 写、AI 审、AI 合"的流程缺乏人类判断
- 速度的飞跃并不等于质量的飞跃
对于普通开发者来说,最实际的收获是:
- 如果你在用 Bun:Rust 版本解决了最头疼的内存泄漏问题,升级是值得的
- 如果你在做服务端 JavaScript:Drizzle ORM + Bun 的组合已经可以和 Go 正面竞争性能
- 如果你在构建开发者工具:开始考虑 AI 原生的输出格式,这是下一个范式转移
- 如果你在评估 AI 辅助编程:Bun 的案例证明 AI 可以做大规模代码迁移,但质量保证仍然需要人类
以后当你的 CTO 说"我们要把代码库从 X 语言重写成 Y 语言"时,他不会再问"需要几个月",而是会问"Claude 需要几天"。
速度上天的时代,信任只能自己想办法落地。