Main no longer opens desktop business SQLite. A single utility-process
storage owner holds the Assistant, Knowledge, attachment, document-result,
remote-binding and native model-ledger databases and runs whole domain
transactions; Main and Runtime callers use explicit async domain operations.
- Remove the Main readWithFallback path and the supervision worker's
separate business write connection; manifests are scanned read-only and
committed by the sole writer.
- Remote events are ACKed only after commit; process loss reports
unconfirmed writes instead of replaying them.
- Use native utilityProcess messaging with one in-flight bound (32) and
ordered waiting instead of capacity rejection and retries.
- Start the storage process before the window so startup overlaps.
- Keep short tool output in memory and store full output once in owned
storage; paging, branching, restart and deletion reuse that copy.
- Put required runtime launch files under goodbuddy-prefixed owned temp
directories and reclaim confirmed stale ones; stop writing duplicate
parsed.md.
- Add lint guards for SQLite ownership and architecture, concurrency
acceptance fixtures, and maintenance/architecture documentation.
Validation: typecheck and lint pass. Alternating baseline (edf019aa) vs
candidate runs show no regression on single-session, C01, C02, C04 and a
5-minute C06; Main p99 drops from ~21 ms to ~16.7 ms under concurrency.
Fresh portable copy (1.75M events) upgrade content unchanged; real-model
output paging passes with 4 external requests. Full npm test runs had
1-9 different timing-sensitive failures per run that pass when rerun.
Not covered: C03 review publication, C07 source body, app-level C05.
87 KiB
GoodBuddy 产品、性能与体验综合改进计划
文档信息
| 项目 | 内容 |
|---|---|
| 文档类型 | 跨功能实施路线图 |
| 状态 | 规划中 |
| 版本 | 1.0 |
| 日期 | 2026-08-31 |
| 适用范围 | GoodBuddy Desktop、GoodBuddy Agent、发布与质量流程 |
| 评审基线 | Desktop 0.11.13、Agent lock 0.11.14 |
1. 目的
本文把 2026-08-31 对当前仓库进行的功能、性能、体验和交付评审结果整理为一份可分批实施、 持续更新的权威清单。后续每一批工作都从本文选择明确范围,完成后更新状态、验证结果和相关 产品文档,不再维护平行的临时待办。
本计划优先完成已经存在但尚未形成真实用户闭环的能力,其次处理经过测量确认的性能问题, 再安排依赖外部平台或产品选择的增强能力。实施继续遵循以下原则:
- 先保证完整、可用的生产路径,再扩展能力范围。
- 性能优化必须有基线、目标和优化后的对比数据。
- 复用现有合同、IPC、Worker、通知、错误边界和界面组件,不引入不必要的框架。
- 问答与工具任务统一执行,使用当前本机或 SSH 账号权限与已接入能力,无产品模式或一般工具审批。
- 每批只处理列明的范围,不借机进行无关重构。
2. 明确排除的工作
本计划不包含初始 Renderer Bundle 优化,包括但不限于:
- 不为了缩小入口包继续拆分轻量同级页面。
- 不单独延迟 KaTeX、全局样式或其他入口依赖。
- 不调整现有 Bundle 预算来制造通过或失败。
- 不以 Bundle 大小为理由改变当前页面交互语义。
现有构建预算和按路由拆包继续作为回归保护保留。只有后续获得首屏解析或执行耗时证据,并由 用户单独确认范围后,才重新讨论该项。
3. 状态与优先级
3.1 状态
| 状态 | 含义 |
|---|---|
| 待开始 | 范围已确认,尚未实施 |
| 进行中 | 已进入当前实施批次 |
| 阻塞 | 存在明确外部依赖,已记录解除条件 |
| 已完成 | 代码、真实路径、测试和文档均完成 |
| 不做 | 经重新评审后明确移出范围,并记录原因 |
3.2 优先级
| 优先级 | 含义 |
|---|---|
| P0 | 影响正式交付、安装或已声明生产路径,发布前处理 |
| P1 | 阻断核心工作流或造成明显错误、卡顿、误导 |
| P2 | 重要增强或规模化问题,可在核心闭环后实施 |
4. 分批实施总览
批次表示建议顺序,不表示必须一次完成整批。每次可以从当前批次选择一组边界清晰、能够独立 验收的事项。
| 批次 | 主题 | 事项 | 开始条件 | 完成门槛 |
|---|---|---|---|---|
| 0 | 正式交付闭环 | REL-01 至 REL-03、QA-03 |
发布资源和目标平台明确 | 正式产物可安装、可启动,版本与验证声明准确 |
| 1 | 核心工作流与恢复体验 | FUN-01、FUN-04、UX-01 至 UX-12、OPS-01 至 OPS-03 |
无 | 用户可理解状态、从常见失败中直接恢复,并能导出有界诊断 |
| 2 | 通道、更新和真实 Runtime | FUN-02、FUN-03、FUN-05、QA-01、QA-02 |
对应平台测试配置可用 | 文件办公闭环、更新下载闭环和真实 Runtime E2E 均通过 |
| 3A | 性能基线 | PERF-01、PERF-03、PERF-05、PERF-07、PERF-09 |
固定测试数据和设备 | 形成可重复基线、目标阈值和原始结果 |
| 3B | 已测量性能优化 | PERF-02、PERF-04、PERF-06、PERF-08、PERF-10 |
对应 3A 指标确认存在瓶颈 | 指标改善且功能、取消、恢复语义不回退 |
| 3C | 架构级界面性能改造 | PERF-11 至 PERF-17 |
PERF-11 完整 App 基线已产生 |
第 7.1 节五条架构规则成立,并由基准和 lint 持续守护 |
| 4 | 增强能力 | FUN-06 至 FUN-09 |
核心闭环稳定;首个电脑控制平台已选定 | 每项形成独立、完整、可真实使用的工作流 |
5. 发布与交付
REL-01 Desktop 与 Agent 版本闭环
- 优先级 / 状态: P0 / 待开始
- 目标: 当前 Desktop 源码依赖的 Agent 能从正式渠道取得并完成真实 Host 验证。
- 范围:
- 发布并验证 Agent
0.11.14,或在 Desktop 发布前恢复到已发布 Agent 契约。 - 验证 Linux x64、Linux arm64 正式候选包。
- 覆盖安装、Attach、工具执行、取消和断线恢复。
- 覆盖 GitHub、镜像和离线 GoodBuddy 传输路径。
- 发布并验证 Agent
- 验收:
- 干净用户目录能获取预期版本。
- Desktop 到 Agent 的正常路径和恢复路径均使用当前候选产物通过。
- 发布说明准确标注已验证架构和仍未验证范围。
- 外部依赖: Agent 发布审批、双架构正式产物、真实 Linux arm64 环境。
- 起始证据:
package.json、agent-runtime-lock.json、docs/features/remote-host/technical-design.md。
REL-02 正式 Latest 签名门槛
- 优先级 / 状态: P0 / 待开始
- 目标: 官网正式推荐下载不指向可能被系统默认安全策略阻止的测试产物。
- 范围:
- macOS 缺少签名或公证凭据时,不更新正式 Latest。
- Windows 未签名构建不作为默认推荐安装包。
- 未签名构建继续作为明确标注的测试 artifact。
- 验收:
- 发布工作流在签名条件不满足时不会切换正式 Latest。
- 正式下载在全新 macOS、Windows 默认安全策略下完成首次启动。
- 外部依赖: Apple 和 Windows 代码签名凭据。
- 起始证据:
.github/workflows/packages.yml、BUILD.md、README.md。
REL-03 发布包启动冒烟
- 优先级 / 状态: P0 / 待开始
- 目标: 发布成功代表打包应用能够启动,而不只是文件结构完整。
- 范围:
- 在对应 CI Runner 启动解包应用。
- 等待 Main、Preload、Renderer ready 健康标识后退出。
- Windows x64 接入现有 packaged DeepSeek Harness smoke。
- 为启动设置总超时,并保留无用户内容的失败诊断。
- 验收:
- 缺 DLL、ASAR unpack、Preload 或 Main 启动错误会使发布任务失败。
- 探针无凭据、无外网也能执行。
- 起始证据:
.github/workflows/packages.yml、build/build-release.cjs、build/run-packaged-deepseek-harness-smoke.cjs。
6. 核心与增强功能
FUN-01 定时任务编辑
- 优先级 / 状态: P1 / 待开始
- 目标: 用户无需删除并重建计划任务即可纠正配置。
- 范围:
- 新增共享 Update schema、Preload 方法和可信 IPC handler。
- 复用定制任务创建对话框作为编辑模式。
- 支持修改名称、Prompt、Runtime、首次运行时间和周期。
- 保留原 Task、Conversation、Schedule 和历史运行关联。
- 验收:
- 从任务界面进入编辑,保存后列表和下一次运行时间立即更新。
- 编辑不产生重复 Task 或 Schedule。
- 正在运行的实例继续使用启动时快照,后续触发使用新配置。
- 起始证据:
CustomTaskDialog.tsx、TaskScheduleActions.tsx、src/main/ipc.ts。
FUN-02 企业微信附件收发
- 优先级 / 状态: P1 / 待开始
- 目标: 企业微信完成“接收文件或图片、处理、回传成果”的办公闭环。
- 范围:
- 接收官方接口支持的图片和普通文件。
- 将附件接入现有远程消息上下文。
- 支持回传当前任务生成的图片或文件。
- 复用
ChannelMediaAttachment、现有大小限制和 outbox 清理。
- 验收:
- 收到支持的附件不会再返回
attachment_not_supported。 - 任务生成的有界成果能发送到原会话。
- 超限、下载失败和发送失败提供明确错误且临时文件得到清理。
- 收到支持的附件不会再返回
- 起始证据:
src/main/channels/wecom-driver.ts、src/main/channels/wechat-sidecar.ts。
FUN-03 钉钉附件收发
- 优先级 / 状态: P1 / 待开始
- 目标: 钉钉非文本消息不再静默丢失,并完成文件办公闭环。
- 范围:
- 支持官方接口允许的图片和普通文件接收。
- 支持任务成果附件回传。
- 对暂不支持的消息类型返回明确说明。
- 验收:
- 支持的非文本消息进入对应远程 Conversation。
- 不支持的消息有可见反馈,不被静默忽略。
- 收发限制和清理行为与其他通道一致。
- 起始证据:
src/main/channels/dingtalk-driver.ts、src/main/channels/dingtalk-channel-driver.ts。
FUN-04 浏览器错误分类
- 优先级 / 状态: P1 / 待开始
- 目标: 浏览器任务失败后,用户知道失败阶段和下一步操作。
- 范围:
- 保留代理、DNS、会话创建、CDP、导航和截图等失败阶段。
- 增加少量稳定错误码及一句修复建议。
- 在任务记录和界面中呈现,不建立通用错误协议框架。
- 验收:
- 典型失败不再全部显示为“工具执行失败”。
- 原始敏感内容不进入 Renderer 或诊断日志。
- 取消继续被识别为取消,而不是错误。
- 起始证据:
src/main/agent/model-runtime.ts、docs/status/computer-control-implementation-status.md。
FUN-05 更新下载闭环
- 优先级 / 状态: P1 / 待开始
- 目标: 用户能在应用中取得当前平台安装文件并确认文件完整。
- 范围:
- 从当前更新源读取当前平台产物。
- 下载到用户选择目录。
- 使用现有发布清单校验大小和 SHA-256。
- 校验成功后允许打开文件或所在目录。
- 安装继续由用户手动触发。
- 验收:
- 下载源、版本、架构和当前系统匹配。
- 损坏或哈希不匹配的文件不会被呈现为可安装。
- 失败保留目标和错误上下文,可重新下载。
- 起始证据:
UpdateSettingsSection.tsx、src/main/version-checker.ts、src/main/ipc.ts。
FUN-06 定时任务常用周期
- 优先级 / 状态: P2 / 待开始
- 目标: 覆盖常见办公日历,而不要求用户编写 Cron。
- 范围: 增加工作日和每月周期,并持续显示下一次实际执行时间。
- 验收: 创建、编辑、重启恢复和到期触发均按本地日历执行且不重复。
- 依赖:
FUN-01、FUN-07。 - 起始证据:
src/shared/assistant-contracts.ts、docs/features/task-and-job/scheduled-task-prd.md。
FUN-07 时区和错过执行策略
- 优先级 / 状态: P2 / 待开始
- 目标: 用户明确控制任务在哪个时区运行,以及错过后是否补跑。
- 范围:
- 默认本机 IANA 时区。
- 支持
skip和run_once。 - 显示 DST 影响后的下一次实际运行时间。
- 验收:
- 重启或系统恢复后不会形成补跑风暴。
- 时区和策略可创建、编辑、持久化并在界面复现。
- 起始证据:
src/main/assistant/assistant-database.ts、docs/features/task-and-job/scheduled-task-prd.md。
FUN-08 旧版 Office 文件
- 优先级 / 状态: P2 / 待开始
- 目标: DOC、XLS、PPT 能进入聊天附件和知识库解析路径。
- 范围:
- 检测本机 LibreOffice。
- 使用 headless 转换到 OOXML 或 PDF 后复用现有解析器。
- 未安装时提供明确安装或手工转换建议。
- 验收:
- 三种旧格式各用真实文件完成聊天解析和知识库导入。
- 转换取消、超时和失败后清理 GoodBuddy 临时目录。
- 不内置第二套文档解析引擎。
- 起始证据:
src/main/knowledge/document-parser.ts、DocumentParsingSettingsSection.tsx。
FUN-09 首个原生电脑控制适配器
- 优先级 / 状态: P2 / 待开始
- 目标: 在一个明确的主要部署平台上真实控制桌面应用。
- 范围:
- 首先从 Windows UIA、macOS AX 或 Linux 原生方案中选择一个平台。
- 完成窗口快照、点击、输入、取消和退出清理。
- 接入现有 computer-control broker。
- 验收:
- 在真实桌面应用完成一次可重复的查看、点击和输入任务。
- 桌面操作沿用能力开关、资源范围和账号权限,执行路径记录活动。
- 取消和关闭后不遗留 GoodBuddy 控制会话。
- 外部依赖: 首个平台产品决策及对应真实设备。
- 起始证据:
src/main/capabilities/capability-service.ts、docs/status/computer-control-implementation-status.md。
7. 性能
PERF-01 启动性能测量
- 优先级 / 状态: P1 / 待开始
- 目标: 得到冷启动各阶段的可重复耗时,而不是依据代码顺序猜测。
- 指标:
app.whenReady、窗口创建、数据库、知识库、Provider/Gateway、Runtime hydration、 IPC 注册、loadMainWindow、ready-to-show和 Renderer 首帧。 - 验收: 固定设备至少运行五次冷启动,报告中位数和 p95;诊断不包含用户内容。
PERF-02 启动路径优化
- 优先级 / 状态: P1 / 待开始
- 目标: 尽早显示可理解的应用壳,并延后经测量确认的非首屏工作。
- 实施条件:
PERF-01证明具体阶段构成主要延迟。 - 验收: 首帧和可交互时间达到批次开始时确定的目标,初始化失败仍有明确状态和恢复入口。
PERF-03 长流式回答基准
- 优先级 / 状态: P1 / 待开始
- 场景: 100KB、500KB、1MB Markdown 回答,包含推理、代码块和常见富文本。
- 指标: React commit 数和 p95、Long Tasks、Markdown 渲染时间、输入延迟、滚动帧率、 Renderer 堆内存。
- 验收: 测试数据、运行步骤和原始结果可重复执行。
2026-09-28 Measurement Evidence (Partial)
npx vitest run tests/streaming-markdown.electron.test.ts runs the real
MarkdownRenderer in Electron 43.2.0 / Chromium 150.0.7871.129. It compares the
original normalizer with fast paths that skip unchanged text, retaining live
Markdown, math, code and HTML behavior. It also measures a 102,400-byte answer
at six growing prefixes, alternating original/current execution order in the
same page, with one warm-up sweep and three measured sweeps (18 samples each).
| Measurement | Original | Current |
|---|---|---|
| 100KiB growing-prefix render median, isolated run | 54.80 ms | 54.65 ms |
| Same run p95 | 95.50 ms | 106.10 ms |
| Growing-prefix render median, full-suite run | 53.50 ms | 53.10 ms |
| Same run p95 | 93.70 ms | 83.40 ms |
These development-mode measurements include synchronous React rendering, DOM commit and style/layout, but exclude frame waiting and paint. With 18 samples, nearest-rank p95 is the maximum sample. The results do not establish an overall rendering improvement. Smaller 6.4-7.2K-character corpora do show lower normalizer cost: prose median 33-34 microseconds to below 1 microsecond, and mixed math/code median 32-35 to 16-20 microseconds. The production changes remain limited to this preprocessing and memoizing unchanged subagent-derived collections. In the four-subagent regression, initial render plus six updates builds the lookup map once instead of seven times; block-dependent scans still run when blocks change.
Validation: 550 prefix normalization comparisons, 1,100 short-prefix DOM
comparisons and 24 long-prefix DOM comparisons passed. The real Electron
terminal-status probe covers cancellation, failure, retry and persistence via
three loopback model-stub requests; the Mermaid viewer covers nine scenarios
and ten PNG downloads. No external model calls were used for these probes.
Full npm test passed 459 files / 5,397 tests, with 11 files / 84 tests skipped.
Type checking and scoped lint passed. Full lint remains blocked by the existing
untracked sidebar-final probe files.
This is partial evidence for PERF-03, not completion: full-App input latency, frame rate, heap measurements and 500KB/1MB scenarios remain unmeasured.
PERF-04 流式更新合并
- 优先级 / 状态: P1 / 待开始
- 目标: 降低长回答期间的状态复制和完整 Markdown 重解析。
- 实施条件:
PERF-03确认 commit 或 Markdown 渲染是瓶颈。 - 范围:
- 同一请求的 text/reasoning delta 按
requestAnimationFrame合并。 - 每帧最多提交一次相关 React 状态。
- 流结束后再执行完整富文本渲染。
- 同一请求的 text/reasoning delta 按
- 验收: 目标指标改善;事件顺序、停止、重试、引用、工具和 Subagent 块不回退。
PERF-05 向量检索规模基准
- 优先级 / 状态: P1 / 耗时基线已完成,取消响应与峰值内存待测
- 场景: 1k、10k、50k chunks 的全文、向量和混合检索。
- 指标: 总耗时、
vectorScannedCount、Main event-loop lag、取消响应和峰值内存。 - 验收: 固定语料和查询集产生可比较结果,并保留检索质量指标。
2026-10-02 耗时基线
npm run perf:knowledge(build/run-knowledge-perf.cjs)用 esbuild 打包生产
KnowledgeDatabase,在临时 SQLite 中写入确定性随机语料(每文档 100 chunks,每 chunk 80 词,
384 维向量,与内置嵌入模型一致),重开连接后对每个规模各测 15 次 search、vectorSearch
和 hybridSearchWithDiagnostics(图谱关闭)。这些调用是同步的,在 App 中由 Main 线程直接
执行,所以单次耗时就是 Main 被阻塞的时长。同一台设备(Node 24.18),p95:
| chunks | 全文 | 向量 | 混合 |
|---|---|---|---|
| 5,000 | 14 ms | 54 ms | 93 ms |
| 20,000 | 59 ms | 173 ms | 219 ms |
| 50,000 | 130 ms | 396 ms | 520 ms |
向量扫描与 chunk 数线性相关,FTS 也随规模增长。按 50 ms 预算,约 5k chunks 起混合检索就会
使 Main 可感知地卡顿;5 万 chunks 时每次检索约冻结 Main 0.5 s,期间全部 IPC(包括流式输出、
终端和界面操作)排队。结论:PERF-06 的实施条件已满足;检索应先移出 Main(PERF-06,
随后并入 PERF-16)。取消响应和峰值内存尚未测量。
PERF-06 向量计算移出 Main
- 优先级 / 状态: P1 / 待开始
- 目标: 大知识库向量扫描不阻塞 Electron Main。
- 实施条件:
PERF-05确认 Main event-loop lag 或取消响应不达标。 - 范围: 优先复用已有 Worker,减少重复扫描和复制;本项不自动引入 ANN。
- 验收: 检索排序和质量不回退,Main 延迟与取消指标达到目标。
PERF-07 知识库 Snapshot 测量
- 优先级 / 状态: P2 / 待开始
- 指标: SQL 时间、对象数量、structured-clone 估算字节、IPC 往返、Renderer commit 和 峰值堆内存。
- 场景: 小型、典型和大型文档与图谱数据集。
- 验收: 明确首屏实际需要的字段和超过目标阈值的数据规模。
PERF-08 知识库按需加载
- 优先级 / 状态: P2 / 待开始
- 实施条件:
PERF-07证明完整 Snapshot 是可见瓶颈。 - 范围:
- 首屏只返回列表字段、状态和计数。
- 文档详情按选择加载。
- 图实体、关系和证据仅在图谱视图加载。
- 复用已有列表和 chunk 接口。
- 验收: 首开与切换指标改善,选择、刷新、取消和局部错误状态不回退。
PERF-09 Keep-alive 压力测试
- 优先级 / 状态: P2 / 文本会话部分已完成,图片与工具记录待补
- 场景: 连续打开 12 个会话,每个会话包含 80 条富 Markdown、图片和工具记录。
- 指标: DOM 节点、Renderer heap、GC pause 和流式 commit。
- 验收: 明确隐藏会话缓存是否构成实际内存或渲染问题。
2026-10-02 文本会话测量
npm run perf:app 在流式场景之后连续打开 24 个未访问过的种子会话(每个 20 条富 Markdown),
每 4 次记录挂载的面板数、DOM 节点数和强制 GC 后的 JS heap(CDP
HeapProfiler.collectGarbage + Runtime.getHeapUsage)。
| 已访问 | 挂载面板 | DOM 节点 | GC 后 heap |
|---|---|---|---|
| 0(两次流式之后) | 12 | 39,657 | 54.6 MB |
| 4 / 8 | 12 | 39,656 | 54.6–55.1 MB |
| 12 | 12 | 15,884 | 27.4 MB |
| 16 / 20 / 24 | 12 | 15,884 | 27.3–27.9 MB |
缓存上限 12 个面板有效:两条长流式回答被挤出缓存后,DOM 和 heap 回落并保持不变,后半段
增长为 0。此前观察到的“4 万节点、210 MB”是两条 40 KB 流式回答仍在缓存中时的瞬时值
(未 GC),不是持续泄漏。结论:文本会话场景下 keep-alive 不构成内存问题,PERF-10 不需要
调整缓存数量或有效期;含图片和工具记录的会话尚未测。
PERF-10 Keep-alive 轻量优化
- 优先级 / 状态: P2 / 已完成输入与热切换渲染隔离,压力测试待完成
- 实施依据: 2026-09-30 的真实 App 组件回归确认输入和热切换会重复渲染缓存内容;
PERF-09的 Electron 内存与 GC 压力测试仍待执行。 - 范围:
ChatHistoryPane、任务条和 Runtime 清单使用 memo,稳定快捷输入、重试、图片恢复、 定时任务回调及任务条 JSX。异步草稿操作仍绑定发起时的会话。- 仍不达标时才调整缓存数量或一小时有效期。
- 暂不引入完整虚拟化框架。
- 验收: 内存和 commit 指标改善,同时保留会话切换位置和快速恢复体验。
App.test.tsx 修复前回归记录:运行请求期间连续输入三次,历史面板、任务条、清单各
渲染三次,会话迁移存储读取三次;两个已缓存会话往返一次,Markdown 渲染 324 次。
长会话已展开 160 条消息,另一会话含一条消息,计数包含展开的推理正文。修复后上述
计数均为零。最终用例还覆盖可见且展开的运行中任务条、实时文本与清单更新、停止、
会话草稿、图片素材选择期间切换会话,以及滚动位置和推理展开状态保留。
这些是 jsdom 中真实 App 的渲染次数,使用 mock preload;尚未测量 Electron 输入延迟、
绘制耗时或内存。流式事件未延后,缓存容量和有效期未改变。
7.1 架构级界面性能改造(批次 3C)
2026-10-02 的架构评审确认,界面卡顿的主要来源是架构组织方式,而不是 Electron 或 GPU:
- Renderer 单组件状态:
App.tsx约 11k 行,App()内有 123 个useState、69 个useEffect。会话、消息、草稿、任务和设置都在其中,输入和每次流式 flush 都会重新执行 整个 App 函数体;多个大面板未 memo,隐藏的 keep-alive 视图也会参与渲染。 - Main 同步存储: 全部 SQLite 访问使用 Main 线程的
DatabaseSync,包括全表向量扫描、 会话列表与搜索、Story Graph 读取;PDF/Office 解析和附件同步文件 IO 也在 Main。 - 全量回写: 活动记录在任何变化后 250 ms 全量
replace;会话每 500 ms 定时保存;*Changed通知不带内容,Renderer 再重新查询完整列表。
改造完成后须同时满足以下规则,并由工具守护,避免回退:
- Main 只负责窗口、生命周期、权限和消息路由,不执行数据库查询、文档解析或批量计算。
- Renderer 状态按领域拆成外部 store,组件通过 selector 订阅;
App.tsx只保留布局壳。 - 流式文本和终端输出走专用通道,只更新正在显示的行或面板,按帧合并提交。
- 数据只有一个权威来源,跨进程只传增量;不再全量回写,也不再“通知后重新查全量”。
- 消息、会话、笔记、文件树等长列表虚拟化,DOM 数量不随数据量增长。
实施方式:先建立 PERF-11 基线,再沿 Renderer(PERF-12→PERF-13→PERF-14)和
数据进程(PERF-15→PERF-16)两条线并行推进,最后由 PERF-17 固化守护。每一步完成后
重新运行 PERF-11 场景并记录对比;不换技术栈,不一次性重写 App.tsx。
PERF-11 完整 App 交互基准
- 优先级 / 状态: P1 / 进行中
- 目标: 在当前生产构建、隔离 profile 和本地假模型下,可重复测量用户能感知的卡顿。
- 运行:
npm run build:bundle后执行npm run perf:app;结果写入GB_PERF_OUTPUT或系统临时目录,不含用户内容,不发出外部模型请求。 - 场景: 空闲基线;Composer 连续输入;切换会话;打开长会话;大量会话列表下的流式 回答;流式回答期间同时输入。
- 指标: Renderer Long Tasks(>50 ms)数量与总时长、按键到下一帧延迟、帧间隔 p95 与
掉帧比例、React commit 无法直接取得时以 DOM mutation 批次近似、Main event-loop 延迟
(
monitorEventLoopDelay)、Main 与 Renderer 进程 CPU 和内存、Renderer JS heap。 - 验收: 同一设备多次运行结果可比较;报告记录源码版本、环境、数据规模和原始样本摘要。
实现
build/run-app-perf.cjs:独立 supervisor。在127.0.0.1:11434(新 profile 默认模型地址) 启动 OpenAI 兼容假模型,清除ELECTRON_RUN_AS_NODE和模型环境变量,设置整轮期限(默认 600 s),汇总报告并删除隔离 profile。端口被占用时直接失败。tests/support/app-perf-driver.mjs:在导入out/main/index.js前设置appPath、userData、sessionData和日志目录,然后由生产 Main 创建窗口、注册 IPC 并加载真实 Preload/Renderer。全部交互使用sendInputEvent原生输入;数据通过生产conversations.saveLocalIPC 写入,再 reload 走正常启动加载。- Renderer 探针只观察:
longtask、long-animation-frame、Event Timing、rAF 帧间隔、 keydown 到下一帧、MutationObserver 计数、DOM 节点数和精确 JS heap。Main 用monitorEventLoopDelay,进程用app.getAppMetrics()。 - 默认规模:300 个会话 × 20 条消息,另加一个 2,000 条消息的长会话;输入 120 个字符,
按键间隔 60 ms;流式回答约 40 KB 混合 Markdown(标题、列表、代码、表格、行内/块级公式、
引用),每个 SSE 事件 12 个字符,2,000 字符/秒。可用
GB_PERF_*环境变量调整, 见脚本开头。 GB_PERF_PROFILE=1为有历史输入和流式场景采集 Renderer CPU profile 并汇总自身耗时 热点;剖析有开销,只用于归因,不作为基线数据。GB_PERF_CPU_THROTTLE=<倍数>用 CDP 降低 Renderer CPU 速度模拟低端设备;GB_PERF_MEMORY_VISITS(默认 24,0 跳过)控制PERF-09内存场景。
2026-10-02 基线
源码 8280158(工作区含其他未提交改动),Windows x64,Intel Core Ultra X7 358H ×16,
32 GB,Electron 43.2.0 / Chromium 150,DPR 1,内容区 1280×800,GPU 合成与光栅化均启用。
连续运行 3 次,假模型请求每次 2 次,外部模型请求 0 次。帧间隔基线为 8.3 ms(120 Hz)。
| 场景 | Long Tasks 次数 / 总时长 | 帧间隔 p95 | 掉帧比例 | 交互延迟 p50 / p95 | Main lag 最大 |
|---|---|---|---|---|---|
| 空闲(新 profile / 有历史) | 0 / 0 | 8.5 ms | 0% | — | 17–21 ms |
| 输入 120 字(新 profile) | 0 / 0 | 8.5 ms | ≤0.2% | 按键→帧 4.8–5.1 / 8.3–8.5 ms | ≤17 ms |
| 输入 120 字(有历史) | 0 / 0 | 8.5 ms | 0% | 按键→帧 6.5–6.7 / 8.4–8.7 ms | ≤18 ms |
| 首次切换会话 ×15 | 1 / 77–81 ms | 8.6 ms | 3.7–4.5% | 点击→显示 49–50 / 102–105 ms | 29–32 ms |
| 再次切换(缓存命中)×10 | 0 / 0 | 8.6 ms | 3.3–4.1% | 点击→显示 52–53 / 56–59 ms | 19–22 ms |
| 打开 2,000 条消息会话 | 1 / 94–102 ms | 8.5 ms | 1.6% | 点击→显示 134–144 ms | ≤21 ms |
| 滚动长会话 3 s | 0 / 0 | 8.5 ms | 0% | — | ≤17 ms |
| 流式 40 KB 回答 | 126–137 / 8.6–9.3 s | 83 ms | 45–48% | — | 30–36 ms |
| 流式期间输入 100 字 | 128–182 / 8.8–11.9 s | 83 ms | 50–52% | 按键→帧 12.5–13.5 / 35–36 ms | 29–144 ms |
其他观察:
- 启动:窗口创建约 0.58 s,
ready-to-show约 1.29 s,Composer 可用约 1.42 s;带 300 个 会话 reload 到侧栏出现约 0.19 s。 - 流式回答约 20 s,Long Tasks 占记录时间约 40%;按四等分统计为
0 / 2.5 / 4.7 / 4.6 s, 随回答变长而增加,说明每次提交的成本与已生成长度成正比。 - 有历史时侧栏一次渲染全部 302 行,DOM 约 4,400 节点;访问过若干会话后约 1.6 万;两次 流式后约 4 万,JS heap 最高约 210 MB(keep-alive 缓存持续增长)。
- 长会话只渲染最近 80 条,打开耗时主要是一次约 100 ms 的 Long Task。
- KaTeX 的
data:字体被 CSP 拒绝(缺少font-src),每轮 8 条控制台错误,字体回退 (已在PERF-12第一步修复)。 app.getAppMetrics()的 CPU 百分比在 Windows 上按全部逻辑核归一,只作相对比较。
归因(剖析运行): 流式期间 Renderer 自身 CPU 约 74% 在应用 bundle 中,热点是
hast-util-from-dom 的 parseFragment/fromDom(rehype-katex 把 KaTeX HTML 再解析成
hast)、micromark 分词、hast→React 转换和 React reconcile。对照实验确认根因:默认语料的
块级公式写在一行($$...$$),命中 MarkdownRenderer.tsx 的 unsafeSegmentLine,
splitMarkdownSegments 退回整篇单段,每次 flush 都重新解析整篇回答并重新渲染全部公式。
仅把 $$ 改成独占一行(GB_PERF_CORPUS=multiline-math)后,同一流式场景 Long Tasks
从约 130 次 / 9 s 降到 1 次 / 0.15 s,帧间隔 p95 从 83 ms 回到 8.5 ms,流式期间按键→帧
p95 从约 36 ms 降到 15.5 ms。
结论与对后续批次的影响:
- 当前最严重、用户最易感知的卡顿是流式长回答,且由 Markdown 分段失效后的整篇重解析
主导;输入、切换、滚动和空闲在本机均未出现明显问题。
PERF-12应优先处理:让分段在 单行$$、HTML 行、链接定义等情况下也能安全工作或局部回退,并对已完成段缓存渲染结果。 - 假模型 2,000 字符/秒高于常见真实模型输出速度,但成本随长度线性增长,真实速度下回答 足够长时同样会出现。
- 会话首次切换 p95 约 100 ms、打开长会话约 140 ms,可在
PERF-13/PERF-14中改善, 优先级低于第 1 项。 - 本机为高性能设备;低端设备上的绝对值会更差,后续应补一台低配 Windows 设备基线。
PERF-12 流式与输入快速止血
- 优先级 / 状态: P1 / 进行中(Markdown 分段已完成)
- 范围: 流式 delta 在 Renderer 按
requestAnimationFrame合并后提交,并以 transition 降低优先级;缓存 Markdown 预处理结果;RightAssistantSidebar、ProjectSwitcher和 keep-alive 路由使用 memo 并稳定回调;滚动跟随只在需要时执行。 - 验收:
PERF-11流式与输入场景的 Long Tasks 和输入延迟下降;事件顺序、停止、重试、 工具与 Subagent 块不回退。该项吸收PERF-04的范围。
2026-10-02 第一步:Markdown 分段与 KaTeX 字体
- 分段(
MarkdownRenderer.tsx):splitMarkdownSegments按 micromark-extension-math 的实际规则识别块级公式:$$等两个以上$开头、其后不含$的行才开启公式块,并需要 不短于开启长度的$行关闭;$$x^2$$这类单行公式按普通段落处理,不再使整篇退回单段。 链接定义和脚注改为逐行检查,位于顶格代码围栏内的行视为字面内容。原始 HTML 行、\r、 缩进围栏内的定义仍整篇退回。 - 字体(
electron.vite.config.ts): Renderer 构建不再把字体内联为data:URL。CSP 没有font-src,原先 4 个小于 4 KB 的 KaTeX 字体被拒绝并回退;现在 60 个 KaTeX 字体 全部作为同源文件输出,CSP 不变。 - 等价性验证: 现有逐前缀对照语料新增单行公式、长
$围栏、代码围栏内的定义等用例; 新增 1,200 份确定性随机文档(半数取随机前缀)对照,要求分段渲染与整篇渲染的 DOM 完全 一致。开发期另跑了约 4.8 万份随机文档与前缀,其中约 6,300 份走分段路径,全部一致。 npm run perf:app对比(同机,各 3 次,80579ee+ 本改动):
| 场景 | 改动前 | 改动后 |
|---|---|---|
| 流式 40 KB:Long Tasks | 126–137 次 / 8.6–9.3 s | 1 次 / 0.16 s |
| 流式 40 KB:帧间隔 p95 / 掉帧 | 83 ms / 45–48% | 8.6–8.9 ms / 3–5% |
| 流式期间输入:按键→帧 p50 / p95 | 12.5–13.5 / 35–36 ms | 11.7–11.9 / 19–21 ms |
| 流式期间输入:掉帧 | 50–52% | 7.7–8.5% |
| KaTeX 字体 CSP 错误 | 每轮 8 条 | 0 |
输入、切换、长会话和滚动场景与基线持平。流式期间输入仍有约 8% 掉帧、按键 p95 约 20 ms,
剩余开销属于 App 级重渲染,留给 PERF-12 后续项和 PERF-13。
- 剩余: 流式 delta 按帧合并、大面板 memo、滚动跟随优化仍待实施。
2026-10-02 低端设备模拟(Renderer CPU 4× 降速)
GB_PERF_CPU_THROTTLE=4 npm run perf:app 通过 CDP Emulation.setCPUThrottlingRate 把
Renderer 主线程降速 4 倍(Main 不降速,每次 reload 后重新应用),源码 5dc6545
(含 PERF-12 第一步),同一设备。不降速一栏为同一轮构建的对照。
| 场景 | 不降速 | 4× 降速 |
|---|---|---|
| 输入 120 字(新 profile)按键→帧 p95 | 8.7 ms | 42 ms,掉帧 21% |
| 输入 120 字(有 300 个会话) | 0 Long Task,p95 9 ms | 37 次 / 3.2 s,p95 355 ms,最大 483 ms |
| 首次切换会话 ×15 点击→显示 p50 / p95 | 52 / 117 ms | 434 / 922 ms |
| 再次切换(缓存命中)p50 / p95 | 55 / 66 ms | 482 / 534 ms |
| 打开 2,000 条消息会话 | 140 ms | 1.2 s |
| 滚动长会话 | 帧 p95 8.5 ms | 帧 p95 17 ms,正常 |
| 流式 40 KB | 1 次 / 0.17 s,帧 p95 17 ms | 156 次 / 16.7 s,帧 p95 183 ms,掉帧 100% |
| 流式期间输入 按键→帧 p50 / p95 | 11.5 / 17 ms | 575 / 1,513 ms |
高性能机上不可见的问题,在降速后全部暴露:
- 新 profile 输入只到 42 ms,有 300 个会话后升到 355 ms,差异来自每次按键重新渲染整个
App 和完整侧栏。降速剖析:
resizeComposerTextarea(每次输入同步读scrollHeight强制 布局)约 8%;i18nextformatLanguageCode/translate约 12%(Intl.getCanonicalLocales每次调用t()都执行,侧栏 302 行各自多次调用);其余是 App 与侧栏行的 React 渲染。 - 流式期间约 18% 是
ChatHistoryPane每次 flush 的滚动跟随 layout effect(读布局后scrollTo),i18next 约 8%,Markdown 解析与 React reconcile 其余。 - 缓存命中切换也约 0.5 s,说明慢的不是挂载新面板,而是切换触发的 App 级整体重渲染。
结论: 低端设备上输入、切换和流式都会明显卡顿,主因是 App 级重渲染和侧栏全量渲染,
证实 PERF-13(领域 store)和 PERF-14(侧栏虚拟化)有必要。PERF-12 剩余项应先做三处
低成本热点:Composer 高度改为只在需要时测量、t() 结果或 i18next 语言码规范化做缓存、
滚动跟随按帧合并且只在贴底时执行。
2026-10-02 第二步:三处低成本热点
- Composer 高度(
App.tsxresizeComposerTextarea): 记住上次的值与高度。值未变不测量; 在原文本末尾追加时不再先设height: auto(只会变高),已到最大高度直接跳过测量;删除、 替换、清空仍按原方式重置并测量。 - i18next 语言码(
i18n/index.ts): i18next 25 每次t()都经toResolveHierarchy→formatLanguageCode调用Intl.getCanonicalLocales。初始化后对languageUtils.formatLanguageCode加按输入缓存(结果只取决于代码与静态初始化选项,上限 64 项),翻译结果不变。 - 滚动跟随(
ChatHistoryPane.tsx): 激活面板时仍同步滚到底;之后内容更新时改为每帧最多 一次requestAnimationFrame中读取scrollHeight并scrollTo,回调里再次确认仍贴底; 卸载时取消。读者离开底部时不滚动,恢复阅读位置逻辑不变。 - 测试: 新增“流式三次更新只滚动一帧”(旧实现 3 次,失败)、Composer 追加与删除的测量
次数、
t()不再调用Intl.getCanonicalLocales。全量测试 5,621 通过,2 失败与本改动 无关(真实 OpenCode 集成测试因免费额度返回 403;Supervisor 超时设置用例此前已失败)。 npm run perf:appA/B(同机,改动前为临时 worktree 中的983cdd4构建,交替运行):
| 场景(4× 降速,3 轮) | 改动前 按键/点击 p50 / p95 | 改动后 |
|---|---|---|
| 输入 120 字(新 profile) | 28–80 / 33–361 ms | 19–36 / 24–57 ms |
| 输入 120 字(有 300 个会话) | 265–2,372 / 1,486–4,266 ms | 27–161 / 32–346 ms |
| 首次切换会话 | 442–761 / 1,063–1,384 ms | 360–398 / 820–1,334 ms |
| 再次切换(缓存命中) | 472–749 / 501–1,143 ms | 390–420 / 456–951 ms |
| 流式 40 KB:Long Tasks 总时长 | 15.9–20.6 s | 15.1–17.4 s |
| 流式期间输入 | 432–6,778 / 1,146–9,634 ms | 96–466 / 297–1,567 ms |
不降速时(2 轮)两版各场景持平,输入 p95 均约 9 ms,流式 1 个 Long Task。4× 降速下
方差很大(同版本轮间可差数倍),应看量级:有历史时输入从秒级回到百毫秒内,切换略有改善,
流式本身仍是每帧卡顿——剩余成本是 App 级重渲染与 Markdown/React 提交,需 PERF-13。
- 剩余: 流式 delta 按帧合并后以 transition 提交、大面板 memo 仍待实施;之后进入
PERF-13。
PERF-13 Renderer 领域 Store 迁移
- 优先级 / 状态: P1 / 进行中(流式缓冲、Composer 草稿已迁移)
- 顺序: 流式缓冲 → Composer 草稿 → 会话与消息 → 任务与通知 → 设置及其余界面状态。
- 每个领域的完成步骤: 建立 store 并把 IPC 订阅移入 store;调用方改为 selector;删除
App 中对应 state/ref 同步代码;运行测试与
PERF-11。 - 验收: 输入和流式更新不再重新渲染 App 根组件;
App.tsx最终降到布局壳规模。
2026-10-02 第一步:流式缓冲与 Composer 草稿
- 流式缓冲(
live-message-store.ts):text/reasoningdelta 不再逐条setConversations, 而是写入按消息分组的外部 store;ChatMessageRow用useSyncExternalStore订阅自己的消息, 只有这一行重新渲染。App 状态每 250 ms 吸收一次,或在任何其他会话更新时先吸收。- 每个 delta 带序号和应用后的正文/思考长度。store 生成的消息对象记录已包含的序号;持久化 快照等外来��象按长度判断(delta 只追加),因此不会重复或丢失。
- 一次更新只吸收派发时已到达的 delta,工具、审批等块与文本保持原有顺序。
- 吸收后裁剪已提交的 delta,行内显示增量折叠,长回答期间缓冲区保持很短。
- 吸收用普通更新而非 transition:transition 会被行的同步更新反复打断,导致缓冲区一直增长 (首版实测出现过流式 Long Tasks 翻倍)。
- Composer 草稿(
composer-draft-store.ts): 按会话的草稿移出 App state;输入框和发送按钮 分别订阅草稿文本和“是否有内容”,按键不再重渲染 App。高度调整改为草稿提交后的 layout effect。 - 测试: store 单测覆盖折叠等价、提交前显示、工具块顺序、派发时快照、替换快照不重复、 React 重放幂等、无基线不隐藏、长流裁剪;App 测试改为断言流式文本不再渲染历史面板与任务条。
npm run perf:appA/B(同机,改动前为临时 worktree 中的90ea33e构建,交替运行):
| 场景(4× 降速,3 轮) | 改动前 p50 / p95 | 改动后 |
|---|---|---|
| 输入 120 字(新 profile) | 18–21 / 24–26 ms,掉帧 7–13% | 9–10 / 11–16 ms,掉帧 0–1% |
| 输入 120 字(有 300 个会话) | 27–81 / 35–227 ms | 10–19 / 12–36 ms |
| 流式 40 KB:Long Tasks 总时长 | 6.5–20.8 s | 3.6–19.3 s(3 轮中 2 轮约 4–5 s) |
| 流式期间输入 | 73–1,808 / 174–3,498 ms | 34–62 / 59–573 ms |
| 打开 2,000 条消息会话(见下方更正) | 1.07–1.80 s,帧 p95 9–42 ms | 0.47–0.76 s,帧 p95 75–1,008 ms |
| 首次 / 缓存命中切换会话 | 持平 | 持平 |
更正(归因): “改动后”构建包含了当时工作区中另一项未提交的改动——新会话面板分两步
渲染(先最后 20 条,首帧后补齐 80 条,后提交为 09c415b)。打开长会话的耗时下降主要来自
它,而不是流式缓冲;同时补齐那一帧变成长帧,4× 降速下该场景帧间隔 p95 从 9–42 ms 升到
75–1,008 ms。输入与流式期间输入的改善来自本步骤。分两步渲染只是过渡,补齐帧的成本需由
PERF-14 虚拟化解决。
不降速时(2 轮)各场景持平或略好,流式期间输入 p95 从 15–17 ms 降到 11–12 ms。流式本身在 4× 降速下仍每帧掉帧:剩余成本是行内 Markdown 解析与 KaTeX 渲染,以及每 250 ms 一次的 App 级吸收;切换会话的 App 级重渲染未改善,留给后续领域迁移。
- 远程 Runtime: 远程事件与本地事件走同一
agent:event通道和同一 Renderer 处理函数, 上述 Renderer 改进同样生效。远程事件在 Main 中逐条同步写 SQLite(每个 delta 约 2 个写事务, 会话恢复路径还会重写整条消息)后再转发,这部分未改,归入PERF-15/PERF-16。 - 基准工具:
GB_PERF_MODEL_PORT可把假模型移到其他端口(例如本机已运行 GoodBuddy 占用 11434 时),隔离 profile 通过GOODBUDDY_MODEL_*环境变量指向它。
2026-10-02 其他改动的架构核对
对同期其他功能改动按本批次 5 条规则核对(监督者功能除外):
- 一致: 剪贴板改为异步读写(
d773f5e);未保存修改通过 context 上报、记录不进 App state, 有未保存修改的路由不被缓存淘汰(43be465);新会话面板分两步渲染(09c415b,过渡方案)。 - 偏离规则 1: Story Graph 检索(
e97263c)仍在 Main 内经DatabaseSync读取后,在 JS 中 逐条JSON.stringify并按多个关键词匹配,单次成本高于原整句匹配;工具描述又鼓励 Agent 主动调用,调用次数会增加。纳入PERF-15/PERF-16,在此之前补测检索耗时并限制扫描规模。 - 偏离规则 2: 侧栏 5 秒重排(
ee4eb73,use-conversation-list-order.ts)由 App 持有状态, 渲染期间 setState,定时器每 5 秒检查;顺序不变时跳过,当前成本低。交互上悬停、滚动、菜单 打开时不重排。侧栏迁移和虚拟化(PERF-13/PERF-14)时移入侧栏组件。 - React 规则: 缓存会话保持滚动位置(
bf6c4a9)在渲染期间写 ref,即当前 lint 的 3 条react-hooks/refs错误;启用 React Compiler 前需改为由状态派生顺序。
2026-10-02 并行修复:补全分帧、远程批量写库、架构守护
- 补全分帧(
ChatHistoryPane.tsx): 新面板补全改为每帧 +20 条(20→40→60→80),每步后 贴底或按高度差保持阅读位置;“加载更早”、批次变化或笔记跳转到达时立即渲染完整批次。 - 渲染期 ref(
App.tsx、pane-order.ts): 会话面板顺序改为 state,用纯函数reconcilePaneOrder在渲染期按需调整,顺序不变时返回原数组;消除 3 条react-hooks/refs。 - 远程批量写库(
remote-event-batcher.ts、assistant-database.ts、ipc.ts): 远程语义事件 按 checkpoint 一个事务提交(Agent 在 checkpoint 之后才可能 ACK),可恢复会话每批只读写一次 消息;非远程事件、question、done/error、显式 flush、256 条或 50 ms 计时器也会先提交。 只转发新写入的事件,顺序不变;提交失败整批回滚且之后拒绝继续提交。交互、后台/定时/通道 和重启恢复三条路径都已改用。当前 checkpoint 粒度是每条 transcript 记录而非每页,按页批量 需要改acp-remote-runtime.ts。 - 架构守护(提前实施
PERF-17的一部分):eslint.config.js新增只减不增的白名单规则:DatabaseSync(19 个文件)、src/main同步 fs(8 个文件)、组件直接订阅window.goodbuddy.*.on*(6 个文件)、App.tsxmax-lines11700;文档演示和 vendor 脚本 不再参与 lint,npx eslint .现为 0 错误。build/perf-thresholds.json+npm run perf:check(或GB_PERF_CHECK=1 npm run perf:app)检查不降速报告的阈值。 npm run perf:appA/B(改动前为9d8477b): 不降速 2 轮两版均通过 60 项阈值;打开长会话 从 1 个 Long Task / 56–61 ms、最大帧 50–58 ms 降到 0 个、最大帧 25–33 ms,其余持平。4× 降速 3 轮:打开长会话最大帧从 475–567 ms 降到 225–283 ms(帧 p95 因多出几帧小提交升到 175–258 ms);切换会话持平;流式场景两版方差都很大,未见一致变化。远程批量写库不在该基准 覆盖范围内,由单元测试和ipc.test.ts验证事务次数、顺序与回放去重。
2026-10-03 会话与消息领域(第一片)
- 会话 store(
conversation-store.ts、conversation-selectors.ts): 会话数组移出 App state,getState()同步可读,取代conversationsRef及其 5 处直接写入;set()先吸收流式 delta 再同步更新。按会话 id 和列表两级通知,选择结果未变时保持同一对象。 - 侧栏(
ConversationSidebar.tsx): 搜索匹配、5 秒重排、行级派生值移入侧栏自身订阅; 切换会话时侧栏只重渲染状态翻转的两行。顺便修复 React 19.2 中memo组件内useEffectEvent不刷新导致重排用旧列表的问题(有回归测试)。 - App 只订阅: 活动会话、缓存面板的会话、数量,以及标题/审批/项目活动的按值比较
选择器。
App.tsx从 11,636 行降到约 10,960 行。 - 仍在 App: 未读、展开、菜单、重命名、删除等界面 ID;持久化、历史加载与合并; 活动会话的更新(含 250 ms 流式吸收)仍重渲染 App。
2026-10-03 PERF-13 进展:Composer、路由边界、任务 store、切换会话
提交 41be9f8、2e3d2e7、651475d、492f1d2、6b634c5。
- Composer(
Composer.tsx,memo): 输入区页脚(约 930 行 JSX、23 个内联回调)移出 App。 草稿读composer-draft-store,会话字段读useComposerConversationView(不含消息数和结果 ID), 弹出菜单状态放进composer-menu-store.ts;动作经use-composer-actions.ts保持同一引用。 其他会话更新、当前会话新增消息、别的会话输入都不重渲染 Composer(有测试)。 - 路由边界:
KeepAliveRoute改为memo,隐藏路由在 App 重渲染时保持上次结果,再次显示时 补一次渲染。知识库、设置、监督、笔记、右侧栏、项目切换器改为memo,知识库 26 个回调移到use-knowledge-workspace-actions.ts,其余用useStableHandlers;笔记面板元素useMemo。 - 任务与结果(
task-store.ts/task-selectors.ts/task-sync.ts):assistantTasks、assistantArtifacts移出 App state,assistantTasksRef删除。App 只选取顶层任务状态、活动摘要 所需字段和当前会话是否忙碌,子代理进度不再重渲染 App;结果刷新时内容相同的对象保留引用, 会话面板自己读结果表。agent-event-handler.ts只改了任务/结果调用。队列状态未迁移:它和发送、 恢复、中断流程交织,单独拆风险高于收益。 - 切换会话: 面板顺序改为渲染期派生(
usePaneOrder),Composer 弹窗按上下文键关闭, 不再在渲染中 setState,切换一次只渲染 App 一次(测试在旧代码上为 2 次)。标题表在标题不变时 保持同一 Map。 - 规模:
App.tsx9,576 → 7,904 行(含期间合入的监督者改动);App 内 useState 120→114、 useEffect 62→56、useRef 79→73、useCallback 89→83。lint 上限 9,600 → 8,000。 npm run perf:appA/B(改动前为b28ddd4临时 worktree 构建,与6b634c5交替各 3 轮, 4× 降速,取中位数,括号为 3 轮范围):
| 场景 | 改动前 | 改动后 |
|---|---|---|
| 首次切换会话 延迟 p95 | 525 ms(520–674) | 489 ms(470–607) |
| 首次切换 Long Tasks 数 / 总时长 | 37 / 3.9 s | 24 / 2.0 s |
| 缓存切换(warm)延迟 p95 | 254 ms(208–256) | 139 ms(135–149) |
| 缓存切换 Long Tasks 数 / 总时长 | 21 / 1.7 s | 12 / 0.7 s |
| 热切换(hot)延迟 p95 | 139 ms | 94 ms(77–154) |
| 流式 40 KB:Long Tasks 数 / 总时长 / 帧 p95 | 90 / 7.0 s / 208 ms | 49 / 3.1 s / 108 ms |
| 流式期间输入:延迟 p95 / Long Tasks 总时长 / 帧 p95 | 110 ms / 8.3 s / 233 ms | 53 ms / 3.5 s / 100 ms |
| 输入(有 300 个会话)延迟 p95 | 11.7 ms | 14.1 ms(12.6–15.9,噪声范围内) |
流式两个场景的掉帧率仍接近 100%(4× 降速下每帧仍有 Markdown 解析和 250 ms 吸收),但 Long
Tasks 减半。不降速一轮 GB_PERF_CHECK=1:60 项阈值全部通过。
PERF-14 长列表虚拟化
2026-10-03 消息时间线窗口化
- 实现(
chat-message-window.ts、ChatHistoryPane.tsx、ChatTimeline.tsx): 自研窗口化, 未引入依赖。只挂载视口附近的行(上限 60 行,外加获得焦点的行),其余用按实测高度或 平均高度估算的占位块代替;行高按会话缓存(最多 24 个会话)。以视口内的一行为锚点, 行高变化或挂载/卸载时修正滚动位置,读者看到的内容不动。 - 保留: 96 px 贴底阈值和流式时每帧跟随;发送消息回到底部;“加载更早”仍按 80 条扩展
可滚动范围;笔记跳转先挂载目标再滚动;隐藏/显示和重新挂载恢复位置;隐藏面板不测量。
分帧补全(
09c415b/855b941)已删除,打开面板一次提交有界行数。 - 滚动滞后窗口: 视口离开已挂载范围 160 px 内才重新取窗,取窗时挂载 1.5 屏余量,避免 每个滚动事件都重挂行。
- 毛玻璃锚点: 锚点以
.chat-content的上内边距为基准,切换毛玻璃时阅读位置不变 (由tests/frosted-glass.electron.test.ts在真实 Electron 中验证,该测试改为按消息 id 跟踪 锚点行)。 - 已知限制: 页内查找搜不到未挂载的消息;快速甩动时可能短暂看到空白占位;单条超长 消息仍整行挂载;估算高度会让滚动条滑块在首次经过时轻微变化。
PERF-15/16 第一片:热点读路径移出 Main
2026-10-03 只读查询 worker
- 实现(
readonly-query-reader.ts、readonly-query-worker.ts): 每个数据库文件一个只读 worker 线程,沿用execution-stats-reader的模式,复用KnowledgeDatabase/AssistantDatabase的生产方法,结果与同步路径同源。最多 32 个并发请求;worker 崩溃或 启动失败时转为同步执行,30 秒后再重启;:memory:始终同步。AbortSignal通过共享标志 取消(向量扫描每 256 行检查)。没有新增DatabaseSync白名单。 - 已移出 Main: 知识库检索(
retrieve、searchHybrid);Story Graph 读取(Agent 通过 MCP 调用,先在 SQL 中按范围预筛,超过 5 万个对象返回scan_limit_exceeded);会话搜索(一条 SQL + 注册函数,保持toLocaleLowerCase().includes()语义)。会话列表的 N+1 查询改为两次 批量查询,IPC 结构不变。 npm run perf:knowledge(改用 Electron 运行):
| 文本块 | 混合检索时 Main 最大事件循环延迟(同步 → worker) | 单次耗时(同步 / worker) |
|---|---|---|
| 5,000 | 54.9 → 19.5 ms | 47.7 / 53.3 ms |
| 20,000 | 204.3 → 19.6 ms | 188.6 / 198.9 ms |
| 50,000 | 489.9 → 18.6 ms | 466.8 / 478.6 ms |
- 剩余: 向量检索仍是全表扫描(只是不在 Main 上);写入和其余约 530 处同步调用仍在
Main;独立数据进程和
DatabaseSync白名单缩减未开始。
2026-10-03 三项合并后的 npm run perf:app A/B(改动前为 53b9ecb)
不降速:新版本通过全部 60 项阈值。4× 降速(改动前 3 轮,改动后 3 轮,滚动场景另取调整后 2 轮):
| 场景 | 改动前 | 改动后 |
|---|---|---|
| 首次切换会话 p50 / p95 | 335–377 / 677–730 ms | 198–312 / 518–634 ms |
| 切回刚看过的会话 p95 | 379–386 ms | 179–240 ms |
| 切回最近的会话 p95 | 161–164 ms | 89–140 ms |
| 打开 2,000 条消息会话 | 448–484 ms,帧 p95 200–225 ms | 290–354 ms,帧 p95 8–9 ms |
| 滚动长会话:帧 p95 / 掉帧 | 17 ms / 7% | 58–67 ms / 15–16% |
| 流式输出、流式时打字 | 两版波动都很大,未见一致变化 | 同左 |
| 打字、空闲 | 持平 | 持平 |
滚动是唯一变差的场景:改动前长会话一次性挂载所有已加载行,滚动只是移动画面;窗口化后
每隔几屏要挂载新行(含 Markdown 渲染)并测量。初版每个滚动事件都重新取窗,掉帧约 70%;
加入滞后窗口后降到 15–16%。剩余开销主要在新行渲染和 getBoundingClientRect 测量,后续
可把锚点测量合并到每帧一次,或给 Markdown 结果加缓存。
2026-10-03 第二轮并行:滚动、Markdown 缓存、侧栏窗口化、App 继续瘦身、活动记录
- 滚动(
ChatHistoryPane.tsx、chat-message-window.ts): 滚动事件只读scrollTop等 滚动容器指标;是否重新取窗改为每帧最多判断一次,并用“锚点行 + 缓存行高”推算视口位置, 稳定滚动时不读元素几何。提交后只有挂载集合或占位高度变化时才校正锚点;上内边距缓存, 在 resize、激活和重新取锚点时刷新(真实 Electron 毛玻璃测试曾因缓存旧边距偏 105 px,已修)。 - Markdown 渲染缓存(
markdown-render-cache.ts): 按源文本和管线配置缓存解析后的 hast 树(LRU),重新挂载的行跳过 micromark/remark/rehype/KaTeX,只做 hast → React。 - 侧栏窗口化(
use-conversation-list-window.ts): 60 个会话以上只挂载视口附近的行, 活动会话、菜单、重命名、获得焦点的行保持挂载;aria-setsize/aria-posinset保留总数。 - App 继续瘦身: 会话持久化与历史加载(
conversation-persistence.ts)、刷新调度 (conversation-refresh.ts)、Agent 事件处理(agent-event-handler.ts)、历史面板 (ConversationHistorySlot.tsx)移出 App。App.tsx从约 10,960 行降到 9,566 行,lint 上限 改为 9,600。 - 活动记录(
activity-history-plan.ts): Main 的整份替换只校验、序列化变化的记录; 新增增量 IPCactivity-history:update。Renderer 还没有改用增量接口。 - 会话读取:
conversations:list-summaries和conversations:get也改由只读 worker 执行。 npm run perf:app驱动: 侧栏窗口化后不再一次挂载全部会话,驱动改为按aria-setsize判断加载完成,点击会话前逐屏滚动侧栏直到该行挂载。
npm run perf:main(1,000 个会话 / 2 万条消息 / 5,000 条活动记录,Main 事件循环最大延迟):
| 路径 | 改动前 | 改动后 |
|---|---|---|
| 会话摘要列表 | 25.4 ms | 0.8 ms(单次耗时 31 ms,在 worker 上) |
| 活动记录整份替换,无变化 | 13.8 ms | 9.3 ms |
| 活动记录增量更新 1 条 | — | 5.3 ms |
npm run perf:app,4× 降速,改动前(169941c)与改动后各 3 轮:
| 场景 | 改动前 | 改动后 |
|---|---|---|
| 滚动长会话:帧 p95 / 掉帧 | 25–50 ms / 10–11% | 17–41 ms / 9–10% |
| 打开 2,000 条消息会话:长任务 / 点击 p95 | 165–170 / 238–247 ms | 97–105 / 211–218 ms |
| 首次切换会话:长任务 / 点击 p95 | 1,969–2,115 / 444–460 ms | 1,256–1,341 / 425–435 ms |
| 切回刚看过的会话:长任务 / 点击 p95 | 1,084–1,183 / 151–158 ms | 152–205 / 110–146 ms |
| 流式输出 40 KB:长任务总时长 / 掉帧 | 1.6–2.4 s / 90–99% | 0.33–0.41 s / 63–72% |
| 流式时打字:长任务 / 按键 p95 | 3.0–3.6 s / 41–48 ms | 0.53–0.64 s / 29–43 ms |
| 有历史时打字:DOM 节点 | 4,382 | 705–719 |
不降速:全部阈值通过;流式输出和流式时打字的长任务均为 0。上一轮滚动的退化已消除(本轮基线
已含窗口化,故与 53b9ecb 的 17 ms / 7% 不完全可比,帧 p95 在两版中都波动较大)。
2026-10-03 本期收尾:已做、暂缓与理由
本期收尾只做低风险改动:
- 侧栏 id 列表保持同一数组(
ConversationSidebar.tsx):流式更新时,会话对象每次都会变, 原来每次都生成新的 id 数组,导致侧栏窗口化的布局 effect 重跑并读取scrollTop。4× 降速剖析 显示,这段在流式场景中占 562 ms self time。现在 id 不变时,数组也保持不变。 - 会话标题跑马灯不在挂载时强制布局(
OverflowMarquee.tsx):挂载时不再同步读取scrollWidth,改由 ResizeObserver 在首次报告时测量,有回归测试。悬停时的测量方式不变。 - 4× 降速下复测,这两项的变化都在波动范围内(同版本多次运行,差距可达 2 倍),没有出现退化; 不降速时 60 项阈值全部通过。
本期暂缓,理由如下:
| 项目 | 理由 |
|---|---|
Renderer 改用 activity-history:update |
需要在 App 中跟踪每次变化并处理顺序与失败重试;Main 端已经只处理变化的记录,Main 卡顿从 13.8 ms 降到 9.3 ms,剩余收益小,而出错会丢活动记录 |
| 数据库写入异步化、独立数据进程(PERF-15/16 剩余部分) | 涉及约 530 处调用和写入顺序语义,属于高风险大改动;本期已把最重的读取路径(检索、会话列表、搜索)移出 Main,需要单独立项 |
| 首次切换会话(4× 降速 p95 约 430 ms) | 剖析显示,开销分散在 React 渲染、首次 Markdown 解析和 i18n 上,没有单一热点;不降速时 p95 约 70 ms,已在阈值内 |
| 流式输出 Markdown 放进 worker | 需要重写渲染管线,并处理流式中途结果和公式;不降速时流式场景已无长任务 |
| 页内查找覆盖未挂载的消息 | 当前 App 没有页内查找功能(未调用 findInPage),属于新功能,不是回归 |
| 性能阈值接入 CI | 仓库目前没有 CI 工作流;npm run perf:check 可以在本地运行 |
7.2 按性能原则补齐差距(2026-10-03,第三轮并行)
统一标准见 docs/architecture/performance-principles.md(P1–P10、性能预算和评审清单)。本轮按其中“当前差距”并行推进:
| 原则 | 改动 | 测量结果 |
|---|---|---|
| P1 | 文档解析、PPTX 提取、分块、大文件(≥ 1 MiB)哈希移到 document-parse-worker 线程,FIFO 队列;worker 不可用或崩溃时退回原来的 Main 内联执行,30 秒后重试;中止正在执行的任务会终止 worker。数据库写入仍由调用方按原顺序执行 |
npm run perf:document-parse,各 3 轮:Main 最长阻塞 large.pdf 498–679 → 16–17 ms,large.docx 313–335 → 17–20 ms;总耗时基本不变 |
| P3 | 终端输出:Main 端按 8 ms 窗口合并(空闲时第一块立即发出,不增加回显延迟),单批不超过 64 KiB;控制事件(退出、错误、状态)发出前先清空缓冲,顺序不变;背压按“已发出 + 已缓冲”字节计算,暂停阈值留 1/4 余量。Renderer 端每帧最多写一次 xterm,写完后按最大序号累计 ACK;窗口不可见时 100 ms 定时兜底,保证 ACK 不中断 | 单元测试覆盖顺序、UTF-8 边界、背压、退出前清空;perf:app 未覆盖终端场景 |
| P5 | 笔记列表(按卡片行窗口化,CSS subgrid)、工作区文件树(展开后拍平,按缩进显示层级)、知识库文档表格只渲染可见部分;抽出通用 use-list-window / use-grid-columns,侧栏会话列表改用同一实现。选中项、打开菜单的行、焦点行保持挂载 |
每个列表新增窗口化单元测试;真实 Electron 文件树布局测试改为验证窗口化后的停靠、滚动和预览返回 |
| P6 | Main 启动打点(startup-marks.ts),新增 npm run perf:startup:首次运行 / 空配置 / 300 个会话三种配置,各 5 轮 |
见下表 |
| P9 | package.json 新增 perf:startup、perf:document-parse |
— |
冷启动基线(标准机,中位数 / p95,ms):
| 阶段 | 首次运行 | 空配置 | 300 个会话 |
|---|---|---|---|
| 启动到输入框可用 | 1284 / 1322 | 1024 / 1040 | 1028 / 1040 |
| Main 模块加载(index.js 求值) | 414 / 420 | 411 / 420 | 413 / 423 |
| 本地工具环境初始化 | 154 / 169 | 160 / 163 | 153 / 160 |
| 知识库初始化 | 240 / 249 | 19 / 22 | 32 / 42 |
| 助手数据库初始化(同步) | 205 / 206 | 4 / 4 | 14 / 15 |
| Renderer 导航到输入框出现 | 228 / 233 | 226 / 244 | 224 / 228 |
启动的主要开销是 Main 模块加载(约 415 ms)和本地工具环境初始化(约 155 ms);首次运行另有约 450 ms 的建库开销。这些只做了测量,没有改动,后续按 P6 拆分延迟加载。
本轮回归:perf:app 60 项阈值全部通过(不降速,流式和流式打字长任务均为 0);typecheck、全量 lint 通过;全量测试中 2 个失败分别为 OpenCode 免费额度 403 的集成用例和 magic-notes-* 真实 Electron 测试在整套并行负载下的偶发失败,后者单独运行和 6 个 Electron 布局测试并行连跑 3 次均通过。
7.2.1 活动记录增量与分页(2026-10-04,P2/P4/P5)
之前启动时整份读取活动记录(本机约 2.4 万条、10 MB),任何变化 250 ms 后把整份列表 replace 回 Main;Main 每次在列表前面插入记录都要重写顺序 JSON。分三次提交改完:
- 增量保存(
6aab78b)。 活动记录移出 App 状态,放进activity-store.ts。领域动作(新增、更新请求、审批、按请求删除、按调用删除、结束请求、对账、清空)修改状态,同时排入对应的契约变更;activity-sync.ts每 250 ms 用activityHistory.update发送(每次最多 2 万条),失败的变更按原顺序留在队列里,退避重试;退出和卸载前也会刷盘。清空改为新增的activityHistory.clear(),Main 在一个事务里完成。 - Main 分页、汇总、对账(
352da66)。 第 57 版 schema 重建activity_history_records:order_seq(越新越大,移到最前就分配新的最大值)加上从记录 JSON 生成的存储列(状态、类型、请求、会话、项目、创建时间)和索引。顺序从record_order_json原样迁移;旧数据里的重复 ID 保留,仍然只显示第一条。ActivityHistoryRepository负责全部 SQL:按索引倒序分页page,summary返回面板原来从整份列表算出的计数、会话标题和会话状态,reconcile用 SQL 关联任务表(追加的说明行由界面传入本地化文本),以及clear。分页和汇总走只读 worker。 - 界面只保留已加载的页(
bf9850a)。 启动顺序:发送排队的变更,Main 对账,再加载首页(200 条)。打开运行记录页时补足一批(500 条,和以前首屏一样多),“继续显示”先显示下一批,再在后台加载下一页;切换筛选会重新取首页;离开页面只留一批。计数、分组、标题、会话状态和清空数量都来自 Main 的汇总。没加载的记录也照样增量更新:update新增按内容匹配的变更(upsert-call、update-request、resolve-approval、settle-request、remove-request、remove-call),由 Main 在 SQL 里执行,界面对已加载的页用同一份参考实现处理。任务刷新后的对账也改在 Main 里用 SQL 完成。旧 localStorage 记录改成增量变更迁移,界面不再调用replace。
npm run perf:main,2.4 万条活动记录,两轮结果一致(括号内为 Main 事件循环最大延迟):
| 路径 | 之前 | 之后 |
|---|---|---|
| 启动读取 | 整份 get:14.2 MB,178 ms(198 ms),在 Main 上执行 |
首页 + 汇总:114 KB,16 ms,在 worker 上执行(Main 0.6 ms) |
| 首页 200 条(同步路径,参考) | — | 1.0 ms(1.3 ms) |
| 汇总(同步路径,参考) | — | 14 ms(15.6 ms) |
| 保存 1 条变化 | 整份 replace:98 ms(114 ms) |
update:3.9 ms(6.9 ms) |
| 每次保存的载荷 | 整份列表(约 14 MB) | 只含变化的记录,与总量无关(单元测试:3 条变化 < 1.5 KB) |
测试:随机动作序列(含翻页、截断、刷盘和注入的保存失败),在真实数据库上与改动前的整份列表逻辑逐步对比,最终数据库与整份 replace 的结果一致(含重复 ID 和移到最前);启动后界面不超过一页;保存载荷只取决于变化的记录(含未加载的);随机数据集(含重复 ID)下,分页加汇总渲染出的计数、分组、标题、状态和清空数量与整份列表渲染一致;重启后遗留的进行中记录被标为中断,说明文本相同;清空后数据库、界面和汇总都为空;SQL 变更语义与参考实现一致。perf:app 60 项阈值全部通过。
后续(2026-10-04):汇总覆盖全部已加载会话,超过 5,000 个时分多次请求再合并,不再截断(单元测试:5,003 个会话分 5,000 + 3 两次请求,全部有标题和状态)。分组里“会话活动数”统计的仍是已加载的记录,和以前统计已显示的记录一致。
7.2.2 Main 写库与启动:真实数据测量(2026-10-04,PERF-15)
用便携版 dist/GoodBuddy-windows-x64/data 的副本测量(源目录从不被打开):assistant.sqlite 2.4 GB,175.5 万条任务事件(其中 148 万条子任务进度、1.1 GB),4,812 条消息(metadata 约 400 MB,单条最大 4 MB),2.4 万条活动记录。新增 npm run perf:write(build/run-write-perf.cjs,按真实数据中最忙一秒的速率 270 条/秒回放真实事件),perf:startup 新增 real 变体(GB_PERF_STARTUP_REAL_DATA,每次启动用新副本,外网请求全部拦截)。
测到的问题:
- 每次写库都按
synchronous = FULL同步落盘,单次约 2.4 ms,与写入内容无关:子任务进度、工具、文本批、会话保存、活动记录都一样。 - SQLite 自动 checkpoint 发生在越过 1,000 页的那次提交里:大库上每隔几百次写入就有一次卡住 Main 100–350 ms(3 轮里 18 次超过 50 ms)。
- 启动时两条恢复语句、一条图片操作恢复语句各扫描全部消息的 metadata:在 Main 上同步约 2.3 秒,启动到输入框可用 3.7 秒。
改动:
- 频繁的本地写入改用
synchronous = NORMAL:本地运行事件(生命周期事件status/done/error除外)、渲染进程 500 ms 一次的会话保存、活动记录增量。WAL 模式下仍是原子提交,应用崩溃不丢;断电最多丢最近一段这类写入,任何 FULL 提交及其之前的数据不受影响。远程事件(Agent ACK 依赖其持久性)、通道投递、任务状态等其余写入仍为 FULL。 - WAL checkpoint 移到 worker 线程:复用只读查询 worker 的入口,每 250 ms 做一次 PASSIVE checkpoint;运行期间 Main 的自动 checkpoint 阈值提到 4,000 页作为兜底(持续写入时被动 checkpoint 追不上 WAL 重置点),worker 退出即恢复默认 1,000。
- 启动恢复按索引读候选消息:第 58 版 schema 新增部分索引
messages_recovery_idx(流式中,或 metadata 含未结束的工具、子任务、图片操作状态);没有用户队列项时跳过按 queueItemId 的全表删除。metadata 只由JSON.stringify写入,状态总以紧凑形式出现,精确判断仍在候选行上用原逻辑执行。一次性迁移在升级 worker 中执行,真实数据约 2 秒。
结果(真实数据):
| 指标 | 之前 | 之后 |
|---|---|---|
| 单次写库 p50(子任务 / 工具 / 文本 / 会话保存 / 活动记录) | 2.2–2.6 ms | 0.1–0.3 ms |
| 写入期间 Main 最长阻塞(270 条/秒,3 轮) | 8–346 ms,超过 50 ms 共 37 次 | 2–34 ms,超过 50 ms 0 次 |
启动:Main 上的初始化(assistant-db-init) |
2,564 ms | 232 ms |
| 启动:到输入框可用(中位数,4 次) | 3,702 ms | 1,359 ms |
| 启动:侧栏出现会话 | 4,081 ms | 1,749 ms |
| 启动期间 Main 最长阻塞 | 2.5–2.8 s | 236–257 ms |
部分索引的代价:metadata 更新时如果新旧值都命中候选条件,要多维护一次索引(4 MB metadata 的更新 36 → 59 ms,233 KB 的 0.7 → 1.6 ms);不命中时没有额外开销。启动时剩下的约 0.6 s 在本地工具环境探测(已在后台初始化,不阻塞 Main)。
未做:读取最大的会话(407 条消息、42 MB metadata)在 worker 上约 170 ms,不阻塞 Main,但切换到这个会话时要等这么久;需要按需加载 metadata,单独立项。
7.2.3 监督回顾初始化(2026-10-04,PERF-15)
真实 2.43 GB 隔离快照的全局七天回顾在调用模型前阻塞 Main:来源清单的 25 次扫描都处于同一写事务。修复复用既有 Worker 通信和打包入口,以独立连接单次扫描读快照,再每次写入最多 200 条元数据;初始化期间可受理暂停、取消,未完成清单在继续时重建。最终结果和 checkpoint 仍按原事务一起提交。
原版与修复版各三轮交替测量,周范围初始化从 16.641–17.911 秒降至 1.099–1.146 秒,最慢 IPC 从 16.642–17.928 秒降至 8.1–14.7 ms。最终取消检查补充后的复核为初始化 1.122 秒、IPC 4.7 ms。来源逐条一致;小范围、取消、并发数据库读写和测量限制统一记录在监督者进度。完整发布仍在 Main,未把此修复计作全库写入异步化完成。
7.3 体验倒退复查(2026-10-03)
逐一复查了所有性能提交对交互体验的影响,并新增原则 P10“体验不倒退”。发现并修复:
| 问题 | 来源 | 修复 | 验证 |
|---|---|---|---|
| “到底部”按钮的平滑滚动动画消失,直接跳到底部 | fdf7d83 消息窗口化:贴底跟随、ResizeObserver、锚定修正都会瞬间写入 scrollTop,打断平滑滚动;点击时还立即切换窗口导致可见行被卸载 |
滑动期间暂停所有滚动写入,只随视口重新窗口化;到达底部、scrollend 或 1.5 秒兜底时再贴底;用户滚轮、按键或点击会接管滚动 |
真实 Electron 逐帧采样,要求有中间帧(旧代码中间帧为 0),连跑 3 次通过;新增单元测试,旧代码失败 |
| 消息滚出可见区后丢失状态:待回答问题里已选的选项和输入的内容、展开的工具详情和推理、Mermaid 重新绘制并显示“加载中” | fdf7d83 |
有待回答问题或待审批的行、用户交互过的最近 8 行、选区两端所在的行保持挂载;Mermaid 按“主题 + 源码”缓存渲染结果(最多 64 个) | 单元测试,旧代码失败 |
| 跨多条消息选择文本时,选区起点被卸载;复制时只拿到已挂载的文字 | fdf7d83 |
选区两端所在的行保持挂载;复制时,未挂载的消息用原文补全 | 单元测试 |
| 侧栏在上方插入、删除、置顶或 5 秒重排时,可见的会话行跳动 | 663aa85,窗口化关闭了原生滚动锚定 |
会话 id 变化时,在绘制前按第一条可见行恢复位置;列表在顶部时不锚定 | 单元测试,旧代码失败 |
| 笔记、文件树、知识库文档表格的锚定修正可能和浏览器原生锚定冲突 | b2eb86a |
窗口化状态下关闭 overflow-anchor |
单元测试 |
| 终端打字回显多等一帧;点击“清屏”前到达的输出在清屏之后才显示 | f4458af |
空闲时立即写入,只合并同一帧内后续到达的输出;清屏前先写完待写入的输出 | 单元测试 |
| 远程 Runtime 流式输出最多 50 ms 一批,显示一顿一顿 | d859cbf |
合并间隔改为 16 ms;写库成功后立即转发,仍然是先写库再转发 | 单元测试 |
| 流式输出时从侧栏复制或导出会话,可能缺最后不超过 250 ms 的文字 | PERF-13 流式缓冲 | 复制、导出前先把待同步的流式文字刷进状态 | 单元测试,旧代码失败 |
检查后没有问题:CSS 动画和过渡没有被删改;其余 scrollIntoView 的行为和原来一致;标题跑马灯、侧栏 5 秒重排的悬停冻结、重命名和菜单行保持挂载;输入框自动增高;Markdown 和 KaTeX 渲染结果。快速大跨度跳转是否出现空白帧也做了实测:在 perf:app 滚动场景中加了检查,CPU 降速 4 倍时 10 次跳转中空白帧为 0,所以没有改动。
已知限制(不是倒退,原来也没有):读屏软件的浏览模式访问不到未挂载的消息;应用本身没有页内查找功能。
修复记录:文件树窗口化最初以 .assistant-sidebar__body 为滚动容器,但真实布局中滚动的是停靠的 .workspace-files__files-view,真实 Electron 测试发现后改为优先使用后者。
PERF-14 状态
- 优先级 / 状态: P2 / 进行中(消息时间线和侧栏已窗口化;页内查找等限制未解决)
- 实施条件:
PERF-13完成会话与消息迁移,行组件 props 已稳定。 - 范围: 消息时间线和会话侧栏优先;保留滚动位置、跳转、展开状态和“加载更早消息”语义。
替换新面板分两步渲染(
09c415b);侧栏 5 秒重排状态随侧栏迁出 App。 - 验收: 5,000 条消息和 1,000 个会话场景下 DOM 节点数有界,滚动与切换指标改善。
PERF-15 存储接口异步化
2026-10-05 决策更新:KISS 局部修复已以 b90ff50f 提交。
用户随后确定 PERF-15/PERF-16 及必要守护按一个完整架构候选交付,不拆成多轮局部产品交付。
职责与 KISS 边界以桌面存储与并发执行为准,
多项目、多会话、多子任务的共同验收见并发规范。
当前候选已完成存储宿主及主要生产组合,完整迁移验收尚未完成;以下编号保留为职责索引,不表示分别验收的发布阶段。实现状态汇总见存储升级实施记录。
- 优先级 / 状态: P1 / 进行中(存储域接口和主要生产调用已异步化;完整调用清理、异常路径和 C01-C07/A01-A10 验收仍未完成)
- 范围: 为桌面业务存储建立异步领域接口,保留完整事务和先写后读顺序;包含 Assistant、 Knowledge、附件、桌面 Runtime 绑定和本机原生客户端账本。跨边界调用方等待实际完成;同步缓存读取 与内部纯计算无需变成 IPC。已完成的摘要、SQL 搜索、活动增量和远程事件批处理继续复用。
- 验收: 与 PERF-16 一起通过 SA-01~SA-07;只增加 Promise 或保留 Main 同步执行不计完成。
PERF-16 数据进程
- 优先级 / 状态: P1 / 进行中(
utilityProcess宿主已接入 Assistant、Knowledge、文件和 Runtime 域;全量生产路径与并发验收仍未完成) - 接口条件: 每条跨进程业务操作具备异步调用及事务边界;与 PERF-15 在同一候选完成和验收,不单独交付半迁移状态。
- 范围: 目标为统一管理业务 SQLite 的
utilityProcess,承接会话、知识库、Story Graph、 远程 Runtime 事件持久化和活动记录的业务事务;Main 只做校验与转发。重查询由受管只读 Worker 分担,解析、OCR 与 Runtime 执行保持独立,每个数据库明确一个写入所有者;不合并现有数据库。 职责、生命周期及 VS Code 对照以桌面存储方向为准。 - 验收: Main 无目标业务 SQLite 直接或间接访问、无同步回退;多项目/会话/子任务、重查询、 导入及发布重叠时满足性能原则。完整矩阵、取消、故障、旧库和平台要求按并发验收执行。
PERF-17 架构守护
- 优先级 / 状态: P2 / 部分实施(已有 lint 与历史回归上限;间接访问、单调收紧及并发性能检查待补)
- 范围: 按实际执行环境检查 Main 同步文件操作、直接和间接 SQLite 打开;存储宿主、受管 Worker、
测试及远端 Agent 的合法连接按并发验收的自动检查规则列明,
不按目录一概禁止。禁止组件直接订阅
window.goodbuddy.*.on*,限制App.tsx规模, 为PERF-11及并发场景建立符合性能原则的检查。 - 验收: 违反规则的改动在 lint 或基准阶段失败;本次完整架构所需守护随同一候选交付。 历史性能回归上限不能替代规范预算,无 CI/性能环境结果时不标记验收通过。
8. 体验与可访问性
UX-01 统一执行文案
- 优先级 / 状态: P1 / 旧模式文案工作由统一执行替代,完整验收见进度。
- 范围:
- 删除工作模式、安全分类及一般工具审批策略说明。
- 统一表达为使用当前账号权限和已接入能力执行,调用记录到活动。
- 同步定时任务、Composer、Runtime、帮助文案及中英文资源。
- 验收: 各入口说明与无模式 Runtime 契约一致,清除数据入口和确认行为可用。
UX-02 Runtime 显式状态
- 优先级 / 状态: P1 / 已完成
- 范围: 顶栏以状态点和 Runtime/模型名称紧凑表达状态:就绪为绿色,连接中为黄色,
不可用为灰色;不重复显示状态文字,并通过
aria-describedby暴露完整原因。 - 验收: 三种状态均不重复显示状态文字;发送不可用原因仍由发送控件的可访问说明提供。
UX-03 等待审批实时通知
- 优先级 / 状态: P1 / 已完成
- 范围: 审批卡首次出现时用 polite live region 播报等待状态和工具名;不抢焦点。
- 验收: 屏幕阅读器能发现新审批,右侧栏数量变化也有可访问状态。
UX-04 长任务阶段播报
- 优先级 / 状态: P1 / 已完成
- 范围: 低频阶段使用
role="status",失败终态使用role="alert",不逐字播报正文。 - 验收: 准备、等待、完成和失败可被辅助技术理解,流式输出不会形成播报风暴。
UX-05 Composer 错误关联
- 优先级 / 状态: P1 / 已完成
- 范围: 附件和上下文错误使用稳定 ID、
role="alert"、aria-describedby和适当的aria-invalid。 - 验收: 键盘和屏幕阅读器用户能知道发送失败原因及对应操作区域。
UX-06 设置离开确认焦点
- 优先级 / 状态: P1 / 已完成
- 范围: 确认出现时滚动到可见区域并聚焦“继续编辑”,或改用共享确认对话框。
- 验收: 在长设置页底部切换分类、关闭设置和应用导航时均不会出现不可见确认。
UX-07 SSH Host 读取失败状态
- 优先级 / 状态: P1 / 已完成
- 范围: 读取失败与真实空列表分开,失败状态提供重新读取。
- 验收: 暂时读取失败不会诱导用户重复添加 Host,重试成功后恢复原列表。
UX-08 工作区预览重试
- 优先级 / 状态: P2 / 已完成
- 范围: 加载使用
role="status"和aria-busy;失败保留路径并提供重新读取。 - 验收: 网络抖动后无需返回目录重新查找文件即可恢复预览。
UX-09 最近对话空状态
- 优先级 / 状态: P2 / 已完成
- 范围: 区分首次为空、搜索无结果和加载失败,分别提供新建、清除搜索和重试。
- 验收: 三种状态文案和操作互不混用。
UX-10 共享加载状态语义
- 优先级 / 状态: P2 / 已完成
- 范围: 为共享
EmptyState增加 loading 变体,并迁移知识库、智能心跳和 SSH Host。 - 验收: 加载状态统一输出
role="status"、aria-live="polite"和aria-busy="true"。
UX-11 更新检查错误反馈
- 优先级 / 状态: P1 / 已完成
- 范围:
- 启动检查失败进入去重后的全局错误通知。
- 保留来源、网络和有界 HTTP 错误上下文。
- 打开下载页失败也显示通知。
- 验收: 不再以空
catch静默丢失用户可处理的更新错误。
UX-12 更新源与自动检查解耦
- 优先级 / 状态: P2 / 已完成
- 范围:
- 关闭启动检查后仍可选择 GitHub 或镜像。
- 手动检查和下载继续使用所选来源。
- 同步修订
UI-DESIGN.md当前相反的规则。
- 验收: “关闭自动检查、切换镜像、手动检查”流程通过测试。
9. 诊断与质量
OPS-01 桌面端有界诊断日志
- 优先级 / 状态: P1 / 已完成
- 范围: 轮转记录最近启动和 Runtime 失败的时间、组件、阶段和稳定错误码。
- 限制: 不记录 Prompt、凭据、文档正文或 Provider 原始响应。
- 验收: 重启后仍可定位最近一次人为制造的数据库或 Runtime 启动失败。
OPS-02 远端 Agent 有界诊断日志
- 优先级 / 状态: P1 / 已完成
- 范围: Detached Agent 保存有界轮转诊断,覆盖 Runtime 启动、退出、连接和恢复阶段。
- 验收: 人为制造远端失败后可导出根因阶段;关闭和轮转不会遗留无界文件。
- 限制: 不记录 accepted Prompt、模型密钥或完整模型配置。
OPS-03 反馈可选附加诊断
- 优先级 / 状态: P1 / 已完成
- 范围: 用户主动勾选后才附加诊断,提交前说明类别,保留现有截图和草稿重试。
- 验收: 默认反馈不附加诊断;勾选后只包含允许的有界记录。
- 依赖:
OPS-01;本批明确不自动读取或附加OPS-02的远端 Agent 记录, 远端记录通过 Agent 固定 CLI 独立导出。
QA-01 真实 Runtime E2E
- 优先级 / 状态: P1 / 待开始
- 范围:
- 直连模型一次有界真实请求。
- Bundled OpenCode 启动并执行文件任务。
- Continue 启动并完成一次任务。
- MCP 工具写入生产路径。
- 验收: 受保护发布候选任务运行通过,并记录精确模型调用次数和费用边界。
QA-02 本地兼容 Provider E2E
- 优先级 / 状态: P1 / 待开始
- 目标: 常规 CI 无需公网或真实模型凭据也能覆盖打包 Runtime 启动和工具路径。
- 范围: 提供最小本地 OpenAI 兼容测试 Provider,覆盖流式响应和工具调用。
- 验收: SDK、Runtime 二进制、Provider 适配或工具写入回归会使常规 CI 失败。
QA-03 Windows ARM64 覆盖纠正
- 优先级 / 状态: P1 / 待开始
- 范围:
- 文档明确 Windows ARM64 当前为交叉打包。
- 在真实设备验证 Electron、node-pty、Koffi、Canvas、OCR 和 Runtime 加载。
- 有可用 ARM64 Runner 后再声明原生 CI。
- 验收: 文档和发布声明不再把文件架构验证描述为原生运行验证。
- 外部依赖: 真实 Windows ARM64 设备或 Runner。
10. 每批完成标准
一个事项只有同时满足以下条件才可标记为“已完成”:
-
正常 UI 操作到达生产实现并产生预期结果,不以 mock、注入 fake 或禁用入口代替。
-
已覆盖本事项声明的可达失败场景,用户输入和可恢复上下文不会被静默丢弃。
-
新增或更新聚焦测试;功能变化后运行:
npm test npm run typecheck npm run lint -
生产构建或发布脚本变化后运行
npm run build。 -
改动涉及 GoodBuddy Agent 或 Desktop-to-Agent 生产路径时,按
AGENTS.md在共享 Linux x64 Test Host 验证当前源码或候选包;Host 不可达时明确记录阻塞,不能标记完成。 -
检查并更新受影响的 PRD、架构、设计、功能矩阵、构建和运维文档。
-
在批次记录中填写实际验证命令、结果、剩余限制和对应提交。
11. 批次记录模板
开始一个批次时,在本节末尾复制以下模板:
### 批次 N:标题
- 状态:进行中
- 范围:ITEM-01、ITEM-02
- 明确不包含:
- 开始日期:
- 完成日期:
- 外部依赖:
#### 验收结果
- [ ] 正常生产路径
- [ ] 声明的失败与恢复路径
- [ ] 聚焦测试
- [ ] npm test
- [ ] npm run typecheck
- [ ] npm run lint
- [ ] npm run build(如适用)
- [ ] 真实 Host 或目标平台验证(如适用)
- [ ] 相关文档更新
#### 结果与剩余限制
- 提交:
- 验证:
- 剩余限制:
批次 1A:体验一致性与 P1 诊断
- 状态:已完成
- 范围:
UX-01至UX-12、OPS-01至OPS-03 - 明确不包含:
REL-*、FUN-*、PERF-*、QA-*,以及初始 Renderer Bundle 优化 - 开始日期:2026-08-31
- 完成日期:2026-08-31
- 外部依赖:无
验收结果
- 正常生产路径
- 声明的失败与恢复路径
- 聚焦测试
npm testnpm run typechecknpm run lintnpm run build- 共享 Linux x64 Host 的当前源码 Agent 候选验证
- 相关文档更新
结果与剩余限制
- 提交:尚未提交
- 验证:306 个测试文件、3322 项测试通过,49 项按既有条件跳过;Node/Web TypeScript、 完整 ESLint、生产构建和 Renderer Bundle 预算均通过。共享 Linux x64 Host 使用隔离 installation 完成 bootstrap、attach、断开重连、controller resume、Runtime 进程启动、 CLI 诊断导出、重启读取、权限和诊断写失败隔离;真实文本模型调用 0 次。
- 剩余限制:真实 Host 本次没有通过 Agent-owned model bridge 执行 Prompt,因此未在该
Host 上覆盖 Prompt 期间的
runtime.starting/runtime.started诊断;三文件真实轮转仍由 聚焦测试覆盖。正式发布、功能增强、性能基线和 QA 路线均不属于本批。