基于 codeagents 全部性能相关 item 的统一 roadmap —— 按 ROI 排序的可执行优化清单。
数据来源:改进报告 P0/P1 引擎优化 + P2 性能优化 + 实际源码审计 + 已合并 PR 度量数据
5 轮审计共修正 11 处事实/方法论错误:
- v1 → v2 修正 7 处事实错误
- v2 → v3 修正 1 处(warmAll 删除过激)
- v3 → v4 修正 1 处(项 ① 实现复杂度低估)
- v4 → v5 修正 1 处(PR#3604 误判为 MERGED)
- v5 → v6 修正 1 处(项 ⑦ in-flight 合并被高估)
本版(v6)完整问题清单:
| v1 错误 | 实际情况 | 影响 |
|---|---|---|
| 声称 MCP 服务器串行启动 | tools/mcp-client-manager.ts:137,459,514 已用 Promise.all(discoveryPromises) 并行 |
项 ④ 改为:MCP 已并行,仅"扩展加载默认路径"和 LSP 还有串行问题 |
代码示例用 lru-cache / p-map |
这两个包都不在 packages/core/dependencies 里;p-limit@7.3.0 在 packages/cli 里 |
改用 Map 实现 LRU 或加 dep;并行用 p-limit |
| 声称工具是"同步加载" | 工具已经 lazy-loaded(config.ts:2580-2628 全部 await import('../tools/...')) |
项 ⑤ 改为:toolRegistry.warmAll() 反而 eager 加载所有 lazy 工具——真正的 gap 是避免 warmAll,让 first-use 触发 import |
| 漏看 readManyFiles 同步 I/O | readManyFiles.ts:107 仍有 existsSync + statSync(PR#3581 没改到这处) |
项 ③ 增加:把 sync I/O 改 async + 同时 32 批并行 |
| 声称 PR#3013 已合并("SlicingMaxSizedBox + useStableHeight") | 实际 PR#3013 CLOSED(2026-04-24 未合并)。flicker 由 PR#3591 (MERGED 2026-04-25) 处理。MaxSizedBox 基础设施来自上游 Gemini CLI(PR#1217 等)非 PR#3013 |
已完成基线表移除 PR#3013,改为标注 MaxSizedBox 来自 upstream + PR#3591 是真正的 flicker foundation |
声称 <available_skills> 注入到 system prompt |
实际是注入到 SkillTool description 字段(tools/skill.ts:182),随 tool schema 在每次 API 请求中发送 |
项 ① 集成点改为 tools/skill.ts 而非 prompts.ts |
| 没区分 PR#3636 vs ⑦ | PR#3636 是 "concurrency cap"(上限),项 ⑦ 是 "request dedupe"(合并),是不同概念 | 项 ⑦ 加注:与 PR#3636 共存,不冲突 |
| v2 项 ⑤ 建议"删 warmAll"过于激进 | getFunctionDeclarations() 只返回已加载工具,完全删 warmAll 会导致 tool schema 缺失 |
v3 项 ⑤ 改为"跨 session 缓存 warmAll + lazy 静态 import",避免破坏 schema 契约。长期方案是 ToolSearch(PR#3589 CLOSED) |
| v3 项 ① 实现复杂度被低估 | SkillTool description 在 updateDescriptionAndSchema() 重建(仅 skill 集合变化时),不是 per-turn 触发。如果改 per-turn 重建 + setTools,会引入额外 setTools 开销,可能负优化 |
v4 项 ① 改为:保留 SkillTool description baseline,在 attachments/system-reminder 层做去重;新增风险提示(模型可能漏调 skill) |
| v4 把 PR#3604 当成 MERGED 列入基线 | 实际 PR#3604 仍 OPEN(截至 2026-04-28),item-28 子项 #1/#2/#6 还在 review 中未合并 | v5 移出"已完成基线"区块,加入新建的"🟡 OPEN / 进行中"区块;item-28 部分覆盖表标注"PR#3604 仍 OPEN" |
| v4 项 ⑦ "in-flight 请求合并"被高估 | AI 模型调用通常带唯一 conversation context(history + tools 各异),完全相同的并行请求较少。模型层 dedup hit rate 通常很低 | v5 加注:"工具调用层 dedup(多个 subagent 并行读同一文件)才是真正高价值场景"——模型层是次要 |
剩余声明(① sentSkillNames / ② FileReadCache / ⑥ Git execSync / ⑦ in-flight coalesce)经审计确认正确。
✅ 已完成(基线)
PR#3581 · sync I/O hot path 110→10 (-91%)
PR#3591 · TUI flicker foundation
MaxSizedBox · 渲染前裁剪基础设施(来自 upstream,非 PR#3013)
❌ CLOSED / 🟡 OPEN(不算基线)
PR#3013 · CLOSED(2026-04-24 未合并;flicker 由 PR#3591 解决)
PR#3604 · 🟡 OPEN(item-28 子项 1+2+6 仍待 review,截至 2026-04-28)
PR#3589 · CLOSED(ToolSearch 方向被关闭)
🥇 P0 本周(建议)
① sentSkillNames 去重 (system-reminder 层) 80-150 行 每轮省 1K token *
② FileReadCache 内容层(Map LRU 无 dep) 150 行 Read-Edit 循环零 I/O
③ readManyFiles 32 批并行 + 同步 I/O 异步 80 行 多文件 I/O 1/32 + 消除 PR#3581 漏改的 sync I/O
🥈 P1 下周
④ Extension 默认目录加载并行 + LSP 并行 50 行 启动 -1-3s
(注:MCP discovery 已 Promise.all 并行,无需改)
⑤ warmAll 短路 + lazy 静态 import 80 行 冷启动 -200ms *
(注:工具已 lazy,但 setTools 每次 warmAll;不能完全删 warmAll
会破坏 schema 契约;保守做法是 factories.size === 0 short-circuit)
⑥ Git 直读避免 spawn (execSync) 100 行 状态栏 -30ms
⑦ in-flight 请求合并 (request dedup) 80 行 并行 subagent 节省 API
(注:与 PR#3636 OPEN "concurrency cap" 不同,可共存)
* 标注的数字为基于源码结构的合理估算,需 PR 阶段 tracer 实测验证
🥉 P2-P3 按需
⑧ React.memo 高频组件 / ⑨ WeakRef 长 session 内存 / ⑩ 正则编译缓存 /
⑪ Shell AST 解析缓存 / ⑫ 终端行宽缓存 / ⑬ Bun 原生 API 等
总投入:P0+P1 约 8-10 天 × 1 人 / ~590 行代码,预期收益:每轮节省 1K+ tokens、冷启动 -300ms、文件 I/O 1/32 + 消除 sync I/O、Extension 启动 -1.6s、状态栏 -30ms、并行 subagent 节省 API 成本。
这些 PR 已合并 / 进行中,不要再重复实现。
| PR | 内容 | 度量 |
|---|---|---|
| PR#3581 | sync I/O hot path | 110→10 syscall/prompt(-91%) |
| PR#3591 | TUI flicker foundation | throttle + pre-slice + soft-wrap 抑制 + 同步终端 allowlist |
| MaxSizedBox 基础(自 Gemini upstream PR#1217 等多 PR) | 渲染前裁剪到 maxLines(基础设施层) | 已有 fork 期前的能力 |
| PR | 内容 | 当前状态 |
|---|---|---|
| PR#3604 🟡 OPEN | Skill 并行加载 + path-conditional 激活 | 截至 2026-04-28 仍 OPEN,对应 item-28 子项 #1+#2+#6 |
| PR#3636 🟡 OPEN | cap concurrent in-flight requests per provider | 与项 ⑦ 共存方向 |
| PR | 关闭时间 | 原因推测 |
|---|---|---|
| PR#3589 | 2026-04-24 08:51 UTC | ToolSearch 设计被否定,schema 按需加载方向卡住 |
| PR#3013 | 2026-04-24 09:27 UTC | SlicingMaxSizedBox 第二尝试被否定,flicker 由 PR#3591 解决 |
| Item | 已完成 | 仍缺 |
|---|---|---|
| item-2 文件读取缓存 | 查询层 LRU(PR#3581) | 内容层 FileReadCache + 32 批并行 |
| item-28 Skill 装载性能 | PR#3604 仍 OPEN(子项 #1/#2/#6 待合并) | 子项 #3/#4/#5/#7/#8/#9 仍未做 |
问题:每个 API 调用把全部 skill 列表通过 SkillTool description 注入(tools/skill.ts:182-184 <available_skills>${skillDescriptions}</available_skills>)。100 skill = 600-1500 tokens × N API call。
审计修正(v3):实现比 v1/v2 描述的复杂。SkillTool description 在
updateDescriptionAndSchema()重建后通过geminiClient.setTools()推送给模型,默认只在 skill 集合变化时调用,不是 per-turn。因此:
- ❌ 不能简单 per-turn 重建 description —— 会引入额外 setTools 开销
- ✓ 应该走 attachments / system-reminder 层 —— 不动 SkillTool description(保留 1 次 baseline 注入),但通过
<system-reminder>在转 turn 时注入"新 skill 通知"- ✓ 维持兼容性:模型仍可通过 SkillTool 调用所有 skill,只是描述层省 token
解决方案(对标 Claude utils/attachments.ts:2607):
// packages/core/src/skills/sentSkillNames.ts (新建)
const sentSkillNames = new Map<string, Set<string>>() // agentId → already-sent
export function getSkillListingDelta(
agentId: string,
allSkills: string[]
): string[] {
const sent = sentSkillNames.get(agentId) ?? new Set()
if (!sentSkillNames.has(agentId)) sentSkillNames.set(agentId, sent)
const newOnes = allSkills.filter(name => !sent.has(name))
newOnes.forEach(name => sent.add(name))
return newOnes // 只返回未发送过的
}
export function resetSentSkillNames(): void { sentSkillNames.clear() }正确集成点(v3 修订后):
- 不改 SkillTool description(保持 baseline)
- 在 prompt assembly 路径(attachments / system-reminder injection)按 agentId 过滤,只在新 skill 出现时注入
<system-reminder>system: 3 个新 skill 已可用:xlsx, pdf, debug</system-reminder> - skill watcher 触发 reload 时调用
resetSentSkillNames() - subagent spawn 时为新 agentId 创建独立 Set(继承父 agent 的 sent Set 作为 baseline)
/clear路径 reset--resume路径检测 transcript 已含 skill listing,调suppressNext
实施风险提示:
⚠️ 模型对"什么时候有什么 skill 可用"的认知如果有断层(skill 注入了但模型没记住),可能漏调;保留 SkillTool description 的 baseline 是为了兜底⚠️ 单做"省 token"是局部优化;如果模型用 ToolSearch 类机制访问 skill schema,整体效果更好(参考项 ⑤ 的 5d)
度量:
# 注入 token 数(before/after)
QWEN_DEBUG_PROMPT_SIZE=1 qwen ...预期收益:100 skill 用户每轮省 ~1K token × 10 turn = 每会话省 ~10K token。
问题:Agent 频繁 Read+Edit 同一文件 —— Read 后 Edit,Edit 后再 Read 验证 —— 每次都从磁盘读,浪费大量 I/O。
解决方案(对标 Claude utils/fileReadCache.ts):
依赖说明:
packages/core当前没有lru-cache依赖,需要选一个:①加 dep;②自己实现简易 LRU(Map + insertion order)。下面给出无依赖版本:
// packages/core/src/utils/fileReadCache.ts (新建)
import * as fs from 'node:fs/promises'
interface CachedRead {
content: string
mtime: number
size: number
}
const MAX = 1000
const cache = new Map<string, CachedRead>() // Map 保留 insertion order
export async function readWithCache(filePath: string): Promise<string> {
const stat = await fs.stat(filePath)
const cached = cache.get(filePath)
if (cached && cached.mtime === stat.mtimeMs && cached.size === stat.size) {
// LRU: 命中后移到末尾
cache.delete(filePath)
cache.set(filePath, cached)
return cached.content
}
const content = await fs.readFile(filePath, 'utf-8')
if (cache.size >= MAX) {
// 删除最早的 entry(Map iteration order = insertion order)
const oldestKey = cache.keys().next().value
if (oldestKey) cache.delete(oldestKey)
}
cache.set(filePath, { content, mtime: stat.mtimeMs, size: stat.size })
return content
}
export function invalidateAfterWrite(filePath: string): void {
cache.delete(filePath)
}集成点:
tools/read.tsreadFile()改用readWithCache()tools/edit.ts/tools/write.ts写入后调invalidateAfterWrite()tools/multiedit.ts同上
度量:
# tracer 追 readFile 调用次数
NODE_OPTIONS='--require trace-readfile.cjs' qwen -p "..."预期收益:Edit-then-Read(Agent 常见验证模式)从 2 次 I/O → 1 次。10K 行长 session 节省 50%+ 文件 read I/O。
问题:packages/core/src/utils/readManyFiles.ts:104-127 有两个问题:
for (const rawPattern of inputPatterns)串行 —— 50 文件 = 50× 累加延迟readManyFiles.ts:107仍有 sync I/O:fs.existsSync(fullPath) ? fs.statSync(fullPath) : null—— PR#3581 没覆盖这处(PR#3581 改的是chatRecordingService+fileUtils+paths+workspaceContext+ripGrep)
解决方案:
// packages/core/src/utils/readManyFiles.ts:104(改造前)
for (const rawPattern of inputPatterns) {
const fullPath = path.resolve(projectRoot, normalizedPattern)
const stats = fs.existsSync(fullPath) ? fs.statSync(fullPath) : null // ← sync I/O
if (stats?.isDirectory()) { ... }
if (stats?.isFile() && !seenFiles.has(fullPath)) {
seenFiles.add(fullPath)
const readResult = await readFileContent(config, fullPath) // ← 串行
// ...
}
}
// 改造后:
const BATCH_SIZE = 32
const resolvedPatterns = inputPatterns.map(p => ({
raw: p,
full: path.resolve(projectRoot, p.replace(/\\/g, '/')),
}))
// 第一阶段:并行 stat(替代 sync existsSync + statSync)
const statResults = await Promise.all(
resolvedPatterns.map(async ({ full }) => {
try {
return { full, stats: await fs.promises.stat(full) }
} catch (err: any) {
if (err.code === 'ENOENT') return { full, stats: null }
throw err
}
})
)
// 第二阶段:分类 + 32 批并行读取
const fileItems = statResults.filter(r => r.stats?.isFile() && !seenFiles.has(r.full))
const dirItems = statResults.filter(r => r.stats?.isDirectory())
for (let i = 0; i < fileItems.length; i += BATCH_SIZE) {
const batch = fileItems.slice(i, i + BATCH_SIZE)
const results = await Promise.all(
batch.map(({ full }) => {
seenFiles.add(full)
return readFileContent(config, full) // 配合 ② 用 readWithCache
})
)
// 合并 contentParts / files
}
// 目录递归保持顺序(避免内存爆炸)
for (const { full } of dirItems) {
const { contentParts: dp, info } = await readDirectory(config, full)
contentParts.push(...dp)
files.push(info)
}度量:
time qwen -p "Read all .ts files in src/ and summarize"预期收益:50 文件场景从 ~250ms → ~25ms(10× 加速)+ sync I/O 消除。配合 FileReadCache 后续访问 0ms。
审计修正:
- MCP discovery 已经并行(
tools/mcp-client-manager.ts:137,459,514用Promise.all(discoveryPromises))。不需要改 MCP - 真正的 gap 在两处:
extension/extensionManager.ts:559 默认路径加载是串行:
// 改造前(串行)
extensions = []
for (const subdir of subdirs) {
const extension = await this.loadExtension({ extensionDir, ... }) // ← 串行
if (extension) extensions.push(extension)
}
// 改造后(并行,10 extensions × 200ms 串行 → 200ms 并行)
import pLimit from 'p-limit' // packages/cli 已有 p-limit@7.3.0,但 core 需加 dep
const limit = pLimit(5)
extensions = (await Promise.all(
subdirs.map(subdir => limit(() => this.loadExtension({
extensionDir: path.join(userExtensionsDir, subdir),
workspaceDir: this.workspaceDir,
})))
)).filter((e): e is Extension => e !== null)命名查找路径(
extensionManager.ts:545)已经是Promise.all,仅默认全量加载是串行。
# 验证命令
grep -n "for.*await\|Promise.all" packages/core/src/lsp/*.ts | grep -v test如果 LSP 启动是串行(待审计),加 pLimit(3) 并行启动 TypeScript / Python / Go。
预期收益:10 个用户级 extension 串行 2s → 并行 ~400ms(-1.6s 启动)。
审计修正(v2 → v3):Qwen Code 工具已经 lazy-loaded(config/config.ts:2580-2628 全部 await import('../tools/...')),但 toolRegistry.warmAll() 在每次 setTools() 时都把所有 ~30 个工具 import 一遍:
// 当前问题:3 处都在 init 路径调 warmAll()
// packages/core/src/core/client.ts:231 await toolRegistry.warmAll() ← 每次 setTools
// packages/core/src/core/geminiChat.ts:948 await toolRegistry.warmAll()
// packages/core/src/agents/runtime/agent-core.ts:307 await toolRegistry.warmAll()
⚠️ 注意:完全删除warmAll()不可行 ——getFunctionDeclarations()(line 550-555) 只读this.toolsMap(已加载的工具),如果不 warmAll,发送给模型的 tool schema 列表会残缺,导致模型无法用未加载的工具。这正是 PR#3589 ToolSearch 已 CLOSED 想解决的根本问题:把 schema 发送改为按需。但 PR#3589 被关闭,本路径阻塞中。
可行的两个解决方案:
warmAll() 第一次进程内调用后,所有 import 已完成。后续 session 的 setTools() 不需要重新 import(只是 Promise.all over this.factories Map,但 factories 已经空了所以是 no-op)。实测看 warmAll 后续调用是否真的零成本:
# 在 client.ts:231 加 timing
console.time('warmAll')
await toolRegistry.warmAll()
console.timeEnd('warmAll')如果第二次以后还是慢,可能是 Promise.allSettled 本身的 overhead,加快速 short-circuit:
// tool-registry.ts:warmAll 增强
async warmAll(options?: { strict?: boolean }): Promise<void> {
if (this.factories.size === 0) return // ← 已全部 warmed,直接返回
// 原有逻辑保留
}如果 factories 在每次 setTools 之间被重置,需要审计 setTools 调用栈找出重置点。
// 当前
import { ArenaAgentClient } from '../agents/arena/ArenaAgentClient.js' // 必加载
// 改为:
let _ArenaAgentClient: typeof import('../agents/arena/ArenaAgentClient.js').ArenaAgentClient | undefined
async function getArenaAgentClient() {
if (!_ArenaAgentClient) {
_ArenaAgentClient = (await import('../agents/arena/ArenaAgentClient.js')).ArenaAgentClient
}
return _ArenaAgentClient
}只在用户用 Arena 模式(--arena)时加载这个相对重的模块。
对标 Gemini PR#25758 backport item-58,把启动期的远程 fetch 改为不阻塞 bootstrap。
虽然 PR#3589 被 CLOSED,根本性的解决方案仍是按需加载 schema。可以做一个更小、风险更低的变体:
- 默认所有工具 schema 都注入(保持向后兼容)
- 加
tools.deferredSchemas: ['mcp__*', 'cron_*']配置项,让用户可选手动 defer 某些工具 - 配合 ToolSearch 工具让模型按需获取 deferred schema
度量:
NODE_OPTIONS='--require trace-startup.cjs' qwen --version
NODE_OPTIONS='--require trace-startup.cjs' qwen -p "..." | grep "warmAll"预期收益:5a 短路返回省 ~50ms × N session;5b 省 ~150ms cold start;5d 是长期 -1K+ tokens/request。
风险提示:直接删除 warmAll 会让某些工具的 schema 缺失,不推荐这种激进改动。
问题:状态栏 / commit attribution / git context 注入都用 spawn('git status') —— 进程开销 ~30ms × N。
解决方案(对标 p2-perf item-11):
// utils/gitDirect.ts (新建)
export async function readHEAD(repoPath: string): Promise<string> {
// 直读 .git/HEAD 文件("ref: refs/heads/main\n")
const head = await fs.promises.readFile(`${repoPath}/.git/HEAD`, 'utf-8')
if (head.startsWith('ref: ')) {
const refPath = head.slice(5).trim()
return await fs.promises.readFile(`${repoPath}/.git/${refPath}`, 'utf-8')
}
return head.trim() // detached HEAD
}
export async function readBranch(repoPath: string): Promise<string> {
const head = await fs.promises.readFile(`${repoPath}/.git/HEAD`, 'utf-8')
if (head.startsWith('ref: refs/heads/')) {
return head.slice('ref: refs/heads/'.length).trim()
}
return 'HEAD' // detached
}
// 配合 LRU 缓存:
const branchCache = new LRUCache({ max: 100, ttl: 5000 }) // 5s TTL集成点:状态栏 getBranch() / commit attribution / <git-context> 注入。
预期收益:每个状态栏更新从 ~30ms → ~1ms。在 1Hz 状态栏更新场景下 30× 加速。
审计提示:PR#3636 OPEN 是
cap concurrent in-flight requests per provider,与本项不同 —— PR#3636 做"并发上限"(rate limiting),本项做"相同请求合并"(dedupe)。两者可共存。
问题:并行 subagent 场景下,多个 agent 可能同时请求同一 model + 相同 prompt(如多个并行的 explore agent 都要列项目结构)。
审计提示(v5):此优化适用范围有限。AI 模型调用通常带唯一的 conversation context(history + tools 各异),完全相同的请求较少。真正高价值的去重是工具调用层(多个 subagent 并行读同一文件 → 合并为 1 次 read)而非模型调用层。如果实际度量发现 hit rate 低,应转向工具层 dedup。
解决方案(对标 Claude coalescing + BoundedUUIDSet):
// core/inFlightCoalesce.ts (新建)
const inFlight = new Map<string, Promise<Response>>()
export async function coalesce<T>(
key: string, // 通常是 hash(model + prompt + tools)
fetch: () => Promise<T>
): Promise<T> {
const existing = inFlight.get(key)
if (existing) return existing as Promise<T>
const promise = fetch().finally(() => inFlight.delete(key))
inFlight.set(key, promise)
return promise
}集成点:openaiContentGenerator/pipeline.ts / anthropicContentGenerator.ts 的入口。
预期收益:并行 subagent + 重复请求场景节省一次 API call 成本(~$0.01-0.10/次)。
| 项 | 文件 | 收益 |
|---|---|---|
| React.memo 高频组件(p2-perf item-25) | HistoryItemDisplay / AppHeader / ToolGroupMessage |
高频 re-render 减少 30-50% |
| WeakRef/WeakMap 防止 GC 保留(p2-perf item-18) | Map<sessionId, ...> 类强引用 | 长 session 内存增长解决 |
| 正则编译缓存(p2-perf item-23) | Hook 系统 + LSP hot path | hot path 微秒级加速 |
| Diff 渲染 useMemo + Regex 预编译(p2-perf item-34) | DiffRenderer.tsx 62 行 regex |
diff 切换不卡顿 |
| 项 | 收益 |
|---|---|
| Shell AST 解析缓存(p2-perf item-32) | 同命令重复执行省 1-2ms |
| Shell 环境快照 session 级缓存(p2-perf item-29) | process.env 收集启动期一次性 |
| Memoization cold start 去重(p2-perf item-22) | 启动期同一 expensive 计算只跑一次 |
| 项 | 适用场景 |
|---|---|
| Bun 原生 API(item-26) | Bun runtime 用户 |
| 编译时 feature gating + DCE(item-28) | bundle 大小 |
| 输出缓冲与防阻塞渲染(item-6) | 大量流式输出 |
| 终端行宽缓存 + Blit screen diff(item-27) | 终端 resize |
| 图片压缩多策略流水线(item-17) | paste 大图 |
每个 perf PR 应带 baseline vs after 数字。tracer 模板:
# /tmp/qwen-trace/trace-sync-io.cjs(PR body 含完整脚本)
NODE_OPTIONS='--require /tmp/qwen-trace/trace-sync-io.cjs' \
QWEN_TRACE_WARMUP_MS=4000 \
qwen -p "<test prompt>"
cat /tmp/qwen-trace/summary.${PID}.txt
# 看 unique_sites + total_calls# /tmp/qwen-trace/trace-startup.cjs(建议新建)
const startTime = process.hrtime.bigint()
const phases = []
require('module').prototype.require = new Proxy(...) // 拦截每个 require
// 记录每个模块加载时间
process.on('exit', () => {
console.log(`Total: ${Number(process.hrtime.bigint() - startTime) / 1e6}ms`)
console.log('Top 10 slow modules:', phases.sort((a,b) => b.dur - a.dur).slice(0, 10))
})QWEN_DEBUG_PROMPT_SIZE=1 qwen -p "..."
# 输出每轮 system prompt 大小(chars + estimated tokens)NODE_OPTIONS='--inspect --max-old-space-size=512' qwen --resume <long-session>
# 用 chrome devtools 看 heap snapshot// trace-readfile.cjs
const fs = require('fs')
const orig = fs.promises.readFile
fs.promises.readFile = function(path, ...args) {
console.error(`[readFile] ${path}`)
return orig.call(this, path, ...args)
}## Summary
<一句话说明改动 + 度量结果>
## Reproducing the measurement
1. Tracer 脚本(贴完整源码或路径)
2. 测试 prompt(让 reviewer 能复现)
3. Before/After 数字对比
### Before (commit <baseline-sha>)
<tracer 输出>
### After (this PR)
<tracer 输出>
## Test plan
- [x] vitest run packages/core
- [x] vitest run packages/cli
- [x] Manual: <场景描述>不要把多个独立优化塞进一个 PR。PR#3581 拆 3 commit(async write / LRU cache / regression test)就是好范式:每个 commit 自洽 + 可独立 review。
- 缓存类改动 必须有失效路径(如
invalidateAfterWrite) - 并行类改动 必须保留串行 fallback(如
--no-parallelflag) - 异步化改动 必须 graceful shutdown(
Config.shutdown()await flush)
| 不推荐 | 原因 |
|---|---|
| 编写自己的 ink fork | 已有 @jrichman/ink@6.6.7(Claude Code 用) |
| 单独优化 OSC 8 / 11 | 内部专精团队(chiga0/Edenman)已覆盖 |
大改 query.ts 主循环 |
内部决策权 + 影响面太大,不会被 review |
| 自实现 LSP 协议 | 已有 vscode-languageclient |
- 改进报告主矩阵 —— 全部 275 项 + PR 追踪
- P0/P1 引擎优化(28 项)
- P2 性能优化(35 项)
- 启动优化 Deep-Dive
- Prompt Cache 优化 Deep-Dive
- Bun 原生 API 优化 Deep-Dive
- 文件读取缓存 Deep-Dive
- 同步 I/O 异步化 Deep-Dive
最后更新:2026-04-28 状态:active roadmap,每两周更新一次 反馈:在 codeagents 仓库提 issue / 在 qwen-code 仓库提 PR