vault backup: 2026-06-09 23:15:17

This commit is contained in:
2026-06-09 23:15:17 +08:00
parent 31fd89aafe
commit e92c0327d9
111 changed files with 7276 additions and 8846 deletions
+103 -108
View File
@@ -1,39 +1,44 @@
---
tags: [claude-code, 内存泄漏, 审计, 修复, 测试]
create time: 2026-06-09 22:30
---
# 内存泄漏排查报告
> 基于官方 CHANGELOG 记录的 11 个已修复内存泄漏 + 1 个代码注释中的已知问题,对反编译代码库进行逐文件验证。
> 审计日期:2026-04-28
## 概述
## TODO
基于官方 CHANGELOG 记录的 11 个已修复内存泄漏 + 1 个代码注释中的已知问题,对反编译代码库进行逐文件验证。审计日期:2026-04-28。所有 12 项均已确认修复或有明确的修复方案。
- [x] #1 图片处理无限内存增长 — 确认已实现 ✅
- [x] #2 /usage 命令泄漏约 2GB — 确认已实现 ✅
- [x] #3 长时间运行工具进度事件泄漏 — 确认已实现 ✅
## 正文
### TODO
- [x] #1 图片处理无限内存增长 — 确认已实现
- [x] #2 /usage 命令泄漏约 2GB — 确认已实现
- [x] #3 长时间运行工具进度事件泄漏 — 确认已实现
- [x] #4 空闲重新渲染循环 — **已确认完整**:所有 10 个 useAnimationFrame 调用者均正确传递 null 暂停时钟,keepAlive 机制工作正常
- [x] #5 虚拟滚动器保留历史消息拷贝 — 确认已实现 ✅
- [x] #6 管道模式超宽行过度分配 — 确认已实现 ✅
- [x] #5 虚拟滚动器保留历史消息拷贝 — 确认已实现
- [x] #6 管道模式超宽行过度分配 — 确认已实现
- [x] #7 语言语法按需加载 — **已修复**:改用 highlight.js/lib/core + 静态注册 26 个常用语言,从 190+ 语言降至 ~25,内存减少 ~80%
- [x] #8 NO_FLICKER 模式流状态泄漏 — **已修复**:StreamingToolExecutor.discard() 现在完整释放 tools 数组、中止 siblingAbortController、清理 turnSpan,7 tests
- [x] #9 Remote Control 权限条目保留 — **已修复**:pendingPermissionHandlers 提升至 useEffect 作用域,cleanup 时显式 clear(),8 tests
- [x] #10 MCP HTTP/SSE 缓冲区累积 — 确认已实现 ✅
- [x] #10 MCP HTTP/SSE 缓冲区累积 — 确认已实现
- [x] #11 LRU 缓存键保留大 JSON — **已确认完整实现**:FileStateCache 使用 LRU 双重限制(max 100 条目 + maxSize 25MB)+ sizeCalculation,22 tests
- [x] #12 QueryEngine.mutableMessages 不收缩 — **已修复**:实现 snipCompactIfNeeded(按 removedUuids 过滤)+ snipProjection(边界检测 + 视图投影),28 tests
- [x] #18 Permission Polling Interval 泄漏 — **已修复**:inProcessRunner 权限响应后未调用 cleanup(),导致 setInterval 永远运行 + abort listener 挂载,6 tests
- [x] #17 LSP Opened Files Map 不收缩 — **已修复**:LSPServerManager 添加 closeAllFiles() 方法,postCompactCleanup 集成调用,compaction 后释放 openedFiles Map,5 tests
## 总览
---
## 1. 图片处理无限内存增长 (v2.1.121)
### 1. 图片处理无限内存增长 (v2.1.121)
**CHANGELOG 描述**:Fixed unbounded memory growth (multi-GB RSS) when processing many images in a session
### 实现位置
**实现位置**
- `src/utils/imageStore.ts` — 核心修复
- `src/commands/clear/caches.ts` — 缓存清理
- `src/screens/REPL.tsx` — UI 层释放
### 修复方式
**修复方式**
三层防护机制:
@@ -41,7 +46,7 @@
2. **磁盘持久化**:图片 base64 数据写入 `~/.claude/image-cache/<sessionId>/`,内存中仅保留路径字符串
3. **立即释放**:`setPastedContents({})` 在消息提交/命令执行后清空 React state 中的 base64 数据
### 关键代码
**关键代码**
```typescript
// imageStore.ts:10
@@ -63,26 +68,23 @@ function evictOldestIfAtCap(): void {
export async function cleanupOldImageCaches(): Promise<void> { ... }
```
---
## 2. /usage 命令泄漏约 2GB (v2.1.121)
### 2. /usage 命令泄漏约 2GB (v2.1.121)
**CHANGELOG 描述**:Fixed /usage leaking up to ~2GB of memory on machines with large transcript histories
### 实现位置
**实现位置**
- `src/utils/sessionStoragePortable.ts:716-792` — 核心流式读取
- `src/utils/attribution.ts` — 调用方
### 修复方式
**修复方式**
1. **分块流式读取**:使用 `TRANSCRIPT_READ_CHUNK_SIZE = 1MB` 固定块大小,通过 `fd.read()` 逐块处理,避免一次性加载整个 transcript
2. **字节级过滤**:在 fd 层面直接跳过 `attribution-snapshot` 类型的行(占长会话 84% 的字节空间)
3. **边界截断**:搜索 `compact_boundary` 标记,只保留边界之后的数据
4. **缓冲区控制**:初始缓冲区限制 `Math.min(fileSize, 8MB)`
### 关键代码
**关键代码**
```typescript
// sessionStoragePortable.ts:716-792
@@ -119,25 +121,22 @@ export async function readTranscriptForLoad(
}
```
---
## 3. 长时间运行工具进度事件泄漏 (v2.1.121)
### 3. 长时间运行工具进度事件泄漏 (v2.1.121)
**CHANGELOG 描述**:Fixed memory leak when long-running tools fail to emit a clear progress event
### 实现位置
**实现位置**
- `src/screens/REPL.tsx:3054-3114` — progress 消息替换逻辑
- `src/utils/sessionStorage.ts:186-196` — 临时消息类型定义
### 修复方式
**修复方式**
1. **向后扫描替换**:从只检查最后一条消息改为向后遍历所有 progress 消息,找到匹配的 `parentToolUseID` + `type` 后替换(修复交错消息导致 13k+ 条目堆积)
2. **全屏模式硬上限**:`MAX_FULLSCREEN_SCROLLBACK = 500`,超出截断
3. **临时消息识别**:`isEphemeralToolProgress()` 区分 `bash_progress`、`sleep_progress` 等一次性消息与需要保留的 `agent_progress` 等
### 关键代码
**关键代码**
```typescript
// REPL.tsx:3094-3114
@@ -169,19 +168,17 @@ const kept = postBoundary.length > MAX_FULLSCREEN_SCROLLBACK
return [...kept, newMessage]
```
---
### 4. 空闲重新渲染循环 (v2.1.117)
## 4. 空闲重新渲染循环 (v2.1.117)
**状态:已确认完整**
> [!info] 状态:已确认完整
**CHANGELOG 描述**:Fixed idle re-render loop when background tasks are present, reducing memory growth on Linux
### 实现位置
**实现位置**
- `packages/@ant/ink/src/components/ClockContext.tsx` — 核心时钟管理
### 已实现部分
**已实现部分**
`ClockContext` 的 `keepAlive` 订阅者分类机制完整存在:
@@ -217,22 +214,19 @@ function createClock(tickIntervalMs: number): Clock {
}
```
### 不确定部分
**不确定部分**
无法确认 `useAnimationFrame` hook 是否在所有使用时钟的组件中正确传递了 `keepAlive` 参数。反编译代码中调用链可能不完整。
---
## 5. 虚拟滚动器保留历史消息拷贝 (v2.1.101)
### 5. 虚拟滚动器保留历史消息拷贝 (v2.1.101)
**CHANGELOG 描述**:Fixed a memory leak where long sessions retained dozens of historical copies of the message list in the virtual scroller
### 实现位置
**实现位置**
- `src/components/VirtualMessageList.tsx:276-296`
### 修复方式
**修复方式**
增量式键值数组:使用 `useRef` 保存 keys 数组引用,流式追加而非每次 O(n) 全量重建。
@@ -261,18 +255,15 @@ const keys = keysRef.current
修复前 27k 消息时每次新消息添加产生 ~1MB 内存分配,修复后降为 O(1) 追加。
---
## 6. 管道模式超宽行过度分配 (v2.1.110)
### 6. 管道模式超宽行过度分配 (v2.1.110)
**CHANGELOG 描述**:Fixed potential excessive memory allocation when piped (non-TTY) Ink output contains a single very wide line
### 实现位置
**实现位置**
- `packages/@ant/ink/src/core/output.ts:200-207`
### 修复方式
**修复方式**
在 `Output.reset()` 中当字符缓存超过 16384 条目时清空:
@@ -288,19 +279,17 @@ reset(width: number, height: number, screen: Screen): void {
}
```
---
### 7. 语言语法按需加载 (v2.1.108)
## 7. 语言语法按需加载 (v2.1.108)
**状态:已修复**
> [!info] 状态:已修复
**CHANGELOG 描述**:Reduced memory footprint for file reads, edits, and syntax highlighting by loading language grammars on demand
### 实现位置
**实现位置**
- `packages/color-diff-napi/src/index.ts:21-37`
### 当前状态
**当前状态**
延迟加载逻辑**已被移除**,改为顶层静态导入。代码注释说明原因:
@@ -322,22 +311,21 @@ function hljsApi(): HLJSApi {
}
```
**影响**:highlight.js 包含 190+ 语言语法(约 50MB),现在在模块加载时即全部载入内存,无法按需释放。这是为了兼容 Bun `--compile` 模式做的妥协。
> [!warning] 内存影响
> highlight.js 包含 190+ 语言语法(约 50MB),现在在模块加载时即全部载入内存,无法按需释放。这是为了兼容 Bun `--compile` 模式做的妥协。
---
### 8. NO_FLICKER 模式流状态泄漏 (v2.1.105)
## 8. NO_FLICKER 模式流状态泄漏 (v2.1.105)
**状态:已修复**
> [!info] 状态:已修复
**CHANGELOG 描述**:Fixed a NO_FLICKER mode memory leak where API retries left stale streaming state
### 实现位置
**实现位置**
- `src/screens/REPL.tsx:1841-1861` — `resetLoadingState()`
- `src/screens/REPL.tsx:3568-3578` — finally 块调用
### 已实现部分
**已实现部分**
`resetLoadingState()` 在 `onQuery` 的 finally 块中无条件调用,清理 `streamingText`、`streamingToolUses` 等:
@@ -358,24 +346,22 @@ const resetLoadingState = useCallback(() => {
}
```
### 不确定部分
**不确定部分**
无法确认 `query.ts` 中 `StreamingToolExecutor.discard()` 的逻辑是否完整实现了旧工具结果的释放。
---
### 9. Remote Control 权限条目保留 (v2.1.98)
## 9. Remote Control 权限条目保留 (v2.1.98)
**状态:已修复**
> [!info] 状态:已修复
**CHANGELOG 描述**:Fixed a memory leak where Remote Control permission handler entries were retained for the lifetime of the session
### 实现位置
**实现位置**
- `src/hooks/useReplBridge.tsx:466-491` — 处理 + 删除
- `src/hooks/useReplBridge.tsx:712-717` — 注册 + 清理函数
### 已实现部分
**已实现部分**
```typescript
// useReplBridge.tsx:466-491
@@ -401,24 +387,21 @@ onResponse(requestId, handler) {
}
```
### 不确定部分
**不确定部分**
hook 的 cleanup 函数(组件卸载时的 `replBridgePermissionCallbacks = undefined`)是否完整调用。
---
## 10. MCP HTTP/SSE 缓冲区累积 (v2.1.97)
### 10. MCP HTTP/SSE 缓冲区累积 (v2.1.97)
**CHANGELOG 描述**:Fixed MCP HTTP/SSE connections accumulating ~50 MB/hr of unreleased buffers when servers reconnect
### 实现位置
**实现位置**
- `src/services/api/claude.ts:1557-1564` — `releaseStreamResources()`
- `src/cli/transports/SSETransport.ts:419` — `reader.releaseLock()`
- `@modelcontextprotocol/sdk` (sse.js, streamableHttp.js) — `response.body?.cancel()`
### 修复方式
**修复方式**
1. **主动释放响应体**:`releaseStreamResources()` 清理 stream 和 response
@@ -449,21 +432,18 @@ function releaseStreamResources(): void {
3. **MCP SDK 层面**:在所有 HTTP 路径(成功/失败/重连)调用 `response.body?.cancel()`
---
## 11. LRU 缓存键保留大 JSON (v2.1.89)
**状态:已确认完整实现**
### 11. LRU 缓存键保留大 JSON (v2.1.89)
> [!info] 状态:已确认完整实现
**CHANGELOG 描述**:Fixed memory leak where large JSON inputs were retained as LRU cache keys in long-running sessions
### 实现位置
**实现位置**
- `src/utils/fileStateCache.ts:37-48` — 大小计算修复
- `src/utils/queryHelpers.ts:48-54` — 类型强制转换
### 修复方式
**修复方式**
1. **正确计算缓存大小**:处理 `content` 为嵌套对象的情况
@@ -495,20 +475,18 @@ function coerceToolContentToString(value: unknown): string {
}
```
---
### 12. QueryEngine.mutableMessages 不收缩
## 12. QueryEngine.mutableMessages 不收缩
**状态:已修复**
> [!info] 状态:已修复
**代码注释描述**:`markers persist and re-trigger on every turn, and mutableMessages never shrinks (memory leak in long SDK sessions)`(`src/QueryEngine.ts:929-930`)
### 实现位置
**实现位置**
- `src/services/compact/snipCompact.ts` — **存根文件**
- `src/QueryEngine.ts:925-962` — 消息处理逻辑
### 问题详情
**问题详情**
`mutableMessages` 数组只增不减,每轮对话 push 多条消息(assistant、progress、user、attachment 等)。清理依赖两条路径:
@@ -560,26 +538,21 @@ if (snipResult !== undefined) {
}
```
### 风险评估
> [!warning] 风险评估
> 在长时间 SDK 会话中,如果 API 不频繁返回 `compact_boundary`,`mutableMessages` 会持续增长。每条消息可能包含大量内容(工具输出、文件内容等),长时间运行可能导致 GB 级内存占用。这是当前代码库中**最明确的未实现内存泄漏点**。
- 在长时间 SDK 会话中,如果 API 不频繁返回 `compact_boundary`,`mutableMessages` 会持续增长
- 每条消息可能包含大量内容(工具输出、文件内容等),长时间运行可能导致 GB 级内存占用
- 这是当前代码库中**最明确的未实现内存泄漏点**
### 17. LSP Opened Files Map 不收缩
---
## 17. LSP Opened Files Map 不收缩
**状态:已修复**
> [!info] 状态:已修复
**代码注释描述**:`closeFile()` 存在但未与 compact 流程集成(`LSPServerManager.ts:373-375` 显式标注为 TODO)
### 实现位置
**实现位置**
- `src/services/lsp/LSPServerManager.ts:414-428` — `closeAllFiles()` 方法
- `src/services/compact/postCompactCleanup.ts:81-88` — 集成调用
### 问题详情
**问题详情**
`LSPServerManager` 中的 `openedFiles: Map<string, string>` 追踪所有通过 `didOpen` 打开的文件。`closeFile()` 方法存在可以发送 `didClose` 通知并清理 Map 条目,但代码注释明确标注:
@@ -590,7 +563,7 @@ TODO: Integrate with compact - call closeFile() when compact removes files from
长时间会话中,每次读取/编辑文件都会通过 `openFile()` 添加条目,但 compaction 不会清理这些条目,导致 Map 无限增长。
### 修复方式
**修复方式**
1. **添加 `closeAllFiles()` 方法**:遍历 `openedFiles` Map,对每个文件发送 `didClose` 通知,然后清空 Map。Best-effort 错误处理。
@@ -626,15 +599,32 @@ try {
}
```
---
### 总结
## 总结
```mermaid
graph LR
subgraph confirmed["确认已实现 (12)"]
A1["#1 图片"]
A2["#2 /usage"]
A3["#3 进度消息"]
A4["#4 空闲渲染"]
A5["#5 虚拟滚动器"]
A6["#6 管道输出"]
A7["#10 MCP缓冲区"]
end
subgraph fixed["已修复 (7)"]
B1["#7 语法加载"]
B2["#8 NO_FLICKER"]
B3["#9 RC权限"]
B4["#11 LRU缓存键"]
B5["#12 snipCompact"]
B6["#17 LSP文件追踪"]
B7["#18 Permission Polling"]
end
```
确认已实现 (12): #1 图片 #2 /usage #3 进度消息 #4 空闲渲染 #5 虚拟滚动器 #6 管道输出 #10 MCP缓冲区
已修复 (7): #7 语法加载 #8 NO_FLICKER #9 RC权限 #11 LRU缓存键 #12 snipCompact #17 LSP文件追踪 #18 Permission Polling
### 测试覆盖
**测试覆盖**
| 修复项 | 测试文件 | 测试数 |
|--------|----------|--------|
@@ -647,9 +637,8 @@ try {
| #18 Permission Polling | `src/hooks/__tests__/swarmPermissionPoller.test.ts` | 6 |
| #17 LSP Opened Files | `src/services/lsp/__tests__/closeAllFiles.test.ts` | 5 |
| **总计** | **8 个测试文件** | **83** |
```
### 需要关注的优先级
**需要关注的优先级**
1. ~~**P0 — `snipCompact.ts` 存根**~~ **已修复**
2. ~~**P1 — 语法按需加载回退**~~ **已修复**
@@ -657,3 +646,9 @@ try {
4. ~~**P2 — 空闲渲染循环**~~ **已确认完整**
5. ~~**P2 — Permission Polling Interval**~~ **已修复**
6. ~~**P2 — LSP Opened Files Map**~~ **已修复**:closeAllFiles() 集成到 postCompactCleanup
## 关联笔记
- [[memory-peak-analysis]] — 内存占用 1GB 调研(Vite 单文件构建根因)
- [[performance-reporter]] — 性能峰值分析(30 项瓶颈清单)
- [[lsp-integration]] — LSP 集成(#17 LSP Opened Files 相关)