perf: 性能优化、兼容性增强与长会话稳定化(含架构文档) - #438
Open
yunper-wang wants to merge 17 commits into
Open
Conversation
三个 install-stream hook(agent/opencode-plugin/officecli)的 logs 数组 原先无上限,长安装下渲染端把每行都渲染成 DOM 节点会无界增长,且每次追加 为 O(n) 拷贝(整体 O(n²))。统一改为共享的 appendInstallLogLine,仅保留 最近 1000 行。同时三个 hook 原先都没有 useEffect 卸载清理,组件中途卸载会 泄漏全局事件订阅并在卸载后 setState;现补齐卸载退订与 subscribe 解析间隙 的取消守卫。
byToolUseId 原先只增不删,一个委托密集的会话会为每个子代理累积一条绑定, 且每次事件都 new Map 全量克隆(O(n)),随应用生命周期无界增长。绑定本质是 实时查找:被逐出的旧条目,其父卡片本就通过 tool-call 快照 meta 的 fallbackChildConversationId 回退读取持久化子会话(与从磁盘恢复旧会话的既有 路径一致)。现新增 setDelegationBinding,仅保留最近 200 条并按 LRU 逐出最旧。
parse_jsonl_chat_value 原先对每一行用 .ok()?,任何一行解析失败(活跃会话 文件被并发写入时末行截断很常见)就让整个函数返回 None,导致该会话从侧边栏 消失且详情拉取失败。改为跳过坏行,与 claude/codex 等其余 JSONL 解析器的 Err(_) => continue 行为对齐。新增测试:两行完好记录后跟一行截断,断言会话 仍列出且详情保留完好消息。
ACP tool_call_update 的 rawInput 是累积快照(携带当前已知完整输入),而非增量 片段。原先把每个快照 push 进 raw_input_chunks 再 join 解析:单片段解析失败时 join 全部历史片段是 O(k²),且在会话写锁内执行会卡住该连接的所有事件;拼接多个 前缀也只会得到无效 JSON(该分支是 delta 假设的残留)。快照模型下只有最新片段有 意义,故移除累积字段,仅对最新快照尽力解析——行为与原快照模型完全等价,同时消除 O(k²) 与随工具输入增长的内存占用。同步更新相关测试断言。
阻塞式 read 只要有字节就绪即返回(不会等待填满缓冲),因此把 8KB 缓冲增大到 64KB 后,交互式输出的延迟不变,而构建日志、cat 大文件等批量输出会被聚合为约 1/8 的广播事件数(原先每 ≤8KB 一个事件)。缓冲改为堆分配以控制线程栈占用;同时 去掉 read_loop 热路径上 TerminalEvent 的冗余 clone。
alertedErrorDetailsRef 以 contextKey 存储错误证据字符串(可能很大,含堆栈),用于 抑制同一连接的重复告警。但单连接移除路径(viewer 拆离与 owner 断开)原先只清理 lastActivityRef 等,未清这个 map,死连接的证据字符串会一直累积到后端切换时的整体 clear。补按 contextKey 删除——同时也更贴合该 map 的注释意图:残留条目会抑制复用同 一 contextKey 的新会话的首次告警。
resolve_patch_line_numbers 原先对每个 apply_patch/edit 块都在 resolve_patch_text 内重新 fs::read_to_string 并按行分配——编辑密集的会话里同一文件被多个补丁块触碰时, 每次详情解析都重复读同一文件 N 次。现新增按次(per-parse)的 FileLinesCache:命中与 未命中都缓存,同一文件每次解析至多读盘一次。解析本就是读取文件某一时刻的快照,故跨 块共享一次读取不改变输出。保留 resolve_patch_text / load_file_lines 原 pub 签名作为 薄封装,不影响 connection.rs 的实时单补丁调用方。新增行为保持与缓存命中/未命中测试。
summary_cache 是进程全局的 insert-only map,原先无逐出:已删除会话文件的条目 (其路径在 fingerprint 处就 bypass,永不再被观察)会随应用生命周期一直累积。 现给 CacheEntry 记录 last_used,命中时刷新;插入新键且该 agent 条目数达 1024 上限 时按 LRU 逐出最旧条目。逐出永远安全——(mtime,size) 指纹会在下次访问时重新校验, 被逐出仅是下次重新解析。逐出逻辑抽为可独立测试的纯函数。
原先进程内每次详情拉取(viewer 轮询、每个 load-older 翻页、turn 结算 refetch)都对 整个 transcript fs::read + 逐行 JSON 解析,50-100MB 的会话在亚秒级轮询下代价很高。 现把解析拆为两半: - parse_raw_session(读文件→累积→group_into_turns)是纯 transcript 函数,结果按 (mtime,size) 指纹缓存进进程级缓存;命中即跳过全量读+解析。 - 其后的全部后处理(relocate_orphaned_tool_results / structurize_read_tool_output / resolve_patch_line_numbers / backfill_durations / attribute_subagent_usage / stats) 每次拉取仍重新执行——它们依赖独立于 transcript 变化的外部文件(被改的源文件、子代理 transcript),故输出保持新鲜,窗口 seam/prefix_hash 字节一致。 内存以 transcript 字节数(watermark)为足迹代理做 64MB 字节预算 LRU;超限的单个大 transcript 不缓存(维持原先每次全新解析)。沿用 summary_cache 的指纹校验:仅在解析 前后指纹一致时才缓存,撕裂/流式写入不被误存。 验证:新增差分测试断言缓存命中与全新解析字节一致、文件增长后正确失效;35 个 claude 单测、9 个 insta 快照、87 个 conversations 命令测试全过(输出字节不变)。
梳理整体架构:一份业务核心经 feature 开关编译出三种二进制(桌面/服务器/codeg-mcp) 的设计决策与四个关键抽象(EventEmitter、_core、AppState、Transport);后端 parsers 历史读取侧与 acp 实时运行侧的划分;两条数据通路;以及 ACP 事件流、双事件总线、 Subscribe-with-Snapshot attach 协议、多智能体委托、窗口一致性协议、前端流式合帧等 深层机制,附目录速查表。
extractLatestPlanEntriesFromMessages 原先在无 plan 的会话(常见)里倒序扫所有消息, 并对每个 reasoning part 全量 split+regex;而 message-list-view 的 useMemo 因输入数组 每 16ms 批次重建而从不命中,导致整段提取每批都重跑。适配器对未变(非流式)turn 返回 稳定的 AdaptedMessage 引用(同引用 ⟺ 同内容),故新增按消息引用的 WeakMap 缓存: 每批只重解析仍在流式的那一条消息,其余命中。同时无 plan 时返回共享空常量,保持调用方 useMemo/React.memo 依赖引用稳定,不再因新 [] 触发重渲染。新增引用稳定与缓存正确性测试。
buildStreamingTurnsFromLiveMessage 在每个 16ms 流式 flush 里对 live message 的每个
tool_call 都调 kimiTodoWriteEntries(883 与 1142 两处),原先无条件 JSON.parse 其
raw_input——而绝大多数工具调用(Read/Write/Bash/Edit,raw_input 可能是数 KB 文件内容)
并不含 todos。函数只有存在顶层 todos 数组时才可能返回非 null,故先用 includes('"todos"')
做必要不充分预筛:无该子串直接返回 null,跳过 JSON.parse。子串扫描远廉价于解析,行为
完全等价(既有正例/反例测试全过)。
crypto.randomUUID() 仅在安全上下文(HTTPS/localhost/多数 Tauri webview)可用。Codeg 服务器模式经纯 HTTP 局域网 IP 访问时为非安全上下文,且 Linux 桌面 WebKitGTK 可能不把 自定义协议当作安全上下文——此时直调会抛 TypeError。两处改为复用已有的 randomUUID() helper(回退到各处可用且加密安全的 crypto.getRandomValues): - remote-desktop-transport:构造期类字段直调,无守卫无回退,非安全上下文实例化即崩。 - web-service-settings:token 生成原先回退到 Math.random(),可预测,对安全令牌太弱。 另为 randomUUID 补测试(含 stub 掉 crypto.randomUUID 的非安全上下文回退路径)。
collapseLiveCollabBlocks 的 content.some(isCollabBlock) 在每个 16ms 流式 flush 里对每个
tool_call 调 isCodexCollabInput,原先 tryParseObject 无条件 JSON.parse raw_input——包括
所有非 codex 会话(Claude 等)。collab 输入要求 senderThreadId/receiverThreadIds/
agentsStates 三键同时存在(AND),故缺任一即必为 false;现用 includes('"agentsStates"')
做必要不充分预筛,无该子串直接返回 false,跳过解析。行为等价(collab 正例/反例测试全过)。
与 Claude 同理:codex 每次详情拉取(viewer 轮询、每个 load-older 翻页)都重读+逐行 解析整个 rollout。现新增通用的 summary_cache::detail_get_or_parse——按 (AgentType,path) 以 (mtime,size) 指纹缓存整个 ConversationDetail,命中即免全量重解析;64MB 字节预算 LRU(以 transcript 字节数为足迹代理),超限不缓存;命中返回克隆,调用方可自由注入 delegation meta / 设计数。仅缓存非空详情(避免冻结空调解析的 Utc::now 回退)。 freshness 契约:指纹覆盖 transcript 文件本身。codex 的 collab 子代理统计与 patch 行号 反映上次解析时的文件状态(外观性,且 rollout 一变即自愈——活跃 collab 会写 rollout 使指纹变化触发新鲜解析)。无时间戳记录回退 Utc::now 在缓存下保持首次值,反而让窗口 prefix_hash 跨 fetch 稳定(无缓存时每解析都新 now,前端会误判未变前缀被改写)。 验证:新增差分测试(缓存命中与全新解析字节一致 + 文件增长后正确失效);164 个 codex 单测、9 个 insta 快照(codex 快照不变)、8 个 summary_cache 测试、clippy 全过。
pi 的 parse_detail 是纯单文件解析(仅 resolve_patch 读源文件,与 codex 同属可接受的 取舍),无 Utc::now 依赖(非空详情才缓存的守卫覆盖其 started_at 回退)。复用 summary_cache::detail_get_or_parse,一行封装接入。新增差分测试(缓存命中与全新解析 字节一致 + 追加记录后正确失效),复用 write_session/sample_records 夹具。openclaw 因 leaf_id 分支导致同路径多详情、以 (AgentType,path) 为键会冲突,故不纳入。
codebuddy 的 parse_detail(path, conversation_id) 签名干净。其解析会读子代理文件 (agent_stats_from_subagent),缓存后这些外观统计反映上次解析状态(与 codex 同属可 接受取舍,rollout 一变即自愈)。复用 summary_cache::detail_get_or_parse 接入。新增 差分测试(复用 write_session 夹具,缓存命中字节一致 + 追加后失效)。19 个单测、快照、 clippy 全过。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
概述
围绕性能优化、兼容性增强、长会话稳定化三个方向的一组修复与优化,共 17 个提交。每项改动均先经代码实读验证,再落地并补齐测试。
性能优化
summary_cache::detail_get_or_parse,按(AgentType, path)以(mtime,size)指纹缓存ConversationDetail,64MB 字节预算 LRU、命中返回克隆。已接入 Claude / Codex / pi / CodeBuddy——此前每次详情拉取(viewer 轮询、每个 load-older 翻页、turn 结算 refetch)都对整个 transcript 全量重解析。其余解析器因多文件/分支参数/外部元数据等原因不适合,已在代码与提交说明中逐一标注。upsert_tool_call:消除会话写锁内的 O(k²) join+parse,并去掉随工具输入增长的内存占用(raw_input_chunks累积字段移除)。plan-overlay提取按消息引用 WeakMap 记忆化;kimiTodoWriteEntries/isCodexCollabInput加子串预筛,免去多数工具调用每 16ms 帧的 JSON.parse。长会话稳定化
alertedErrorDetailsRef。summary_cache加 LRU 逐出,修复进程级缓存无界增长。兼容性增强
randomUUID:非安全上下文(HTTP 局域网 server 模式 / Linux WebKitGTK)回退到crypto.getRandomValues,修复 remote-desktop-transport 构造即崩与 web-service token 弱回退(Math.random)。文档
ARCHITECTURE.md:整体架构与关键机制(双模式共享核心、双事件总线、Subscribe-with-Snapshot attach、多智能体委托、窗口一致性协议、流式合帧)。验证
clippy -D warnings干净;server + desktop 双配置编译通过。tsc --noEmit干净、next build通过、ESLint 干净、vitest 0 新增失败(247 个失败为与 main 基准完全一致的既有 jsdomlocalStorage环境问题,净增 14 个通过测试)。