diff --git a/docs/architecture/performance-principles.md b/docs/architecture/performance-principles.md new file mode 100644 index 00000000..95871fc0 --- /dev/null +++ b/docs/architecture/performance-principles.md @@ -0,0 +1,114 @@ +# GoodBuddy 性能原则 + +## 文档信息 + +- **适用范围:** Desktop(Electron Main、Preload、Renderer)及其后台进程和线程。 +- **用途:** 新功能设计和代码评审的约束。多个 agent 并行修改代码时,以本文为统一标准。 +- **关联文档:** `docs/roadmap/product-performance-experience-improvement-plan.md`(测量记录与实施进度)。 +- **参照:** VS Code 的进程分工和渲染做法。我们没有在同一台机器上实测过 VS Code,下面的预算按常见的流畅标准设定。 + +## 1. 性能预算 + +“流畅”用下面的数字判断,不靠感觉。“标准机”指不降速运行 `npm run perf:app`,“低端机”指加上 `GB_PERF_CPU_THROTTLE=4`。 + +| 指标 | 标准机 | 低端机 | +| --- | --- | --- | +| 按键到下一帧,p95 | ≤ 16 ms | ≤ 50 ms | +| 点击到界面响应(切换会话、打开面板),p95 | ≤ 100 ms | ≤ 300 ms | +| 稳定交互期间(打字、滚动、流式输出)超过 50 ms 的长任务 | 0 | 尽量为 0,总时长不超过场景时长的 5% | +| 流式输出、滚动时的帧间隔,p95 | ≤ 17 ms | ≤ 33 ms | +| Main 事件循环单次阻塞 | ≤ 16 ms | — | +| Main 事件循环延迟,p99 | ≤ 30 ms | — | +| DOM 节点数 | 不随会话数、消息数增长 | 同左 | + +标准机的阈值由 `npm run perf:check` 检查。低端机的数值目前只做记录,等结果稳定后再加进阈值检查。 + +## 2. 原则 + +### P1 Main 只做调度 + +Main 负责窗口、生命周期、权限和消息转发。数据库查询、文档解析、哈希、向量计算、批量文件读写,都放到 worker 线程或 `utilityProcess` 里执行。 + +- 新代码不能在 Main 里使用 `DatabaseSync` 和同步文件 API。lint 已有白名单,名单只能减少。 +- 暂时必须留在 Main 的同步操作,单次耗时要能证明在 16 ms 以内,并在代码里注明理由。 +- 耗时可能超过 16 ms 的操作,必须支持取消和进度上报,不能一直占着 Main。 + +### P2 界面状态局部订阅 + +状态按领域放进外部 store,组件通过 selector 只订阅自己用到的那一部分。 + +- 不再往 `App.tsx` 里加 `useState`、`useEffect` 或领域逻辑。`App.tsx` 有行数上限,只能往下调。 +- IPC 订阅放在 store 模块里,组件不直接调用 `window.goodbuddy.*.on*`。 +- 一个按键、一个流式片段,只能让显示它的那一小块界面重新渲染。 +- 传给 memo 组件的回调和 context 值,引用必须稳定。 + +### P3 高频数据走专门通道,按帧合并 + +流式文本、终端输出、进度、滚动这类高频数据,不进全局状态。 + +- 多条更新合并后,每帧最多提交一次;同步到全局状态的频率要更低,目前是 250 ms。 +- 高频路径上不能读取布局(`getBoundingClientRect`、`scrollHeight` 等),布局读取要集中到每帧一次。 +- 合并更新不能丢失、重复或打乱顺序。每条数据带序号,并有测试覆盖。 + +### P4 数据只有一份来源,只传变化部分 + +- 每类数据只有一个权威来源,由数据所在进程负责持久化。界面不把整份数据写回。 +- 跨进程的通知直接带上变化的内容,接收方不再“收到通知后重新查全量”。 +- 写入合并成批次和事务,不按每个事件单独开一个事务。 + +### P5 工作量和可见内容成正比 + +- 长列表只渲染可见部分,包括消息、会话、笔记、文件树和知识库列表。 +- 解析和渲染的结果要缓存,比如 Markdown 语法树。内容没变,就不重新计算。 +- 增量处理:只解析变化的部分。没有把握时,退回到慢但正确的完整处理,并记录在测试里。 + +### P6 启动只做首屏需要的工作 + +- 首屏之前只做显示应用壳和恢复上次界面所需的工作。 +- 其他初始化(Provider、Runtime、知识库、通知渠道等)按需执行,或等首屏显示后再空闲执行。 +- 后台模块在第一次使用时才加载。 + +### P7 先测量,再修改 + +- 先跑测量,拿到数据再改代码;每项改动都要有前后对比,基线用临时 worktree 构建。 +- 每个版本至少跑 3 轮,交替运行;结果差异小于同一版本多次运行之间的波动时,不能说成“有提升”。 +- 测量用隔离的用户目录和本地假模型,不发出外部模型请求,结果里不含用户内容。 + +### P8 正确性优先于速度 + +- 事件顺序、持久化、取消、崩溃恢复和显示结果都不能因为性能而改变。 +- 异步改造要先确认“先写入、后读取”的顺序,没有测试覆盖就不合并。 +- 风险高、收益小,或者会改变现有功能的优化先不做,在计划文档里记录理由。 + +### P9 用工具守住规则 + +- lint 规则和性能阈值是合并前的硬性条件,不靠人工记得。 +- 规则只能收紧,白名单和上限只能往下调。 +- 每个 agent 只提交自己改的文件和片段,不把别人未提交的改动带进自己的提交。 + +## 3. 评审清单 + +提交性能相关改动,或者会影响热点路径的功能改动时,逐项确认: + +1. Main 里有没有新增同步数据库调用、同步文件 IO,或者耗时可能超过 16 ms 的计算? +2. 有没有往 `App.tsx` 里加状态?一次按键或一个流式片段,会让多大范围的界面重新渲染? +3. 高频路径上有没有读取布局,有没有每次事件都提交状态? +4. 有没有整份数据回写,或者“收到通知后重新查全量”? +5. 列表的 DOM 数量会不会随数据量增长? +6. 有没有在启动阶段新增同步初始化? +7. 是否附上了 `perf:app`、`perf:main` 或 `perf:knowledge` 的前后对比? +8. 顺序、取消、持久化和恢复,有没有测试覆盖? + +## 4. 当前差距 + +截至 2026-10-03,按原则对照的主要差距: + +| 原则 | 差距 | +| --- | --- | +| P1 | 写入和大量同步数据库调用仍在 Main;没有独立数据进程。文档解析、分块和大文件哈希已移到 worker | +| P2 | `App.tsx` 约 9,600 行,当前会话更新时 App 仍会整体重新渲染 | +| P3 | 已基本满足:流式输出和终端输出都已按帧合并 | +| P4 | 界面仍发送整份活动记录;部分 `*Changed` 通知之后还会重新查全量 | +| P5 | 已基本满足:消息、会话、笔记、文件树、知识库文档表格都只渲染可见部分 | +| P6 | 已有基线(`npm run perf:startup`):启动到输入框可用约 1.0 秒,Main 模块加载约 415 ms、本地工具环境初始化约 155 ms,还没有优化 | +| P9 | 性能阈值还没有接入 CI,也没有真实低端机的基线 |