长对话内存持续增长,无自动释放机制
状态:Open
优先级:高
类型:性能
创建日期:2026-05-22
更新日期:2026-06-14
问题描述
Agent 对话过程中,内存(RSS)随对话轮数线性增长,每轮约增长 40 MB,且不会自动下降。持续 50-100 轮对话后可达数 GB,最终导致 OOM。debug 和 release 模式下均表现相同:/clear 后 RSS 不会下降。
已尝试的缓解措施(均未解决):
- jemalloc 调优(
dirty_decay_ms=200, lg_tcache_max=16)→ 效果有限
- 切换为 mimalloc(
PAGE_RESET/DECOMMIT/BACKGROUND_THREAD)→ 现象未改善
/clear 时调用 alloc_collect()(mi_collect(true) 或 jemalloc_decay())→ RSS 不降
- AgentPool LLM 实例复用 → 减少瞬态分配但 RSS 增长模式不变
当前结论:RSS 增长中大部分不是 Rust 堆上的活跃对象(allocated 不增长),而是分配器碎片化 + 运行时基础设施持有(tokio 线程栈、reqwest HTTP 连接池、TLS session 缓冲)。详见下方根因分析。
症状详情
| 维度 |
观察 |
| 增长模式 |
对话轮数相关,非时间相关 |
| 增长速度 |
~40 MB/轮 |
| 是否自动下降 |
否,只增不减 |
| 触发场景 |
各类操作均有(SubAgent/大文件读取/纯文本) |
| 手动缓解 |
/clear (new_thread) 无法释放(debug/release 均如此) |
| 分配器历史 |
jemalloc → 系统默认 → mimalloc (当前),均表现相同 |
复现条件
- 复现频率:必现
- 触发步骤:
- 启动 TUI,正常对话
- 每发一轮消息,观察 RSS 增长
- 持续对话数轮后,RSS 持续上升
/clear 后 RSS 不下降
- 环境:macOS,Rust 2021,任何模型下均出现
- 诊断工具:无(
/heapdump 已在 mimalloc 迁移中删除,mimalloc 无等价工具)
现象 2(2026-05-23):debug/release 均无法通过 /clear 释放
debug 和 release 模式下 /clear 后 RSS 均不下降。排除"debug 模式分配器不归还内存"的推测。初步怀疑数据结构泄漏 → 后被推翻(allocated 不增长)。
现象 3(2026-05-30):mimalloc 迁移后问题持续
已从 jemalloc 切换至 mimalloc(MIMALLOC_PAGE_RESET=1, MIMALLOC_DECOMMIT=1, MIMALLOC_BACKGROUND_THREAD=1),并在 /clear、/compact、切换会话后调用 alloc_collect() → mi_collect(true)。
结果:RSS 线性增长模式未发生变化,/clear 后 RSS 仍然不降。详见 spec/issues/2026-05-30-retry-mimalloc-with-mi-options.md(已 Fixed)。
现象 4(2026-05-30):问题持续确认
wrap_map 增量优化(Plan 1)实施后,内存增长模式未发生变化。确认与近期渲染优化无关。
现象 5(2026-06-14):单次 /clear 后稳态 RSS 148 MB 不释放
用户报告:执行 /clear 后,TUI Status Bar 显示 MEM 148MB,RSS 没有下降。
与现象 1-4 的差异:原现象强调「每轮 ~40 MB 线性增长」,本次报告的是「单次 clear 后停在 148 MB 的稳态」——是同一问题的不同切片(前者关注增长速率,后者关注 clear 后的 floor)。
代码层面已做的清理(peri-tui/src/app/thread_ops.rs::new_thread,行 250-358):
view_messages clear + shrink_to_fit()
ephemeral_notes clear
origin_messages clear + shrink_to_fit()
pipeline clear + shrink_to_fit()
todo_items / pending_attachments / pasted_text_blocks clear
reset_agent_session():token_tracker / retry_status / cancel_token 等重置
- 通过 ACP 协议
client.new_session() 通知 server 端开新 session
- 调
crate::alloc_config::alloc_collect() 两次(jemalloc epoch advance + 每个 arena purge)
- 发送
RenderEvent::Clear 通知渲染线程
与原 issue 描述的偏差:原 issue 说"已切换至 mimalloc",但当前 peri-tui/src/main.rs:30-31 实际是 #[global_allocator] static GLOBAL: tikv_jemallocator::Jemalloc——当前代码库已切回 jemalloc。peri-tui/src/alloc_config.rs 全部基于 tikv_jemalloc_ctl / tikv_jemalloc_sys,没有 mimalloc 残留。MALLOC_CONF 设置为 dirty_decay_ms:0,muzzy_decay_ms:0,background_thread:true(最激进归还策略)。
怀疑主因(待排查,未验证):
- 进程级常驻内存(syntect / reqwest / tokio / MCP / LSP / 插件加载内容 / ratatui 缓冲区)
- jemalloc
retained 内存(即使 arena.{n}.purge 也不会 100% 归还 OS,mmap munmap 成本太高)
/clear 是 in-place 清理当前 session,session_mgr 是否仍持有旧 session 引用未释放
待执行诊断:在 TUI 内 /gc 查看 jemalloc breakdown(allocated / active / resident / retained)精确判断是分配器碎片还是真泄漏。
根因分析
核心发现(jemalloc 时代的 heapdump 数据沉淀)
经过多轮 heapdump 对比(详见下方历史附录),关键发现:
allocated 不增长(9.5 → 9.0 MB):Rust 堆活跃对象并未随对话轮数线性增长。ACP executor / State.messages 不是泄漏源
/clear 后 TUI 数据归零:agent_state_messages=0, pipeline_completed=0, view_messages=0 — TUI 前端完全释放
- free/malloc 比 97.3%:每轮 68 万次分配中绝大部分已释放,不是传统意义的内存泄漏
- 增长来自两部分:
- 分配器碎片化:已 free 但未归还 OS 的页面(jemalloc dirty pages / mimalloc free segments)
- 运行时基础设施:tokio 线程栈(8MB×N threads)、reqwest HTTP 连接池、TLS session 缓存
为什么 mimalloc 也没解决
mimalloc 的 DECOMMIT 和 BACKGROUND_THREAD 在 macOS 上的实际效果待验证。mi_collect(true) 是同步回收但可能需要多次调用才能触发 OS 归还。手动 /clear 路径已调用 alloc_collect(),但 RSS 未降——说明:
- 要么 mimalloc 在 macOS 上也受限于同样的 OS 层面限制(macOS 的
madvise(MADV_FREE) 不立即回收物理页)
- 要么非分配器开销(tokio/reqwest)占比太大,分配器层面的优化无法触及
当前 RSS 构成(估算,基于历史数据)
RSS 增长/轮 (~40 MB)
├── 分配器碎片化 (~17 MB) ← mimalloc DECOMMIT 理论上可缓解,实际待验证
├── 非分配器运行时 (~20 MB) ← tokio 线程栈 + reqwest 连接池 + TLS 缓冲
│ ├── reqwest HTTP 连接池 ← 默认无限制,TLS session 不释放
│ ├── tokio runtime 线程栈 ← 8MB/线程 × worker threads
│ └── hyper 响应体缓冲区 ← streaming response 的 Bytes 积累
└── 分配器元数据 (~3 MB) ← 不可控
修复方向
P0:降低非分配器运行时开销(分配器已无法进一步优化)
- 限制 reqwest 连接池:检查
ClientBuilder 的 pool_max_idle_per_host 和 pool_idle_timeout。默认无限制的连接池持续持有 TLS session。建议 pool_max_idle_per_host(2) + pool_idle_timeout(30s)
- 减小 tokio 线程栈:默认 8MB/线程,N 个 worker threads 就有 N×8MB 纯栈开销。检查是否可用
thread_stack_size 减半
- 审计 hyper 响应体缓冲区:LLM streaming response 的
Bytes 是否在 response 完成后及时释放
P1:减少每轮分配 churn(治本)
- 消除 serde JSON 双重解析:
run_pump 中 serde_json::from_value(event_value.clone()) 先 clone 再反序列化,改为零拷贝解析
- 减少 String clone:68 万次 malloc 中大量是字符串克隆(event 序列化/反序列化路径),审计
AcpNotification::AgentEvent 构造路径中的 clone
- LLM response body buffer 复用:考虑用
Bytes pool 或复用已有 buffer
P2:已验证/已否决的方案
- ✅ jemalloc 调优(
dirty_decay_ms=200, lg_tcache_max=16)— 已实施,效果有限
- ✅ mimalloc 替代(
PAGE_RESET/DECOMMIT/BACKGROUND_THREAD)— 已实施,未改善
- ✅ 系统分配器对照实验 — 已测试,同样表现,排除分配器独有因素
- ✅ AgentPool LLM 复用 — 已实施,减少瞬态分配但 RSS 增长模式不变
- ❌ macOS
background_thread — jemalloc 的 background_thread 和 mimalloc 的 BACKGROUND_THREAD 在 macOS 上实际效果待验证(macOS 线程模型限制)
- ❌
/heapdump — 已随 jemalloc 一起删除,mimalloc 无等价内置工具
P3:备选方案
- 考虑定期重启策略:对于长时间运行的 TUI 会话,在 N 轮对话后提示用户重启或自动重置 runtime
- 外部内存 profiling:使用 macOS Instruments (Allocations/Leaks) 或
sample 命令获取非分配器内存分布
2026-06-01 深度排查:分配风暴源定位
通过 3 个并行 agent 对 peri-agent/peri-acp/peri-tui/peri-middlewares 全量扫描,定位到碎片化的根本原因是大块临时分配/释放循环。
根因链条
RSS ~200MB = 活跃对象(~9MB) + 分配器碎片(~70MB) + 非分配器运行时(~120MB)
↑ ↑
← 风暴源 A/B → ← 进程级资源 →
风暴源 A(最严重):before_agent 每轮 ReAct 迭代全量 clone
位置:peri-middlewares/src/subagent/mod.rs:456-457
if let Some(ref pm) = self.parent_messages {
*pm.write() = state.messages().to_vec(); // 全量深拷贝
}
- 触发频率:每个 ReAct 迭代(不是 SubAgent 调用),一次对话可能 10-50 次迭代
- 影响:500 条消息时每次 clone ~1-2MB × 50 次迭代 = 50-100MB 临时分配/释放
- 根因:这些大型 Vec 跨越多个内存页,释放后页面因相邻存活对象(MCP Pool、ToolSearchIndex 等)无法归还 OS
风暴源 B:state.history.clone() 每轮 prompt 全量克隆
位置:peri-tui/src/acp_server/prompt.rs:86
state.history.clone() // 可用 std::mem::take 替代
- 影响:1-2MB/轮,但可用
std::mem::take 完全消除
风暴源 C:prompt_locks HashMap 泄漏
位置:peri-tui/src/acp_server/mod.rs:87
session/prompt 时 or_insert 创建 lock,但 session/close 不清理
- 每次
/clear = 1 废弃 + 1 新 lock 条目(单条目 ~50 bytes,非主因但需修复)
风暴源 D:Arc::try_unwrap 失败导致 AgentPool 残留
位置:peri-tui/src/acp_server/mod.rs:165
build_agent 将 pool Arc clone 到中间件链 → 引用计数 > 1 → try_unwrap 失败
- 失败时 AgentPool 中
reqwest::Client(~1-2MB)+ LLM 缓存不会被恢复或 invalidate
消息历史三重存储(/clear 可清理,但正常使用时占 3x 内存)
| # |
位置 |
生命周期 |
| 1 |
AcpSession.state_messages(ACP 层) |
session 级 |
| 2 |
AgentComm.origin_messages(TUI 层) |
session 级 |
| 3 |
MessagePipeline.completed(TUI pipeline) |
session 级 |
修复方向(按影响排序)
| 优先级 |
修复 |
预估效果 |
复杂度 |
| P0 |
before_agent 延迟 clone:仅在 SubAgent 工具实际调用时才 clone |
减少 50-100MB/轮 |
中 |
| P0 |
prompt.rs:86 用 std::mem::take 替代 .clone() |
减少 1-2MB/轮 |
低 |
| P1 |
session/close 清理 prompt_locks |
消除 HashMap 泄漏 |
低 |
| P1 |
Arc::try_unwrap 失败时 pool.lock().invalidate() |
释放残留 LLM 实例 |
低 |
涉及文件(当前代码库)
| 文件 |
角色 |
peri-tui/src/main.rs:266-268 |
#[global_allocator] mimalloc + tokio runtime 配置 |
peri-tui/src/mimalloc_config.rs |
init_mimalloc_conf() + alloc_collect() |
peri-tui/src/app/thread_ops.rs |
/clear 路径(new_thread) |
peri-tui/src/acp_server/mod.rs:87 |
prompt_locks HashMap(泄漏源 C) |
peri-tui/src/acp_server/prompt.rs:86 |
history.clone()(风暴源 B) |
peri-tui/src/acp_server/mod.rs:165 |
Arc::try_unwrap 失败(风暴源 D) |
peri-middlewares/src/subagent/mod.rs:456-457 |
before_agent 全量 clone(风暴源 A) |
peri-acp/src/session/executor.rs |
execute_prompt 内 event channel + spawn 闭包 |
peri-acp/src/session/agent_pool.rs:47 |
subagent_llm_cache |
关联 Issue
spec/issues/2026-05-30-retry-mimalloc-with-mi-options.md — mimalloc 迁移方案(Fixed,已实施但未改善)
spec/issues/2026-05-30-render-event-unbounded-channel.md — RenderThread 事件通道(Fixed)
spec/issues/2026-05-30-cpu-spike-on-session-restore.md — 会话恢复 CPU 暴涨(Partial)
历史附录:jemalloc 时代的诊断数据
以下数据来自 2026-05-23 的 /heapdump 定量分析,基于 jemalloc 分配器。当前已切换至 mimalloc,这些数据不可复现,仅作为历史参考保留。
jemalloc 现象 A:debug 模式 heapdump
| 指标 |
对话前 |
对话后 |
增长 |
| RSS |
54.4 MB |
93.1 MB |
+38.7 MB |
| jemalloc allocated |
11.1 MB |
23.4 MB |
+12.3 MB |
| jemalloc active |
17.5 MB |
37.2 MB |
+19.7 MB |
| jemalloc resident |
24.8 MB |
51.8 MB |
+27.0 MB |
| RSS - resident(非 jemalloc) |
29.6 MB |
41.4 MB |
+11.8 MB |
small malloc 次数:+786,935(80 万次小对象分配/轮)
jemalloc 现象 B:release 模式 heapdump
| 指标 |
空会话 |
5 tool calls 后 |
增长 |
| RSS |
52.9 MB |
94.8 MB |
+41.9 MB |
| jemalloc allocated |
9.5 MB |
9.0 MB |
-0.5 MB ← 不增长! |
| jemalloc resident |
23.3 MB |
68.0 MB |
+44.7 MB |
| jemalloc mapped |
67.3 MB |
204.5 MB |
+137.2 MB |
| total mallocs |
— |
700,782 |
— |
| total frees |
— |
681,795 |
free/malloc 比 97.3% |
jemalloc 现象 C:/clear 后 RSS 构成
RSS: 81.8 MB
├── jemalloc allocated: 9.3 MB ← 实际在用极少
├── arena 碎片化空闲: 17.5 MB ← active - allocated
├── jemalloc metadata: ~7.6 MB
├── tcache: 4.4 MB
└── 非 jemalloc: 43.4 MB ← tokio/hyper/reqwest/rustls
关键结论:allocated 不增长说明不是传统泄漏,RSS 增长来自分配器碎片化 + 运行时基础设施持有。这一结论在切换至 mimalloc 后仍然成立。
长对话内存持续增长,无自动释放机制
状态:Open
优先级:高
类型:性能
创建日期:2026-05-22
更新日期:2026-06-14
问题描述
Agent 对话过程中,内存(RSS)随对话轮数线性增长,每轮约增长 40 MB,且不会自动下降。持续 50-100 轮对话后可达数 GB,最终导致 OOM。debug 和 release 模式下均表现相同:
/clear后 RSS 不会下降。已尝试的缓解措施(均未解决):
dirty_decay_ms=200,lg_tcache_max=16)→ 效果有限PAGE_RESET/DECOMMIT/BACKGROUND_THREAD)→ 现象未改善/clear时调用alloc_collect()(mi_collect(true)或jemalloc_decay())→ RSS 不降当前结论:RSS 增长中大部分不是 Rust 堆上的活跃对象(
allocated不增长),而是分配器碎片化 + 运行时基础设施持有(tokio 线程栈、reqwest HTTP 连接池、TLS session 缓冲)。详见下方根因分析。症状详情
/clear(new_thread) 无法释放(debug/release 均如此)复现条件
/clear后 RSS 不下降/heapdump已在 mimalloc 迁移中删除,mimalloc 无等价工具)现象 2(2026-05-23):debug/release 均无法通过
/clear释放debug 和 release 模式下
/clear后 RSS 均不下降。排除"debug 模式分配器不归还内存"的推测。初步怀疑数据结构泄漏 → 后被推翻(allocated不增长)。现象 3(2026-05-30):mimalloc 迁移后问题持续
已从 jemalloc 切换至 mimalloc(
MIMALLOC_PAGE_RESET=1,MIMALLOC_DECOMMIT=1,MIMALLOC_BACKGROUND_THREAD=1),并在/clear、/compact、切换会话后调用alloc_collect()→mi_collect(true)。结果:RSS 线性增长模式未发生变化,
/clear后 RSS 仍然不降。详见spec/issues/2026-05-30-retry-mimalloc-with-mi-options.md(已 Fixed)。现象 4(2026-05-30):问题持续确认
wrap_map 增量优化(Plan 1)实施后,内存增长模式未发生变化。确认与近期渲染优化无关。
现象 5(2026-06-14):单次 /clear 后稳态 RSS 148 MB 不释放
用户报告:执行
/clear后,TUI Status Bar 显示MEM 148MB,RSS 没有下降。与现象 1-4 的差异:原现象强调「每轮 ~40 MB 线性增长」,本次报告的是「单次 clear 后停在 148 MB 的稳态」——是同一问题的不同切片(前者关注增长速率,后者关注 clear 后的 floor)。
代码层面已做的清理(
peri-tui/src/app/thread_ops.rs::new_thread,行 250-358):view_messagesclear +shrink_to_fit()ephemeral_notesclearorigin_messagesclear +shrink_to_fit()pipelineclear +shrink_to_fit()todo_items/pending_attachments/pasted_text_blocksclearreset_agent_session():token_tracker / retry_status / cancel_token 等重置client.new_session()通知 server 端开新 sessioncrate::alloc_config::alloc_collect()两次(jemalloc epoch advance + 每个 arena purge)RenderEvent::Clear通知渲染线程与原 issue 描述的偏差:原 issue 说"已切换至 mimalloc",但当前
peri-tui/src/main.rs:30-31实际是#[global_allocator] static GLOBAL: tikv_jemallocator::Jemalloc——当前代码库已切回 jemalloc。peri-tui/src/alloc_config.rs全部基于tikv_jemalloc_ctl/tikv_jemalloc_sys,没有 mimalloc 残留。MALLOC_CONF设置为dirty_decay_ms:0,muzzy_decay_ms:0,background_thread:true(最激进归还策略)。怀疑主因(待排查,未验证):
retained内存(即使arena.{n}.purge也不会 100% 归还 OS,mmap munmap 成本太高)/clear是 in-place 清理当前 session,session_mgr 是否仍持有旧 session 引用未释放待执行诊断:在 TUI 内
/gc查看 jemalloc breakdown(allocated/active/resident/retained)精确判断是分配器碎片还是真泄漏。根因分析
核心发现(jemalloc 时代的 heapdump 数据沉淀)
经过多轮 heapdump 对比(详见下方历史附录),关键发现:
allocated不增长(9.5 → 9.0 MB):Rust 堆活跃对象并未随对话轮数线性增长。ACP executor / State.messages 不是泄漏源/clear后 TUI 数据归零:agent_state_messages=0, pipeline_completed=0, view_messages=0 — TUI 前端完全释放为什么 mimalloc 也没解决
mimalloc 的
DECOMMIT和BACKGROUND_THREAD在 macOS 上的实际效果待验证。mi_collect(true)是同步回收但可能需要多次调用才能触发 OS 归还。手动/clear路径已调用alloc_collect(),但 RSS 未降——说明:madvise(MADV_FREE)不立即回收物理页)当前 RSS 构成(估算,基于历史数据)
修复方向
P0:降低非分配器运行时开销(分配器已无法进一步优化)
ClientBuilder的pool_max_idle_per_host和pool_idle_timeout。默认无限制的连接池持续持有 TLS session。建议pool_max_idle_per_host(2)+pool_idle_timeout(30s)thread_stack_size减半Bytes是否在 response 完成后及时释放P1:减少每轮分配 churn(治本)
run_pump中serde_json::from_value(event_value.clone())先 clone 再反序列化,改为零拷贝解析AcpNotification::AgentEvent构造路径中的 cloneBytespool 或复用已有 bufferP2:已验证/已否决的方案
dirty_decay_ms=200,lg_tcache_max=16)— 已实施,效果有限PAGE_RESET/DECOMMIT/BACKGROUND_THREAD)— 已实施,未改善background_thread— jemalloc 的background_thread和 mimalloc 的BACKGROUND_THREAD在 macOS 上实际效果待验证(macOS 线程模型限制)— 已随 jemalloc 一起删除,mimalloc 无等价内置工具/heapdumpP3:备选方案
sample命令获取非分配器内存分布2026-06-01 深度排查:分配风暴源定位
通过 3 个并行 agent 对
peri-agent/peri-acp/peri-tui/peri-middlewares全量扫描,定位到碎片化的根本原因是大块临时分配/释放循环。根因链条
风暴源 A(最严重):
before_agent每轮 ReAct 迭代全量 clone位置:
peri-middlewares/src/subagent/mod.rs:456-457风暴源 B:
state.history.clone()每轮 prompt 全量克隆位置:
peri-tui/src/acp_server/prompt.rs:86std::mem::take完全消除风暴源 C:
prompt_locksHashMap 泄漏位置:
peri-tui/src/acp_server/mod.rs:87session/prompt时or_insert创建 lock,但session/close不清理/clear= 1 废弃 + 1 新 lock 条目(单条目 ~50 bytes,非主因但需修复)风暴源 D:
Arc::try_unwrap失败导致 AgentPool 残留位置:
peri-tui/src/acp_server/mod.rs:165build_agent将 pool Arc clone 到中间件链 → 引用计数 > 1 →try_unwrap失败reqwest::Client(~1-2MB)+ LLM 缓存不会被恢复或 invalidate消息历史三重存储(/clear 可清理,但正常使用时占 3x 内存)
AcpSession.state_messages(ACP 层)AgentComm.origin_messages(TUI 层)MessagePipeline.completed(TUI pipeline)修复方向(按影响排序)
before_agent延迟 clone:仅在 SubAgent 工具实际调用时才 cloneprompt.rs:86用std::mem::take替代.clone()session/close清理 prompt_locksArc::try_unwrap失败时pool.lock().invalidate()涉及文件(当前代码库)
peri-tui/src/main.rs:266-268#[global_allocator]mimalloc + tokio runtime 配置peri-tui/src/mimalloc_config.rsinit_mimalloc_conf()+alloc_collect()peri-tui/src/app/thread_ops.rs/clear路径(new_thread)peri-tui/src/acp_server/mod.rs:87peri-tui/src/acp_server/prompt.rs:86peri-tui/src/acp_server/mod.rs:165peri-middlewares/src/subagent/mod.rs:456-457peri-acp/src/session/executor.rsperi-acp/src/session/agent_pool.rs:47关联 Issue
spec/issues/2026-05-30-retry-mimalloc-with-mi-options.md— mimalloc 迁移方案(Fixed,已实施但未改善)spec/issues/2026-05-30-render-event-unbounded-channel.md— RenderThread 事件通道(Fixed)spec/issues/2026-05-30-cpu-spike-on-session-restore.md— 会话恢复 CPU 暴涨(Partial)历史附录:jemalloc 时代的诊断数据
以下数据来自 2026-05-23 的
/heapdump定量分析,基于 jemalloc 分配器。当前已切换至 mimalloc,这些数据不可复现,仅作为历史参考保留。jemalloc 现象 A:debug 模式 heapdump
small malloc 次数:+786,935(80 万次小对象分配/轮)
jemalloc 现象 B:release 模式 heapdump
jemalloc 现象 C:
/clear后 RSS 构成关键结论:
allocated不增长说明不是传统泄漏,RSS 增长来自分配器碎片化 + 运行时基础设施持有。这一结论在切换至 mimalloc 后仍然成立。