BitFun 运行时性能审阅报告(02)
- 审阅日期:2026-07-26
- 审阅维度:执行性能(运行时)—— Rust 后端热点路径 / 前端 React 高频路径 / Tauri IPC 层
- 方法:Grep 系统性扫描典型反模式(clone 密集区、async 中同步 IO、
Regex::new非静态、block_on、JSON.parse(JSON.stringify)、setInterval、structuredClone、未节流事件监听、高频invoke/emit),对每个可疑点精读上下文并论证调用频率后才收录;所有明显但经确认无害的候选项在文末"误报排除记录"中留档,避免后续重复审查。 - 所有行号以当前 main 分支(48a003b73)为准。
一、发现总览表
按预期收益排序。"层"= R(Rust 后端)/ F(前端)/ I(IPC)。
| # | 层 | 问题 | 位置(相对 src/) |
预期收益 | 修复风险 |
|---|---|---|---|---|---|
| 1 | R | 每条消息追加触发整个 turn 上下文全量 clone + pretty 序列化 + 落盘,总成本 O(N²) | crates/assembly/core/src/agentic/session/session_manager.rs:6310-6336、agentic/persistence/manager.rs:1287-1298,646-654、crates/services/services-core/src/json_store.rs:211-213 |
高 | 中 |
| 2 | R | 文件树递归扫描:sort 比较器内同步 stat(O(n log n) 次)、每条目 3+ 次 metadata、同步 canonicalize、不读 gitignore |
crates/services/services-core/src/filesystem/tree.rs:436,446-469,486,504,538,897(复制版 572-769 同病) |
高 | 中 |
| 3 | R/I | 所有后端→前端事件经 PeerAwareEmitter 无条件深拷贝 payload;terminal 事件另有无条件 to_value |
apps/desktop/src/api/remote_connect_api.rs:558-561、apps/desktop/src/api/terminal_api.rs:347-359 |
高 | 低 |
| 4 | R | 终端 transcript:每 chunk 同步 open/write/flush/close,std::sync::Mutex 持锁做阻塞 IO,跑在 tokio PTY 事件循环 |
crates/services/terminal/src/transcript.rs:412-437,176-187、terminal/src/session/manager.rs:346 |
高 | 中 |
| 5 | R | LSP 诊断/响应同一份 Value 单次通知深拷贝 5 次(含 MB 级 semanticTokens 响应) |
crates/services/services-core/src/lsp/process.rs:164-170,445、crates/assembly/core/src/service/lsp/workspace_manager.rs:886-906 |
高 | 中 |
| 6 | R | 流式事件队列:每个模型 delta ~15 次堆分配 + 2 次 envelope 深拷贝 + 2 把异步锁(含 stats 计数锁) | crates/execution/agent-stream/src/lib.rs:806-819、crates/execution/agent-runtime/src/event_queue.rs:178-207 |
高 | 中 |
| 7 | R | ACP 工具流:raw_input(可含数百 KB Edit/Write 参数)单次 ToolCallUpdate 深拷贝 3-4 次 |
crates/interfaces/acp/src/client/stream.rs:193,204-221,240,294-301 |
高 | 低 |
| 8 | R | LS 工具:同步 BFS 无 spawn_blocking(阻塞 tokio worker),每条目 2 次 symlink_metadata |
crates/services/services-core/src/filesystem/listing.rs:41,137-160、crates/assembly/core/src/agentic/tools/implementations/ls_tool.rs:292 |
高 | 低 |
| 9 | F | 侧栏拖拽 mousemove 每次全量 setState,无 rAF,触发整个工作区子树重渲染 | web-ui/src/app/layout/WorkspaceBody.tsx:64-77,89 |
高(拖拽期间) | 低 |
| 10 | F/I | 桌宠 120ms 轮询 cursorPosition() IPC + 每 tick querySelectorAll/getBoundingClientRect,全仓频率最高常驻 IPC |
web-ui/src/app/components/AgentCompanionDesktopPet/AgentCompanionDesktopPet.tsx:454,471-482,490 |
中高 | 低 |
| 11 | F/I | 编辑器每 1s 轮询 get_file_metadata,与已有 file-system-changed 推送通道功能重叠 |
web-ui/src/tools/editor/components/CodeEditor.tsx:120,1990、MarkdownEditor.tsx:43,428 |
中高 | 中 |
| 12 | R | 文件监视线程:每个 notify 事件 block_on 异步 RwLock;每次 watch/unwatch 全量重建 watcher |
crates/services/services-integrations/src/file_watch/service.rs:117-197(172,185) |
中 | 低 |
| 13 | R/I | 目录分页命令每页全量重扫目录再内存切片,O(页数×目录大小) | apps/desktop/src/api/commands.rs:2408-2441 |
中 | 中 |
| 14 | F | EventBus 每次 emit 复制 1000 元素历史数组,且历史持有 payload 强引用阻止 GC | web-ui/src/infrastructure/event-bus/EventBus.ts:144,295-302 |
中 | 低 |
| 15 | F | SessionStateMachine 每次状态转换 structuredClone 整个 context 广播 |
web-ui/src/flow_chat/state-machine/SessionStateMachine.ts:61-72,320 |
中 | 中 |
| 16 | R | 内容搜索每行无条件分配 String(未命中行也分配);结果收尾整体 clone | crates/services/services-core/src/filesystem/tree.rs:1485-1491,1140,1275 |
中 | 低 |
| 17 | R | grep 工具:每文件冗余 is_file() stat;输出 split→逐行 alloc→join 往返 |
crates/execution/tool-execution/src/search/grep_search.rs:743,776,835-840,875 |
中 | 低 |
| 18 | F | 桌宠打字机:interval 依赖自身 setState 目标,每 28ms 销毁重建 effect + 全部气泡强制布局 | web-ui/src/app/components/AgentCompanionDesktopPet/AgentCompanionDesktopPet.tsx:308-339 |
中 | 低 |
| 19 | F | Tooltip 捕获阶段 scroll 监听未节流:2×getBoundingClientRect + 3×setState/滚动帧 | web-ui/src/component-library/components/Tooltip/Tooltip.tsx:171-205,294-296 |
中 | 低 |
| 20 | F | MouseGlow 每个 pointermove 在 rAF 之前执行 composedPath() + filter 建 2 个数组 |
web-ui/src/infrastructure/mouse-glow/core/MouseGlowService.ts:126-134 |
中 | 低 |
| 21 | F/I | SnapshotAPI 逐 turn 串行 get_turn_files,长会话 N 次串行 IPC 往返 |
web-ui/src/infrastructure/api/service-api/SnapshotAPI.ts:514-531 |
中 | 低 |
| 22 | R | PTY chunk from_utf8_lossy(..).to_string() 强制整串拷贝(最大 64KB);tap 分发逐个 clone |
crates/services/terminal/src/session/manager.rs:285,429 |
中低 | 低 |
| 23 | R | HeadTailText 逐 char 推入 VecDeque<char>,10MB 构建日志 = 千万次操作 |
crates/services/terminal/src/exec.rs:934-959 |
中低 | 低 |
| 24 | F | Peer 模式会话快照 3s 轮询 reconcile | web-ui/src/flow_chat/services/flow-chat-manager/PeerSessionRefreshModule.ts:26,229 |
中低 | 中 |
| 25 | R | front-matter 正则每次解析 .md 时重新编译(4+ 处重复实现) |
crates/services/services-core/src/markdown.rs:15 及同构处 |
低 | 极低 |
| 26 | F | Browser 检查器 2 个 Tauri 监听无卸载清理,场景关闭后常驻 | web-ui/src/app/scenes/browser/BrowserPanel.tsx:78,114,137,147 |
低 | 低 |
| 27 | F | App.tsx 两处 listen effect 缺 disposed 旗标(StrictMode 下泄漏) |
web-ui/src/app/App.tsx:650-683,685-712 |
低 | 极低 |
| 28 | F | ToolExecutionService 4 个事件监听句柄丢弃,destroy 后重建即重复注册 | web-ui/src/shared/services/tool-execution-service.ts:84-93,103-120 |
低 | 低 |
| 29 | F | 会话列表第 2 档展开无上限、未虚拟化 | web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.tsx:591-595,1017 |
低 | 低 |
| 30 | R | 持久化全局 FILE_LOCKS/per-path 锁 map 永不淘汰,按路径无界增长 |
crates/services/services-core/src/persistence.rs:9-18、json_store.rs:307-314 |
低 | 低 |
二、逐条详情
高收益
1. 会话消息追加 = O(N²) 全量重写 turn 快照【R,高】
问题:每次 add_message 都把当前 turn 的全部上下文消息深拷贝一份、serde_json::to_string_pretty 全量序列化、再原子写盘。第 N 条消息触发第 N 次全量序列化,单 turn 总成本 O(N²·消息体积)。
证据链(4 跳):
session_manager.rs:6310-6336:add_message尾部无条件调用persist_current_turn_context_snapshot_best_effort(session_id, "context_message_added");session_manager.rs:1429-1433:取出整个get_context_messages(session_id)传给save_turn_context_snapshot;persistence/manager.rs:1287-1298+646-654:sanitize_messages_for_persistence对每条消息执行message.clone()(全量深拷贝)后write_json_atomic;json_store.rs:211-213:serde_json::to_string_pretty(比 compact 大 30-50%)+ 临时文件 +ReplaceFileW(带 write-through 强制刷盘)。
调用频率:agentic 主循环逐条调用 —— execution_engine.rs:3338(每轮 assistant 消息)、:3351(每个工具结果)、:3446/3502/3564/3628/3675/3844/3858(各类注入)。50 轮 × 3 工具的会话 ≈ 200 次;10MB 上下文时单次追加 ≈ 10MB clone + 14MB JSON 字符串 + 14MB 强制刷盘磁盘写。这是 agent 会话卡顿随会话变长而恶化的首要嫌疑。
优化方案:
sanitize_messages_for_persistence改Cow:仅含Multimodal{images}/ToolResult需脱敏的消息才 clone,其余借用序列化;to_string_pretty→serde_json::to_writer(compact)+BufWriter;- 结构性修复:快照改 JSONL 追加式(每条消息一行,turn 结束/压缩时才全量重写),或加 dirty-window 合批(200ms 内多次 add_message 只落一次盘)。崩溃恢复语义保持:JSONL 天然可恢复到最后一条完整行。
2. 文件树递归扫描的系统调用风暴【R,高】
问题:build_tree_recursive(tree.rs:446-)存在五个叠加低效,build_tree_recursive_with_stats(tree.rs:572-769)是同一段代码的复制,问题相同:
tree.rs:461-469:sort 比较器内做同步 stat ——a.path().is_dir()每次比较 2 次PathBuf分配 + 2 次阻塞系统调用,1000 项目录 ≈ 20,000 次同步 stat,全部在 async 上下文;tree.rs:486 / 504 / 538→897:每条目file_type().await+metadata().await+get_permissions_string内再fs::metadata—— 3+ 次 stat/条目;Windows 分支只需readonly(),:504的 metadata 已含该信息;tree.rs:436:path.canonicalize()同步阻塞 IO,每目录一次;tree.rs:446,452:Vec::new()无预分配;每条目 3 次to_string_lossy().to_string()(:480-484,542,543);tree.rs:771-793+107-124:默认不读.gitignore、include_hidden: true、max_depth 50→node_modules/、target/全量递归。
调用频率:Tauri 命令 get_file_tree/explorer_get_file_tree(commands.rs:2360)→ 工作区打开与文件树刷新必经。15 万文件的中型前端仓 = 数十万次异步 stat + 上万次 sort 内同步 stat + ~45 万次字符串分配,单次可达数十秒。
优化方案:
- 读目录时一次性落地
Vec<(DirEntry, FileType, Metadata)>再排序,比较器零系统调用; - 用
ignore::WalkBuilder(tree.rs:8已引入该 crate,search 路径已在用)替代手写递归,免费获得 gitignore + 并行遍历 + 每条目单次 stat; canonicalize移入spawn_blocking或改用文件 ID 去环;Windows 权限直接复用已取 metadata;- 两份复制代码合并为一个带 stats 回调的实现。
3. 全局事件出口无条件深拷贝(PeerAwareEmitter)【R/I,高】
问题:remote_connect_api.rs:558-561:
async fn emit(&self, event_name: &str, payload: serde_json::Value) -> anyhow::Result<()> {
self.inner.emit(event_name, payload.clone()).await?; // 无条件深拷贝
maybe_fanout_peer_ui_event(event_name, payload); // 守卫(事件名单 + 是否有 peer)在函数内部才判断
Ok(())
}
PeerAwareEmitter 包裹的是全局 emitter(lib.rs:2008-2015)和 LSP workspace emitter(lsp_workspace_api.rs:189-192),即所有 backend→frontend 事件(终端 chunk、文件变更、LSP 诊断、工具进度)每条都吃一次 serde_json::Value 深拷贝(逐节点堆分配,非 memcpy)—— 而绝大多数用户从不开 Peer Mode。同构问题:terminal_api.rs:347-359 对每个终端事件(64KB 峰值,~200 次/秒/终端)无条件 serde_json::to_value 后才进守卫。
优化方案:把 should_fanout_peer_ui_event(event_name) && !attached_controllers().is_empty() 判断提到 clone/to_value 之前;不满足则 move payload 直接 emit,零拷贝。改动约 10 行,无语义变化,收益覆盖所有高频事件通道。
4. 终端 transcript 同步 IO 跑在 async 事件循环【R,高】
问题:transcript.rs:412-437 append_inner 每个 chunk 执行 OpenOptions::open + write_all + flush(open/close 每次重来);入口 with_store(transcript.rs:176-187)持 std::sync::Mutex 执行上述阻塞 IO;调用点 session/manager.rs:346 位于 tokio::spawn 的 PTY 事件循环内,无 spawn_blocking。
调用频率:上游 DataBufferer 合批后仍高达 ~200 chunk/秒/终端(5ms flush 间隔),即每秒 200 次 CreateFile+WriteFile+FlushFileBuffers+CloseHandle;Windows 上叠加 Defender 实时扫描,单次 100µs-1ms。多终端并发时同一把 std Mutex 让 tokio worker 互相 park(非 yield)。
优化方案:TranscriptStore.writers: HashMap<String, TranscriptWriter> 已存在,补上常开的 BufWriter<File> 句柄;去掉每 chunk 的 open/close 与 flush()(改定时/会话结束 flush);整个写路径移到专用写线程 + mpsc 或 spawn_blocking。
5. LSP 诊断/响应的五连深拷贝【R,高】
问题:一次 publishDiagnostics 通知,同一份 Vec<Value> 被拷贝 5 次:
process.rs:445diagnostics_arr.clone()(notif在:371已拥有,可Value::take移出);workspace_manager.rs:889写缓存 clone(且持lsp_manager读锁跨 await,:886-891,与start_server的写锁竞争);workspace_manager.rs:896构造事件再 clone;workspace_manager.rs:906serde_json::to_value(&event)整树重建;remote_connect_api.rs:559PeerAwareEmitter 再深拷贝(见问题 3)。
另 process.rs:164-170 因 match &message 借用而 response.clone() —— 每条 LSP 响应(semanticTokens/full、documentSymbol 可达数 MB,随滚动/光标持续产生)整体深拷贝一次。
频率:工作区打开时 rust-analyzer/tsserver 为数百文件连发诊断;编辑期每次防抖后触发。200 条诊断 ≈ 100KB Value 树 × 5 = 每条通知 500KB 逐节点分配/释放。
优化方案:process.rs:164 改 match message 按值解构消除响应 clone;:445 用 Value::take;workspace_manager 缓存与事件间用 Arc<Vec<Value>> 共享;diagnostics_cache 提为独立 Arc<RwLock<..>> 摆脱外层 lsp_manager 锁。
6. 流式事件队列:每 delta 的分配与锁风暴【R,高】
问题:agent-stream/lib.rs:806-819 handle_text_chunk 每个 SSE delta 构造 AgenticEvent::TextChunk,session_id/turn_id/round_id/attempt_id 各一次 String clone;下游 event_queue.rs:178-207:queue.push(envelope.clone())(深拷贝#1)→ channel.sender.send(envelope.clone())(#2)→ broadcast_tx.send(envelope)(按订阅者数再各克隆)→ self.stats.lock().await(第 2 把异步锁,只为计数 +1)。
频率:每模型 token 一次。2000 token 回复 ≈ 30,000 次堆分配 + 4,000 次 tokio Mutex 获取,全部搬运恒定不变的 id 元数据;ToolEventData::Completed{result} 走同路时 envelope clone 是完整工具结果(数百 KB)的深拷贝。
优化方案:① id 字段改 Arc<str>(turn 内共享);② broadcast::Sender<Arc<EventEnvelope>> 消除 fan-out 深拷贝;③ stats 改 AtomicU64;④ 评估 legacy queue 对 TextChunk 级事件是否可跳过(与 broadcast 语义重复)。
7. ACP raw_input 单次 update 深拷贝 3-4 次【R,高】
问题:acp/client/stream.rs:294-301 每条 ToolCallUpdate 走 update.fields.raw_input.clone()(#1)→ update_from_fields(:204-221)内 self.calls.get(tool_id).cloned()(#2,整个 snapshot)→ previous...raw_input.clone()(#3)→ insert(..., snapshot.clone())(#4)。ToolCall 路径(:193,231-240)同构。raw_input 对 Edit/Write 工具含完整 old_string/new_string/content,可达数百 KB;ACP agent 工具执行期持续推送 status/content update(manager.rs:1247-1254 主循环逐条调用)。
优化方案:AcpToolCallSnapshot.raw_input 改 Option<Arc<serde_json::Value>>;仅在新值为 None 时 Arc::clone 旧值;insert 后返回引用而非先 clone 再 insert。
8. LS 工具同步 BFS 阻塞 tokio worker【R,高】
问题:listing.rs:137-160 用 std::fs 做同步 BFS:入队时 symlink_metadata(:151-155,已获 is_dir/mtime),出队时(:137)对同一路径再 stat 一遍;:144 每保留一项 entry.clone();:41 结果 Vec 未按 limit 预分配。调用点 ls_tool.rs:292 在 async call_impl 中直接调用,无 spawn_blocking —— 而同目录 grep_tool.rs:794、glob_tool.rs:475 都正确包了 spawn_blocking,证明是遗漏而非设计。agent 每次 LS 调用都 park 一个 tokio worker,期间该 worker 上的流式输出/终端事件全部停摆。
优化方案:① 包 spawn_blocking(与 grep/glob 对齐);② is_symlink 入队时存进 entry,删掉出队二次 stat;③ Vec::with_capacity(limit);④ push 改 move。
9. 侧栏拖拽 mousemove 全树重渲染【F,高(拖拽期间)】
问题:WorkspaceBody.tsx:64-77 拖拽期间每个 mousemove 直接 setNavWidth(newWidth),无 rAF/节流;navWidth 是 WorkspaceBody 自身 state,其 JSX 渲染 <NavBar/><NavPanel/><SceneBar/><SceneViewport/>(:106-132),且 NavPanel.tsx:44、SceneViewport.tsx:84 均无 React.memo → 每个 mousemove 重渲染整棵工作区子树(含 Monaco 宿主、会话面板)。同仓库同类拖拽全部做了 rAF + 直写 DOM(SessionScene.tsx:214-226/262-274、hooks/useResizer.ts:56-69、SplitHandle.tsx),此处是唯一例外,实锤遗漏。
优化方案:照抄 SessionScene 模式 —— rAF 合并 + 拖拽期间 navAreaRef.current.style.setProperty('--nav-width', ...) 直写(该 CSS 变量已存在,:104),mouseup 时才 setNavWidth 落状态。
中高 / 中收益
10. 桌宠 120ms cursorPosition() 常驻 IPC【F/I,中高】
AgentCompanionDesktopPet.tsx:490 每 120ms(≈8.3 次/秒)跨进程调 cursorPosition() + outerPosition()(:454),回调内再 querySelectorAll + 逐气泡 getBoundingClientRect(:471-477)+ 2 次 setState(:479-482)。桌宠窗口一开就持续运行,无 visibilityState/focus 守卫。这是全仓频率最高的常驻 IPC。
方案:无气泡或窗口非活动时停表;getBoundingClientRect 结果缓存到 resize/位移事件;长期可改后端鼠标 hook 推送。
11. 编辑器 1s get_file_metadata 轮询冗余【F/I,中高】
CodeEditor.tsx:1990(FILE_SYNC_POLL_INTERVAL_MS = 1000,:120;peer 模式 15s)+ MarkdownEditor.tsx:428 同构:激活 tab 每秒 1 次 get_file_metadata invoke,差异时追加 get_file_editor_sync_hash/读内容。有 isActiveTab 守卫(:1976)不随 tab 数放大,但仓库已有后端推送通道 file-system-changed(tools/file-system/services/FileSystemService.ts:120、TauriExplorerFileSystemProvider.ts:268),1s 轮询与之功能重叠。
方案:订阅 file-system-changed 按 filePath 过滤,事件命中或窗口重获焦点时才校验 metadata;轮询降级为 30s 兜底。注意远程工作区(无本地 watcher)需保留轮询分支。
12. 文件监视线程 per-event block_on + watcher 全量重建【R,中】
file_watch/service.rs:163-194:spawn_blocking 循环内对每个 notify 事件 rt.block_on(Self::convert_events(...))(:172)—— 仅为读一次异步 RwLock 的路径表;debounce 到期再 block_on(flush)(:185)。大量文件变更(git checkout、npm install)时每事件一次跨运行时往返。另:每次 watch_path/unwatch_path 都 create_watcher 重建整个 watcher 并重注册所有根(:99,112),Windows 上递归 watch 注册成本高。
方案:线程启动时快照 HashMap<PathBuf, FileWatcherConfig>(或改 std::sync::RwLock),过滤逻辑同步执行,block_on 仅保留在 debounce flush;watcher 支持增量 watch/unwatch 而非整体重建。
13. 目录分页命令每页全量重扫【R/I,中】
commands.rs:2408-2441 get_directory_children_paginated:每页调用 get_directory_contents_with_remote_hint(全量读目录 + 每条目 metadata),再 skip(offset).take(limit) 内存切片 —— 翻到第 K 页要重扫 K 次全目录;service.rs:110-119 与 tree.rs 层均无缓存。10 万条目的目录逐页浏览成本 O(K·n)。附带:commands.rs:2315-2347 手工把 FileTreeNode 转 serde_json::Value 后 Tauri 再序列化一遍,大树双倍分配 —— 直接返回 Vec<FileTreeNode>(已 derive Serialize)即可。
方案:短 TTL(数秒)按路径缓存目录快照供分页复用,或游标式增量读取;删除手工 Value 转换层。
14. EventBus 事件历史:每 emit O(1000) 拷贝 + payload 强引用【F,中】
EventBus.ts:295-302:recordEvent 在每次 emit() 无条件执行(:144),超过 MAX_HISTORY(1000)后每次 emit 都 slice(-1000) 复制整个数组;metadata.data 持 payload 强引用 —— 最近 1000 个事件的完整 payload(含 AppManager.ts:370/378 的整个 app state 快照)无法 GC。全局单例,117 处 emit 调用点。
方案:改环形缓冲(写指针覆盖,零拷贝);生产环境只记事件名 + 时间戳,payload 仅 dev 模式保留。
15. SessionStateMachine 每次转换 structuredClone【F,中】
SessionStateMachine.ts:61-72:getSnapshot 对 context 做 structuredClone + new Set + slice(-100);:320 每次 transition() 都调用并广播,useSessionStateMachine.ts:28-32 直接灌进 React state。agent 执行期转换极频繁(工具开始/进度/完成/权限)。
方案:context 改不可变更新(transition 产生新对象),getSnapshot 返回引用,删除 clone。风险:需审查订阅方是否有原地修改 snapshot 的代码。
16-17. 搜索路径的行级分配与冗余 stat【R,中】
tree.rs:1485-1491:内容搜索对每一行先String::from_utf8_lossy(..).to_string()再is_match—— 未命中行(绝大多数)的分配全部浪费;10 万行仓库 = 10 万次无用分配。改为在Cow/&str上先 match、命中才to_string,并用read_until复用行缓冲。tree.rs:1140/1275收尾lock_search_results(&results).clone()(至多 1 万条)改mem::take。grep_search.rs:776:walker 循环内path.is_file()同步 stat,而:783-785已在用entry.file_type()—— 直接删掉换file_type判断;:835-840,875:sink 输出整串 → 逐行to_string→join("\n")完整往返,让GrepSink直接产出Vec<String>或全程单串切片;:743content_lines预分配。
18-21. 前端高频路径其余确认项【F,中】
- 桌宠打字机(
AgentCompanionDesktopPet.tsx:308-339):effect 依赖数组含typedOutputBySessionId而 interval 回调本身在改它 → 每 28ms 卸载重建 interval;:335的useLayoutEffect同 deps,每 28ms 对所有输出元素写scrollTop(强制布局)。目标文本改useRef,deps 改[hasTypingOutput]。 - Tooltip(
Tooltip.tsx:294-296):visible期间捕获阶段监听全应用 scroll,calculatePosition(:171-205)每滚动帧 2 次getBoundingClientRect(强制同步布局)+ 3 次 setState。rAF 合并 +passive: true+ 三 state 合一。 - MouseGlow(
MouseGlowService.ts:126-134):composedPath()+filter建数组发生在 rAF 之前,120Hz 鼠标下每秒 240 次数组分配。把求值推迟到 rAF 内(缓存event.target,closest()定位即可)。 - SnapshotAPI(
SnapshotAPI.ts:514-531):get_session_turns后 for-await 串行逐 turnget_turn_files,百轮会话 = 百次串行 IPC。后端加批量命令get_turns_files(session_id, turn_indices[]);短期先改Promise.all。
22-24. 中低收益
session/manager.rs:285:String::from_utf8_lossy(&data).to_string()——data: Vec<u8>已拥有,合法 UTF-8 时Cow::Borrowed被强制再分配(最大 64KB/chunk)。改String::from_utf8(data).unwrap_or_else(|e| ...)零拷贝复用;:429tap 分发data_str.clone()改Arc<str>。exec.rs:934-959:HeadTailText::push_str逐char推VecDeque<char>(4B/char),大输出千万次 push/pop;head 无with_capacity。tail 改字节环形缓冲 + UTF-8 边界对齐。PeerSessionRefreshModule.ts:229:仅 Peer 模式生效的 3s 快照 reconcile,可加"最近无事件才轮询"守卫或退避。
低收益(顺手修)
- #25:
markdown.rs:15front-matter 正则每次Regex::new;调用方为文件监视触发的命令/agent 扫描(command_source.rs:572等 6 处,其中opencode-adapter/command_source.rs:769,774失败重试同内容编译两次)。改static LazyLock<Regex>,并将skills/types.rs:105、custom_agent.rs:549等同构实现统一。 - #26:
BrowserPanel.tsxinspectorUnlistenRef.current仅在手动stopInspector(:78)调用,无卸载useEffect清理 → 检查器开启时关闭 Browser 场景,2 个 Tauri 监听 +addContext闭包常驻,后续事件仍向已卸载 store 注入。加useEffect(() => () => stopInspector(), [])。 - #27:
App.tsx:650-683,685-712两个 listen effect 缺disposed旗标(同文件:188,565是正确范本);StrictMode 下稳定泄漏,agent-companion://open-session被处理两次。 - #28:
tool-execution-service.ts:103-120四个api.listen返回值丢弃,destroy()(:84-93)不解除;destroy→getInstance 重建即重复注册。保存 unlisten 数组并在 destroy 调用。 - #29:
SessionsSection.tsx:591-595第 2 档展开返回全量(无上限、无虚拟化);默认档只渲 5/10 条无问题。第 2 档接 Virtuoso 或 200 条上限 + 加载更多。 - #30:
persistence.rs:9-18/json_store.rs:307-314per-path 锁 map 永不淘汰,长期运行按路径数无界增长。改Weak或定期清理无持有者条目。
IPC 层整体评估
- 已做对的:终端输出后端合批(
data_bufferer.rs:89-139,5ms/64KB,零拷贝移交);文件搜索进度BatchedFileSearchProgressSink;前端EventBatcher(rAF + 32ms text-chunk 上限)对流式事件做了渲染层合并;tauri-adapter.ts:120-155的 listen 包装正确处理了 async-unlisten 竞态;ConfigAPI已批量取配置。 - 主要缺口:① 事件出口的无条件深拷贝(#3,修复最划算);②
agentic://text-chunk与 ACP 路径(acp_client_api.rs:357-400)后端逐 delta emit,每次 emit 是一次独立 IPC 序列化 + 广播到所有 webview —— 前端已批,后端可选做 16-33ms 合批进一步降低 IPC 次数(中低,前端已缓解);③ 可批量化的串行小调用:get_turn_files(#21)、编辑器轮询(#11)、桌宠cursorPosition(#10)。
三、实施建议清单(可直接派发)
按投入产出比排序;每项独立可交付。风险 = 引入回归的可能性。
| 任务 | 内容 | 涉及文件 | 风险 |
|---|---|---|---|
| T1 | PeerAwareEmitter/terminal 事件出口:把 should_fanout_peer_ui_event && !attached_controllers().is_empty() 提到 payload.clone()/to_value 之前,不满足则 move 零拷贝 |
apps/desktop/src/api/remote_connect_api.rs:558-561、terminal_api.rs:347-359 |
低 |
| T2 | LS 工具:list_directory_entries 包 spawn_blocking(对齐 grep/glob);entry 增加 is_symlink 字段消除出队二次 stat;Vec::with_capacity(limit);push 改 move |
crates/services/services-core/src/filesystem/listing.rs、assembly/core/src/agentic/tools/implementations/ls_tool.rs:292 |
低 |
| T3 | LSP 消息零拷贝:process.rs:164 match &message→match message 消除响应 clone;:445 Value::take;workspace_manager.rs:889-906 缓存/事件共享 Arc<Vec<Value>>;diagnostics_cache 独立锁 |
crates/services/services-core/src/lsp/process.rs、assembly/core/src/service/lsp/workspace_manager.rs |
中 |
| T4 | 会话持久化:sanitize_messages_for_persistence 改 Cow 按需 clone;to_string_pretty→compact to_writer;加 200ms dirty-window 合批(保留 turn 结束强制落盘) |
assembly/core/src/agentic/persistence/manager.rs:646-654,1287-1298、services-core/src/json_store.rs:211 |
中(崩溃恢复语义需回归测试) |
| T4b | (进阶)turn 快照改 JSONL 追加式,turn 结束/压缩时才全量重写 | 同上 + 读取端 | 高(格式迁移,需兼容旧快照) |
| T5 | 文件树扫描:sort 前落地 (entry, file_type, metadata) 元组;Windows 权限复用 metadata;canonicalize 入 spawn_blocking;Vec 预分配;评估切换 ignore::WalkBuilder 并默认尊重 gitignore;合并 stats 复制版 |
services-core/src/filesystem/tree.rs:436-560,572-769,897 |
中(gitignore 默认值属行为变化,建议加开关) |
| T6 | 终端 transcript:TranscriptWriter 持常开 BufWriter<File>,去 per-chunk open/flush;写路径移 spawn_blocking/专用线程 + mpsc |
services/terminal/src/transcript.rs、session/manager.rs:346 |
中(注意会话结束 flush 与文件轮转) |
| T7 | 流式事件队列:AgenticEvent id 字段改 Arc<str>;broadcast 改 Arc<EventEnvelope>;stats 改 AtomicU64 |
execution/agent-stream/src/lib.rs、agent-runtime/src/event_queue.rs:178-207 |
中(类型改动波及面广,建议分 3 个 PR) |
| T8 | ACP tracker:raw_input 改 Option<Arc<Value>>,消除 4 次深拷贝 |
interfaces/acp/src/client/stream.rs:180-320 |
低 |
| T9 | 侧栏拖拽:rAF 合并 + 拖拽期直写 --nav-width CSS 变量,mouseup 落 state(照抄 useResizer.ts 模式) |
web-ui/src/app/layout/WorkspaceBody.tsx:53-91 |
低 |
| T10 | 桌宠:① hover 轮询加窗口活动/有气泡守卫,rect 缓存;② 打字机目标文本改 useRef,interval deps 改 [hasTypingOutput],去每 tick 重建 |
AgentCompanionDesktopPet.tsx:308-339,440-500 |
低 |
| T11 | 编辑器外部变更检测:订阅 file-system-changed 按路径过滤 + focus 校验,本地轮询降 30s 兜底(远程保留现状) |
CodeEditor.tsx:1830-2000、MarkdownEditor.tsx:400-440 |
中(需覆盖 watcher 不可用的降级路径) |
| T12 | EventBus 历史改环形缓冲;生产只记事件名+时间戳 | web-ui/src/infrastructure/event-bus/EventBus.ts:280-310 |
低 |
| T13 | 状态机:context 不可变更新,getSnapshot 免 clone(先审查订阅方无原地修改) |
flow_chat/state-machine/SessionStateMachine.ts |
中 |
| T14 | 搜索行级优化:先 match 后分配 + 行缓冲复用;grep 删冗余 is_file()、sink 直出 Vec<String>;结果收尾 mem::take |
services-core/src/filesystem/tree.rs:1140,1275,1485、tool-execution/src/search/grep_search.rs:743,776,835-875 |
低 |
| T15 | file_watch:线程内路径表改同步快照,去 per-event block_on;watcher 增量注册 |
services-integrations/src/file_watch/service.rs:117-197 |
低 |
| T16 | 目录分页短 TTL 缓存 + 删手工 Value 转换层(直接返回 derive Serialize 的节点) | apps/desktop/src/api/commands.rs:2315-2441 |
中(缓存失效需订阅 file watch) |
| T17 | 批量 IPC:后端新增 get_turns_files 批量命令,前端 SnapshotAPI 改批量(过渡期 Promise.all) |
SnapshotAPI.ts:514-531 + 对应 Rust command |
低 |
| T18 | PTY chunk:from_utf8_lossy().to_string() 改零拷贝路径;tap 改 Arc<str>;HeadTailText 改字节环形缓冲 |
services/terminal/src/session/manager.rs:285,429、exec.rs:934-959 |
低 |
| T19 | 杂项清理包:front-matter 正则静态化并统一实现;App.tsx 补 disposed;BrowserPanel 补卸载清理;ToolExecutionService 保存 unlisten;FILE_LOCKS 弱引用淘汰;会话列表第 2 档限流 | 见 #25-#30 | 极低 |
建议实施顺序:T1/T2/T8/T9(低风险高收益,先行)→ T3/T6/T4 → T5/T7 → 其余按资源排期。T4b 与 T5 的 gitignore 默认值涉及行为变化,需产品确认。
四、误报排除记录(已确认无需处理)
| 候选 | 排除依据 |
|---|---|
| 终端输出无合批 | pty/data_bufferer.rs:89-139 已有 5ms/64KB 合批,mem::take 零拷贝移交,预分配到位 |
| replay 历史 O(n) 重建 | session/replay.rs:65-82 push_str 合并,摊还 O(1)。(顺带发现正确性缺陷::130-135 超 100KB 时 pop_front 一次丢掉全部合并历史,重连 replay 为空 —— 建议按字节截断,已列 T19 之外单独跟进) |
grep/edit_constraint_guard/两个 redaction/claude command_source 的 Regex::new |
均为每次搜索一次性编译或 OnceLock/get_or_init 静态缓存(grep_search.rs:654、edit_constraint_guard.rs:203-217、session_usage/redaction.rs:77-90、diagnostics/redaction.rs:114-173、claude-code-adapter/command_source.rs:723-757) |
终端/exec 的 .lock().await 跨 await |
session/manager.rs:288-297,411-444、exec.rs:744-791 均为显式作用域收窄,锁外 notify,写法正确 |
| persistence per-session/per-path 锁跨 IO | 锁粒度即单 session/单文件,串行化是设计目标 |
persistence.rs save_json 每次备份 |
调用方仅 workspace/cron/git-history 低频配置写 |
ThemeService JSON 深拷贝(ThemeService.ts:120) |
仅注册/导入自定义主题时执行,不在切换/渲染路径 |
| canvasStore 两处 structuredClone | 仅工作区切换触发,LRU 有上限 |
AppManager resize(AppManager.ts:413-430) |
已 200ms debounce,shutdown 清理 |
| SessionScene 拖拽 mousemove | rAF + 直写 DOM,本仓正确范例 |
api.listen 竞态泄漏(大范围) |
tauri-adapter.ts:120-155 已正确处理 promise/unlisten 竞态;抽查 10+ 调用点均正确清理 |
| FileExplorer 文件树 | >100 节点走 react-virtuoso 虚拟化(FileExplorer.tsx:219,490-503),行组件 memo |
| ModelRoundItem / Markdown 渲染 | 全量 memo + useMemo + 渐进渲染,是全仓性能做得最好的区域 |
| 帐号/文件面板/Peer ping 轮询 | 60s / 15s / 20s,频率合理且有存在理由 |
ACP update_session_from_events 三次遍历 |
events 长度 1-2,clone 仅低频事件命中时发生 |
| ConfigAPI 逐路径读取 | 主路径已批量 get_configs,逐路径仅为降级 fallback |
启动期 block_in_place(lib.rs:708)/ workspace_search shutdown_blocking |
一次性启动/关闭路径,非运行时热点 |
| 文件搜索进度 emit | BatchedFileSearchProgressSink 已合批 |
报告完。共收录 30 项确认问题(高 9 / 中高 2 / 中 13 / 低 6),全部经上下文与调用频率核实。